# PayPal Payouts

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 2-3 hours
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to PayPal Payouts by building a Firebase Cloud Function or Supabase Edge Function that handles OAuth 2.0 token exchange and batch payout creation — both use your PayPal client secret and must never run in the Flutter app. FlutterFlow API Calls hit your proxy to request payouts; the app then polls or streams payout status from Firestore or Supabase.

## PayPal Payouts sends money out — and that means keeping credentials on your server

Most PayPal integrations in mobile apps handle checkout — a customer pays and money flows in. PayPal Payouts is the opposite: it sends money from your platform account out to freelancers, affiliates, sellers, or contest winners. A single API call can disburse funds to hundreds of recipients at once, each identified by a PayPal email address. This is the tool behind 'referral bonus paid to your PayPal' or 'your earnings have been transferred' experiences.

The authentication model is OAuth 2.0 client-credentials. Your PayPal app has a Client ID and a Client Secret. Step one is exchanging those credentials for a Bearer access token at POST /v1/oauth2/token. Step two is creating a payout batch at POST /v1/payments/payouts with the Bearer token in the Authorization header. Both the Client Secret and the Bearer token are sensitive — the Client Secret because it can generate tokens indefinitely, and the Bearer token because it authorises money movement. Placing either in a FlutterFlow API Call header bakes them into the compiled app bundle, where they can be extracted.

The correct architecture: deploy a Firebase Cloud Function or Supabase Edge Function that holds the Client ID and Client Secret. FlutterFlow makes a simple API Call to your proxy with the payout details (recipient email, amount, currency). The proxy authenticates to PayPal, creates the batch, and returns the batch payout ID. Payouts are asynchronous — a 201 response means 'batch accepted', not 'money sent'. You then poll the batch status or use a PayPal webhook to detect when each payment reaches SUCCESS or DENIED status, and write that into Firestore or Supabase for the app to read.

PayPal charges payout fees that vary by region, recipient type, and payout method — check developer.paypal.com for current fee schedules. The sandbox environment (api-m.sandbox.paypal.com) is completely separate from live and uses different credentials; you can create sandbox business and personal accounts in the PayPal Developer Dashboard to simulate full payout flows without moving real money.

## Before you start

- A PayPal Business account (personal accounts cannot use the Payouts API)
- A PayPal Developer account at developer.paypal.com with a REST API app created — note your Client ID and Client Secret for both sandbox and live
- PayPal Payouts must be enabled on your account — contact PayPal support if the Payouts product is not available in your developer dashboard
- A Firebase project (Blaze plan) or Supabase project to host the backend proxy function
- A FlutterFlow project with Firestore or Supabase configured for streaming payout status to the app

## Step-by-step guide

### 1. Create a PayPal REST app and note your sandbox credentials

Go to developer.paypal.com and sign in with your PayPal Business account. Navigate to Apps & Credentials. Click Create App, give it a name (e.g. 'MyPlatform Payouts'), select Business for the app type, and click Create App. You'll land on the app details page showing two credential sets: Sandbox and Live. Each has a Client ID and a Client Secret. For development, use the Sandbox credentials — they work against api-m.sandbox.paypal.com and don't touch real money. Copy both the Sandbox Client ID and Sandbox Client Secret somewhere secure (you'll add them to your Firebase or Supabase environment variables, not to FlutterFlow). Also check the Payouts feature on the app settings page — if you see a toggle for 'Payouts', make sure it is enabled. If you don't see the Payouts option, your PayPal account may need Payouts activated; contact PayPal Business support and explain you're building a disbursement integration. Under your Developer Dashboard, also create a Sandbox Business account and one or more Sandbox Personal accounts — you'll use the Personal accounts as payout recipients during testing.

**Expected result:** You have a PayPal REST app with Sandbox Client ID and Client Secret. Payouts is enabled. You have at least one Sandbox Personal account to act as a payout recipient during testing.

### 2. Deploy a Firebase or Supabase proxy that authenticates and creates payouts

Create a Firebase Cloud Function or Supabase Edge Function with a single HTTP endpoint your FlutterFlow app will call. The function must handle two things internally: first, call POST https://api-m.sandbox.paypal.com/v1/oauth2/token with your Client ID and Client Secret (Base64-encoded as Basic Auth: Authorization: Basic base64(clientId:secret)) and grant_type=client_credentials in the body. This returns an access_token (a Bearer token). Cache this token in the function's in-memory state — it is valid for several hours, so there's no need to request a new one on every payout call. Second, using the Bearer token, call POST https://api-m.sandbox.paypal.com/v1/payments/payouts with a JSON body containing: sender_batch_header (a unique batch ID, e.g. a UUID generated in the function), and an items array where each item has recipient_type (EMAIL), amount (value and currency), and receiver (the recipient's PayPal email). The payout fee is deducted from your platform PayPal balance — check PayPal's current fee schedule. Return the payout_batch_id to your FlutterFlow app so it can store and poll for status. Store both the Client ID and Client Secret in Firebase Functions config or Supabase secrets, not in code.

```
// Firebase Cloud Function: PayPal Payouts proxy (Node.js)
const functions = require('firebase-functions');
const axios = require('axios');
const cors = require('cors')({ origin: true });
const { v4: uuidv4 } = require('uuid');

const PAYPAL_BASE = 'https://api-m.sandbox.paypal.com'; // switch to api-m.paypal.com for live
let cachedToken = null;
let tokenExpiry = 0;

async function getAccessToken() {
  if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
  const clientId = functions.config().paypal.client_id;
  const secret = functions.config().paypal.client_secret;
  const credentials = Buffer.from(`${clientId}:${secret}`).toString('base64');
  const res = await axios.post(
    `${PAYPAL_BASE}/v1/oauth2/token`,
    'grant_type=client_credentials',
    { headers: { Authorization: `Basic ${credentials}`, 'Content-Type': 'application/x-www-form-urlencoded' } }
  );
  cachedToken = res.data.access_token;
  tokenExpiry = Date.now() + (res.data.expires_in - 60) * 1000;
  return cachedToken;
}

exports.createPayout = functions.https.onRequest((req, res) => {
  cors(req, res, async () => {
    const { recipient_email, amount, currency, note } = req.body;
    const token = await getAccessToken();
    const payout = await axios.post(
      `${PAYPAL_BASE}/v1/payments/payouts`,
      {
        sender_batch_header: {
          sender_batch_id: uuidv4(),
          email_subject: 'You have a payment',
        },
        items: [{
          recipient_type: 'EMAIL',
          amount: { value: amount, currency: currency || 'USD' },
          receiver: recipient_email,
          note: note || 'Payment from platform',
        }],
      },
      { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } }
    );
    res.json({ batch_id: payout.data.batch_header.payout_batch_id });
  });
});
```

**Expected result:** The proxy is deployed and reachable. Calling it with a POST containing recipient_email and amount returns a batch_id. You can verify the batch in your PayPal Sandbox dashboard under Activity → Payouts.

### 3. Configure FlutterFlow API Calls to hit your proxy

In FlutterFlow, click API Calls in the left navigation panel. Click + Add → Create API Group. Name it PayPalProxy. Set the Base URL to your deployed Firebase Cloud Function URL or Supabase Edge Function URL (e.g. https://us-central1-yourproject.cloudfunctions.net). No shared headers needed — authentication happens inside the proxy. Now add an API Call within this group: click + Add Call, name it CreatePayout, set Method to POST and Path to /createPayout (or whichever path your function handles). Go to the Variables tab and add three body variables: recipient_email (String), amount (String — use a string to avoid decimal precision issues; your proxy formats it correctly), and currency (String, default 'USD'). In the Body tab, configure as JSON with body { "recipient_email": "{{ recipient_email }}", "amount": "{{ amount }}", "currency": "{{ currency }}" }. In Response & Test, paste a sample response { "batch_id": "ABCDEF123456" } and click Generate JSON Paths to auto-detect $.batch_id. Add a second API Call named GetPayoutStatus: Method GET, Path /getPayoutStatus (add this to your proxy), Variable: batch_id (String, query param). In Response & Test paste { "batch_status": "SUCCESS", "items": [{ "transaction_status": "SUCCESS" }] } and generate paths. Save both calls.

```
// Sample responses to paste into Response & Test tab:

// CreatePayout response:
{
  "batch_id": "ABCDEF1234567890"
}

// GetPayoutStatus response:
{
  "batch_status": "SUCCESS",
  "items": [
    {
      "payout_item_id": "ITEM123",
      "transaction_status": "SUCCESS",
      "payout_item": {
        "receiver": "recipient@example.com",
        "amount": { "currency": "USD", "value": "50.00" }
      }
    }
  ]
}
```

**Expected result:** Two API Calls appear under PayPalProxy: CreatePayout and GetPayoutStatus. Each shows auto-detected JSON paths in the Response & Test tab. Test sends return the expected sample responses.

### 4. Store the batch ID and poll for payout status in the app

Payouts are asynchronous: calling CreatePayout returns a batch_id immediately, but the actual disbursement happens within minutes (sandbox) to a few hours (live). Your FlutterFlow app needs to track the status. When the CreatePayout API Call succeeds, store the returned batch_id in an App State variable (String) and also write a Firestore document or Supabase row for the payout: { batch_id, recipient_email, amount, currency, status: 'PENDING', created_at }. Bind a Text widget on the 'Payout History' screen to stream from that Firestore collection or Supabase table filtered to the current user. To update the status, add a scheduled Firebase Cloud Function (Cloud Scheduler) or a Supabase pg_cron job that runs every 5 minutes, calls GET https://api-m.sandbox.paypal.com/v1/payments/payouts/{batch_id} with the cached Bearer token, and updates the Firestore/Supabase document status to 'SUCCESS', 'DENIED', or 'PROCESSING'. Alternatively, set up a PayPal webhook (developer.paypal.com → Apps → Webhooks → Add webhook, event PAYMENT.PAYOUTS-ITEM.SUCCEEDED) pointing at a Firebase/Supabase endpoint that updates the document status directly. Show the user a live status indicator: 'Processing' (yellow), 'Paid' (green), 'Failed — contact support' (red). Never surface the raw PayPal batch_id to end users.

```
// Firestore document structure for payout tracking:
// Collection: 'payouts'
// Document ID: batch_id (from PayPal)
{
  "batch_id": "ABCDEF1234567890",
  "recipient_email": "user@example.com",
  "amount": "50.00",
  "currency": "USD",
  "status": "PENDING",   // PENDING | PROCESSING | SUCCESS | DENIED
  "user_id": "ff_user_id_xxx",
  "created_at": "2026-07-10T10:00:00Z",
  "updated_at": "2026-07-10T10:03:00Z"
}

// JSON path to bind status in FlutterFlow stream:
// Firestore → Collection → payouts → where user_id == currentUser.uid
// Bind Text widget to documents[0].status
```

**Expected result:** After tapping 'Cash Out', the app writes a PENDING payout record to Firestore/Supabase and shows 'Processing'. Within minutes (sandbox) the status updates to 'SUCCESS' when polled or when the webhook fires, and the UI shows 'Paid'.

### 5. Test in sandbox and switch to live credentials

In the PayPal Developer Dashboard, navigate to your Sandbox Personal accounts (under Accounts). Each sandbox personal account has a PayPal email address you can use as the payout recipient during testing. Call your proxy's CreatePayout endpoint with one of these sandbox emails as recipient_email and a test amount (e.g. '10.00'). The sandbox responds as if real money was sent — you can see the transaction in the sandbox account's Activity tab. Check the batch status by calling your GetPayoutStatus endpoint or the PayPal sandbox API directly with the batch_id returned. Confirm the status reaches 'SUCCESS'. Then test the error cases: try sending to a non-existent email (expect DENIED with item error RECEIVER_UNREGISTERED) and to an email without a PayPal account (expect UNCLAIMED). Once the full flow works in sandbox, switch to live by: (1) updating your Firebase Functions config / Supabase secrets to use live Client ID and Client Secret; (2) changing PAYPAL_BASE in your function from api-m.sandbox.paypal.com to api-m.paypal.com; (3) ensuring your PayPal Business balance has sufficient funds for the payouts. Never test live payouts with production recipient emails until the sandbox flow is fully validated.

```
// Sandbox vs Live base URLs:
// Sandbox: https://api-m.sandbox.paypal.com
// Live:    https://api-m.paypal.com

// Update this single variable in your Cloud Function or Edge Function:
const PAYPAL_BASE = process.env.PAYPAL_ENV === 'live'
  ? 'https://api-m.paypal.com'
  : 'https://api-m.sandbox.paypal.com';

// Set environment via Firebase:
// firebase functions:config:set paypal.env="live" paypal.client_id="live_id" paypal.client_secret="live_secret"
```

**Expected result:** Sandbox tests show payouts reaching 'SUCCESS' status for valid recipient emails and 'DENIED' for invalid ones. After switching to live credentials, a small test payout to your own PayPal email arrives successfully before you open the feature to real users.

## Best practices

- Never place your PayPal Client Secret or any Bearer token in a FlutterFlow API Call header — route all PayPal authentication and payout calls through a Firebase Cloud Function or Supabase Edge Function.
- Cache the OAuth Bearer token in your backend proxy until it expires (typically several hours) rather than requesting a new token on every payout call — this avoids unnecessary token-endpoint requests and stays clear of rate limits.
- Always validate the payout recipient email in your backend before calling PayPal — UNCLAIMED payouts tie up funds for 30 days and create poor user experiences.
- Implement idempotency in your proxy using a unique sender_batch_id (UUID) per batch — if your function is retried on error, PayPal will return the original response rather than creating a duplicate payout.
- Use the PayPal sandbox (api-m.sandbox.paypal.com) with sandbox accounts for all development and QA testing — switch to live credentials only after thorough sandbox validation.
- Add a server-side rate-limit or queuing mechanism (e.g. Firestore-backed queue + Cloud Scheduler) for the batch creation endpoint — the ~1 batch per 30 second throttle can easily be hit in a busy app.
- Store payout records (batch_id, recipient, amount, status, timestamps) in Firestore or Supabase keyed to your user's ID for audit trails — never rely solely on the PayPal dashboard for payout history.
- Handle the async nature of payouts in the UI: show 'Processing' immediately after CreatePayout succeeds, and only show 'Paid' after the webhook or poll confirms SUCCESS — a 201 response means accepted, not disbursed.

## Use cases

### Gig marketplace app that pays out earnings to freelancers weekly

A FlutterFlow app lets freelancers track completed jobs and see their pending earnings. On a weekly schedule the backend compiles all approved earnings into a PayPal payout batch. Freelancers see a 'Payout sent to your PayPal' notification pushed through Firebase Cloud Messaging when the batch status reaches SUCCESS.

Prompt example:

```
Show freelancers their current unpaid balance. When the admin triggers a payout, send all eligible balances to each freelancer's registered PayPal email and update their payout history screen.
```

### Referral rewards app that pays affiliates on conversion

Users earn referral bonuses when someone they referred makes a purchase. When the bonus threshold is reached and the user taps 'Cash Out', the FlutterFlow app calls the backend to queue a PayPal payout. The app shows the payout status live from Supabase — 'Processing', 'Paid', or 'Failed'.

Prompt example:

```
Build a 'My Earnings' screen showing total referral earnings and a 'Cash Out' button. When tapped, create a PayPal payout for the balance and update the status in real time.
```

### Contest platform that disburses prize money to winners

A creative contest app collects entries and lets the community vote. When voting closes and winners are confirmed by an admin, the app triggers a batch payout to the top three winners' PayPal accounts in different amounts. Results and payout confirmation are displayed on a 'Winners' screen.

Prompt example:

```
After an admin marks winners, automatically send prize money to each winner's PayPal email — first place $500, second $200, third $100 — and show each winner their payout status in the app.
```

## Troubleshooting

### Proxy returns 401 Unauthorized when calling PayPal's token endpoint

Cause: The Client ID or Client Secret is wrong, or they are from the wrong environment (sandbox credentials used against the live host or vice versa).

Solution: Check your Firebase Functions config or Supabase secrets to confirm the paypal.client_id and paypal.client_secret match the environment (sandbox vs live) of the PAYPAL_BASE URL in your function. The Basic Auth string must be base64(clientId:secret) with a colon separator and no spaces.

### CreatePayout returns 429 Too Many Requests

Cause: You are hitting PayPal's batch creation rate limit, which allows approximately one batch per 30 seconds.

Solution: Add a debounce or queue mechanism in your app. In the 'Cash Out' button's Action Flow, disable the button immediately after the first tap using an App State boolean (isPayoutInProgress: true) and only re-enable it after the API Call completes. For high-volume platforms, queue payout requests in Firestore and process them in a backend Cloud Function with a 30-second minimum gap between batches.

### Payout item status shows UNCLAIMED even though the email looks correct

Cause: The recipient's email address is not registered with a PayPal account. PayPal will hold the funds for 30 days and then return them to the sender.

Solution: Before creating a payout, optionally call the PayPal Merchant API to check if an email is registered as a PayPal user. In practice, most platforms surface this UNCLAIMED status in the app as 'Recipient email not linked to a PayPal account — please update your payment email in profile settings' and return the funds automatically after PayPal's 30-day hold.

### FlutterFlow API Call to proxy returns 'XMLHttpRequest error' in web preview

Cause: The deployed Cloud Function or Edge Function does not have CORS headers configured, so the browser blocks cross-origin requests from FlutterFlow's web preview.

Solution: Ensure your Firebase function uses the cors() middleware (as shown in the proxy code above) or your Supabase Edge Function sets Access-Control-Allow-Origin: * in the response headers. On native mobile builds this CORS error does not occur — only in browser-based previews.

```
// Add to Supabase Edge Function (Deno):
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
if (req.method === 'OPTIONS') {
  return new Response('ok', { headers: corsHeaders });
}
```

## Frequently asked questions

### Is PayPal Payouts available to all PayPal Business accounts?

Not automatically. PayPal Payouts must be enabled on your account, and in some regions it may require review or approval. Log in to your PayPal Business account, go to Account Settings → Website Payments, and check if Payouts is listed. If not, contact PayPal Business support and explain your use case (disbursing earnings to users). The Developer Dashboard app will show the Payouts capability once it is approved.

### Can I send payouts in currencies other than USD?

Yes. PayPal Payouts supports multiple currencies. In the amount field of each payout item, set the currency code (e.g. EUR, GBP, CAD). Your PayPal Business account must hold a balance in that currency or be able to convert from your primary currency. International payouts may incur additional conversion fees — check PayPal's current fee structure for cross-currency payouts.

### How long does it take for a PayPal payout to reach the recipient?

In the sandbox, payouts are credited instantly for testing purposes. In live mode, PayPal typically disburses payout funds within minutes to a few business hours, but the exact timing depends on PayPal's internal processing, the recipient's account standing, and whether the transaction triggers any risk checks. Design your app's UX around an asynchronous flow — show 'Processing' after creation and only confirm payment when the webhook or poll returns SUCCESS.

### What happens if a recipient's PayPal email is not registered?

The payout item receives a status of UNCLAIMED. PayPal sends the recipient an email inviting them to create a PayPal account and claim the funds. If they don't claim within 30 days, the funds are returned to your PayPal balance. Your app should surface this UNCLAIMED status to the user (e.g. 'Your payout is unclaimed — update your PayPal email in your profile') and prompt them to fix their payment email.

### Can I send a single payout to one person, or must it be a batch?

The PayPal Payouts API always creates a 'batch', even if that batch contains only one item. A batch with a single recipient is completely valid. The batch abstraction exists because the API is designed for mass disbursements — but there is no minimum number of items. Use a one-item batch for any individual payout request triggered from your app.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/paypal-payouts
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/paypal-payouts
