# How to Integrate Lovable with Payoneer

- Tool: Lovable
- Difficulty: Intermediate
- Time required: 35 minutes
- Last updated: March 2026

## TL;DR

Integrating Payoneer with Lovable uses Edge Functions to onboard payees, initiate payouts, and check payment status via the Payoneer Payouts API. Store your OAuth2 client credentials in Cloud Secrets, create Edge Functions to generate access tokens and send mass payouts, then build a payout dashboard for your Lovable app. Setup takes 35 minutes.

## Why integrate Payoneer with Lovable?

Payoneer solves one of the most painful problems in the global digital economy: paying freelancers, suppliers, and service providers in 190+ countries without the complexity of international wire transfers, high banking fees, and currency conversion losses. Unlike Stripe Payouts (US/EU focused) or PayPal (high fees in many countries), Payoneer specializes in cross-border business-to-business and business-to-individual payments with local bank transfer options that let recipients receive money in their home currency at favorable rates.

For Lovable developers, Payoneer integration is essential for building marketplace apps, freelance platforms, influencer payment systems, affiliate networks, and any application where your business needs to send money to people or businesses globally. Payoneer is the preferred payout method for sellers on Amazon, eBay, Fiverr, and Upwork — if your platform serves a global talent or seller base, many recipients already have Payoneer accounts and prefer it over bank wires.

The Payoneer Payouts API is the programmatic interface for sending payouts from your company's Payoneer account to registered payees. The flow involves: registering a payee (the recipient creates or links their Payoneer account via a registration link you generate), then initiating payouts from your company balance to their Payoneer account. Payoneer handles all the cross-border complexity — currency conversion, local banking relationships, and compliance.

## Before you start

- A Lovable project with Cloud enabled
- A Payoneer business account with API access enabled — contact Payoneer to request Payouts API access
- Your Payoneer API client_id and client_secret from the Payoneer Developer Dashboard
- Your Payoneer program ID (identifies your company's payout program)
- Understanding that Payoneer requires onboarding approval before API access — this can take days to weeks

## Step-by-step guide

### 1. Set up Payoneer API access and store credentials

Payoneer's API access is not self-serve — you need to apply through Payoneer's business development team. Start by creating a business account at payoneer.com if you don't have one. Then contact Payoneer's API support (developer.payoneer.com has a 'Contact API Sales' link) to request Payouts API access. You'll need to provide your use case, expected payout volume, and business details. Approval typically takes 5-15 business days.

Once approved, Payoneer provides access to their Developer Portal at developer.payoneer.com. Log in and create an API application under Applications → Create Application. Select 'Payouts' as the application type. You'll receive a client_id and client_secret. Also note your program_id (your company's unique payout program identifier).

Payoneer has a sandbox environment at sandbox.api.payoneer.com for testing. Request sandbox access alongside production access. In your Lovable project, open Cloud tab → Secrets and add: PAYONEER_CLIENT_ID, PAYONEER_CLIENT_SECRET, PAYONEER_PROGRAM_ID, and PAYONEER_ENVIRONMENT set to 'sandbox' or 'production'. The sandbox and production environments have completely separate credentials.

**Expected result:** PAYONEER_CLIENT_ID, PAYONEER_CLIENT_SECRET, PAYONEER_PROGRAM_ID, and PAYONEER_ENVIRONMENT stored in Cloud Secrets. Sandbox access confirmed from Payoneer.

### 2. Create an Edge Function for Payoneer OAuth2 authentication

Payoneer uses OAuth2 client credentials flow for API authentication. Your Edge Function exchanges the client_id and client_secret for a Bearer access token, which is then used in all subsequent API calls. Access tokens expire after a set period (typically 1 hour), so you need to handle token refresh.

Create supabase/functions/payoneer-auth/index.ts. The token endpoint is POST https://api.payoneer.com/v4/oauth2/token (production) or https://api.sandbox.payoneer.com/v4/oauth2/token (sandbox). The request uses Basic Auth with client_id as username and client_secret as password, with the body as application/x-www-form-urlencoded containing grant_type=client_credentials and scope=read write payouts.

To avoid calling the token endpoint on every API request, cache the token in a Supabase table with the expiry timestamp. Before making an API call, check the cache — if the token is still valid (more than 5 minutes from expiry), use the cached token. Otherwise, fetch a new one and update the cache. This reduces latency and avoids hitting Payoneer's token endpoint rate limits. Create a payoneer_token_cache table in Supabase with columns: token (text), expires_at (timestamp), and environment (text).

```
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

const CLIENT_ID = Deno.env.get("PAYONEER_CLIENT_ID") ?? "";
const CLIENT_SECRET = Deno.env.get("PAYONEER_CLIENT_SECRET") ?? "";
const ENVIRONMENT = Deno.env.get("PAYONEER_ENVIRONMENT") ?? "sandbox";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";

const TOKEN_URL = ENVIRONMENT === "production"
  ? "https://api.payoneer.com/v4/oauth2/token"
  : "https://api.sandbox.payoneer.com/v4/oauth2/token";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

export async function getAccessToken(): Promise<string> {
  const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);

  // Check cache
  const { data: cached } = await supabase
    .from("payoneer_token_cache")
    .select("token, expires_at")
    .eq("environment", ENVIRONMENT)
    .gt("expires_at", new Date(Date.now() + 5 * 60 * 1000).toISOString())
    .single();

  if (cached?.token) return cached.token;

  // Fetch new token
  const authString = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
  const res = await fetch(TOKEN_URL, {
    method: "POST",
    headers: {
      "Authorization": `Basic ${authString}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: "grant_type=client_credentials&scope=read+write+payouts",
  });

  if (!res.ok) {
    const err = await res.text();
    throw new Error(`Payoneer auth failed: ${err}`);
  }

  const data = await res.json();
  const expiresAt = new Date(Date.now() + (data.expires_in - 60) * 1000).toISOString();

  await supabase.from("payoneer_token_cache").upsert(
    { environment: ENVIRONMENT, token: data.access_token, expires_at: expiresAt },
    { onConflict: "environment" }
  );

  return data.access_token;
}

serve(async (req) => {
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }
  try {
    const token = await getAccessToken();
    return new Response(JSON.stringify({ token }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
  } catch (e) {
    return new Response(JSON.stringify({ error: (e as Error).message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
  }
});
```

**Expected result:** Edge Function returns a valid Payoneer access token. Second call within expiry window returns cached token without hitting the Payoneer token endpoint again.

### 3. Create an Edge Function to register payees

Before you can pay someone via Payoneer, they need to be registered as a payee in your program. The registration process involves generating a unique registration link for the payee, which they click to create or connect their Payoneer account. Once registered, Payoneer returns a payee_id that you use for all future payouts to that person.

Create supabase/functions/payoneer-register-payee/index.ts. This function calls the Payoneer Payees API to create a new payee record and get a registration link. The endpoint is POST https://api.payoneer.com/v4/programs/{programId}/payees (production) or the sandbox equivalent. The request body includes the payee's email, name, and an internal reference ID (your user's ID in Supabase).

Payoneer returns a payee object with a registration_link the payee must visit to connect their account, and a payee_id you save in Supabase. The registration link expires after 30 days and may need to be regenerated if the payee doesn't complete onboarding. After registration, check payee status via GET /payees/{payee_id} — status 'ACTIVE' means they can receive payouts. Show a 'Connect Payoneer' button in your app that opens this registration link in a new tab.

```
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

const PROGRAM_ID = Deno.env.get("PAYONEER_PROGRAM_ID") ?? "";
const ENVIRONMENT = Deno.env.get("PAYONEER_ENVIRONMENT") ?? "sandbox";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";

const API_BASE = ENVIRONMENT === "production"
  ? "https://api.payoneer.com/v4"
  : "https://api.sandbox.payoneer.com/v4";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

async function getToken(): Promise<string> {
  // Import from payoneer-auth or duplicate the logic inline
  const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
  const { data } = await supabase.from("payoneer_token_cache").select("token").eq("environment", ENVIRONMENT).gt("expires_at", new Date().toISOString()).single();
  if (data?.token) return data.token;
  throw new Error("No valid Payoneer token — call payoneer-auth first");
}

serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });

  try {
    const { userId, email, firstName, lastName } = await req.json();
    const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
    const token = await getToken();

    const res = await fetch(`${API_BASE}/programs/${PROGRAM_ID}/payees`, {
      method: "POST",
      headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        payee_id: userId,
        contact: { email, first_name: firstName, last_name: lastName },
      }),
    });

    if (!res.ok) {
      const err = await res.text();
      throw new Error(`Payoneer payee error: ${err}`);
    }

    const data = await res.json();
    await supabase.from("users").update({
      payoneer_payee_id: data.payee_id ?? userId,
      payoneer_registration_link: data.registration_link,
      payoneer_status: "PENDING",
    }).eq("id", userId);

    return new Response(
      JSON.stringify({ payeeId: data.payee_id, registrationLink: data.registration_link }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" } }
    );
  } catch (e) {
    return new Response(
      JSON.stringify({ error: (e as Error).message }),
      { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
    );
  }
});
```

**Expected result:** Edge Function registers payees and returns registration links. Test by creating a payee with a test email — the registration link should be a valid Payoneer URL.

### 4. Create payout Edge Function and build the payout dashboard

With payees registered and tokens cached, create the core payout Edge Function. This function takes a list of payout instructions (payee_id, amount, currency, description) and calls the Payoneer Payouts API to transfer money to registered payees.

The Payoneer Payouts API endpoint for creating a payout is POST https://api.payoneer.com/v4/programs/{programId}/payouts (production). Each payout has a unique client_reference_id (your payout ID), a payment_type ('Payout'), amount, currency, and the destination payee_id. Payoneer processes the payout and sends webhook events when the status changes. Track each payout's status by storing the Payoneer payment_id alongside your internal payout record.

Build a payout dashboard in Lovable showing pending payees, their earned balances, payout history, and a 'Run Payouts' button that triggers the batch payout function for all eligible payees. Show payout status (Pending, Processing, Paid, Failed) in real-time using Supabase realtime subscriptions. For failed payouts, show the failure reason and provide a retry button. For complex payout scenarios with split payments, foreign currency handling, or compliance holds, RapidDev's team can help implement the full payout workflow.

```
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

const PROGRAM_ID = Deno.env.get("PAYONEER_PROGRAM_ID") ?? "";
const ENVIRONMENT = Deno.env.get("PAYONEER_ENVIRONMENT") ?? "sandbox";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";

const API_BASE = ENVIRONMENT === "production"
  ? "https://api.payoneer.com/v4"
  : "https://api.sandbox.payoneer.com/v4";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });

  try {
    const { payouts } = await req.json() as { payouts: Array<{ payeeId: string; amount: number; currency: string; description: string; internalId: string }> };
    const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);

    // Get cached token
    const { data: tokenData } = await supabase.from("payoneer_token_cache").select("token").eq("environment", ENVIRONMENT).gt("expires_at", new Date().toISOString()).single();
    if (!tokenData?.token) throw new Error("No valid token — refresh authentication");

    const results: Array<{ internalId: string; success: boolean; paymentId?: string; error?: string }> = [];

    for (const payout of payouts) {
      const res = await fetch(`${API_BASE}/programs/${PROGRAM_ID}/payouts`, {
        method: "POST",
        headers: { "Authorization": `Bearer ${tokenData.token}`, "Content-Type": "application/json" },
        body: JSON.stringify({
          client_reference_id: payout.internalId,
          payee_id: payout.payeeId,
          amount: payout.amount,
          currency: payout.currency,
          description: payout.description,
        }),
      });

      const data = await res.json();
      if (res.ok && data.payment_id) {
        await supabase.from("payouts").update({ payoneer_payment_id: data.payment_id, status: "sent" }).eq("id", payout.internalId);
        results.push({ internalId: payout.internalId, success: true, paymentId: data.payment_id });
      } else {
        await supabase.from("payouts").update({ status: "failed", error_message: data.message ?? "Unknown error" }).eq("id", payout.internalId);
        results.push({ internalId: payout.internalId, success: false, error: data.message });
      }
    }

    return new Response(JSON.stringify({ results }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
  } catch (e) {
    return new Response(JSON.stringify({ error: (e as Error).message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
  }
});
```

**Expected result:** Payout Edge Function processes payouts and updates Supabase with Payoneer payment IDs. Dashboard shows real-time payout status. Test payouts appear in the Payoneer sandbox portal.

## Best practices

- Store PAYONEER_CLIENT_ID and PAYONEER_CLIENT_SECRET in Cloud Secrets and never expose them in frontend code — all API calls must go through Edge Functions
- Cache OAuth2 access tokens in Supabase to minimize API calls to the token endpoint — generate a new token only when the cached one is within 5 minutes of expiry
- Always save Payoneer payment IDs to your Supabase payouts table immediately after a successful payout API call — you need these IDs for status checks, dispute resolution, and reconciliation
- Implement idempotency by checking if a payout with the same client_reference_id already exists in Payoneer before creating a new one — this prevents duplicate payouts if your Edge Function is called twice
- Build payee status checks into your payout workflow — only attempt payouts to ACTIVE payees, and surface a clear 'Complete Payoneer setup' prompt to PENDING payees in their dashboard
- Run payouts in small batches (20-50 per batch) with individual error handling rather than sending hundreds in a single function call — this prevents timeouts and ensures partial success in case of individual failures
- Monitor your Payoneer company account balance before scheduled payout runs and send automated alerts when the balance drops below your typical weekly payout volume

## Use cases

### Marketplace seller payouts for completed orders

A freelance services marketplace pays sellers weekly for completed and delivered orders. When an order is marked complete, the Edge Function records the payout amount owed. On the weekly payout schedule, a batch payout Edge Function sends all pending payments to registered Payoneer payees in their local currencies. Sellers in India, the Philippines, and Eastern Europe receive money in their local currency via local bank transfer.

Prompt example:

```
Build a weekly marketplace payout system. Create a Supabase table to track pending payouts with sellerId, amount, currency, and orderId. Create an Edge Function that runs on a weekly schedule (or triggered manually from admin dashboard) to fetch all pending payouts from Supabase, group them by Payoneer payee ID, and call the Payoneer Payouts API to send each payment. On success, mark the payout as sent with the Payoneer payment ID. On failure, mark as failed with the error reason. Show a payout history page for sellers in their dashboard.
```

### Affiliate commission payments

An affiliate marketing platform tracks referral commissions and pays affiliates monthly. Affiliates register their Payoneer account via an onboarding link generated by the Edge Function. At month end, the Edge Function calculates each affiliate's earned commissions from the Supabase database and initiates payouts to their registered Payoneer accounts. Commission payments above the minimum threshold ($20) are sent automatically.

Prompt example:

```
Create an affiliate payout system. Add a 'Connect Payoneer' button to the affiliate dashboard. When clicked, call an Edge Function that generates a Payoneer payee registration link for the affiliate (using the Create Payee API with the affiliate's email and name). Store the payeeId returned by Payoneer in the affiliate's Supabase profile. Create a monthly payout Edge Function that queries affiliates with unpaid commissions over $20, initiates Payoneer payouts to each, and records the payment IDs in a commissions_paid table.
```

### Creator economy tip and revenue sharing

A creator platform distributes ad revenue and tip income to creators globally. Each creator connects their Payoneer account during onboarding. The platform holds accumulated earnings in the company Payoneer account and sends payouts on a rolling 30-day basis. Creators in regions without robust Stripe support (parts of Southeast Asia, Africa, Middle East) receive reliable payouts through Payoneer's local bank transfer network.

Prompt example:

```
Build creator revenue distribution using Payoneer. During creator signup, generate a Payoneer payee registration URL via an Edge Function and embed it as a mandatory onboarding step. Once connected, save their Payoneer payeeId to Supabase. Create an Edge Function that runs monthly to fetch all creators with balance over $10 (from a creator_earnings table), initiate Payoneer payouts for each, and log the payout in a creator_payouts table with amount, payoneer_payment_id, and status. Update creator balance to zero after confirmed payouts.
```

## Troubleshooting

### OAuth2 token request returns 401 or 'invalid_client' error

Cause: The PAYONEER_CLIENT_ID and PAYONEER_CLIENT_SECRET combination is wrong, the application credentials haven't been activated yet, or you're using sandbox credentials against the production endpoint.

Solution: Verify your credentials in the Payoneer Developer Portal. Confirm PAYONEER_ENVIRONMENT in Cloud Secrets matches the credentials (sandbox vs production). Sandbox credentials only work with sandbox.api.payoneer.com. If credentials are newly created, they may need a few minutes to activate in Payoneer's system after creation.

### Payee registration returns 400 or 'payee already exists'

Cause: The payee_id (your internal user ID used as Payoneer payee reference) already exists in your Payoneer program, or the email is already registered to another payee in the program.

Solution: Check if the user already has a payoneer_payee_id in your Supabase users table before calling the registration endpoint. If they do, use GET /payees/{payee_id} to retrieve their current registration link instead of creating a new one. Payoneer allows regenerating registration links for existing payees that haven't completed onboarding.

```
// Check for existing payee before creating:
const { data: user } = await supabase.from('users').select('payoneer_payee_id').eq('id', userId).single();
if (user?.payoneer_payee_id) {
  // Fetch existing registration link instead of creating new payee
  const existing = await fetch(`${API_BASE}/programs/${PROGRAM_ID}/payees/${user.payoneer_payee_id}`, ...);
}
```

### Payout API returns 'payee not active' or 'payee status pending'

Cause: The payee was registered (payee record created) but has not yet completed their Payoneer account registration via the registration link. Payouts can only be sent to ACTIVE payees who have completed KYC.

Solution: Check the payee's status via GET /programs/{programId}/payees/{payeeId}. Status 'PENDING' means they haven't clicked the registration link. Status 'ACTIVE' means they're ready for payouts. Send a reminder email with the registration link stored in your Supabase users table. Registration links expire after 30 days — generate a new one if expired.

### Payout processing fails with 'insufficient funds' error

Cause: Your Payoneer business account balance is insufficient to cover the payout amount. Payoneer payouts debit from your company's Payoneer balance, which must be funded before sending payouts.

Solution: In the Payoneer Business Portal, check your account balance. Fund your Payoneer account via bank transfer or wire transfer before running payouts. Implement a balance check in your payout Edge Function by calling the Payoneer Balances API before initiating payouts to avoid failed batches mid-run.

## Frequently asked questions

### How long does it take for Payoneer payouts to reach recipients?

Payoneer payout timing depends on the recipient's payout method. Payoneer-to-Payoneer transfers are typically instant. Payoneer to local bank account takes 2-5 business days depending on the country and bank. Payoneer to prepaid card is typically same-day. Processing times are outside your control once the payout is initiated via API — you can only check status. Build user expectations around the slowest option (5 business days) to avoid support issues.

### Does Payoneer require KYC verification for payees?

Yes — Payoneer requires all payees to complete identity verification (KYC) as part of their account registration. This is handled within Payoneer's registration flow that the payee completes after clicking the registration link you generate. Until KYC is complete, the payee status remains PENDING and payouts will be blocked. KYC requirements vary by country but typically require government-issued ID.

### What countries does Payoneer support for payouts?

Payoneer supports payouts to recipients in 190+ countries. However, local bank transfer availability varies by country — some countries support direct bank deposits while others require recipients to keep funds in their Payoneer balance or withdraw via other methods. Check the Payoneer website for country-specific transfer options. Payoneer's coverage in Southeast Asia, South Asia, Latin America, and Eastern Europe is particularly strong relative to Stripe.

### Can I send payouts in multiple currencies?

Yes — Payoneer supports payouts in 70+ currencies. Specify the currency code in the payout API request. Payoneer handles the currency conversion from your account's balance currency. Note that currency conversion involves Payoneer's exchange rate, which includes a spread above the mid-market rate. For large payout programs, negotiate currency terms with your Payoneer account manager.

### How do I handle failed payouts and notify recipients?

Monitor Payoneer webhook events for payout status changes. Failed payouts return a status of 'FAILED' with an error code in the webhook payload. Store the failure reason in your Supabase payouts table and send the recipient an email explaining the issue (account details incorrect, account suspended, etc.). Implement a retry mechanism in your dashboard that lets admins resubmit failed payouts after the root cause is resolved.

---

Source: https://www.rapidevelopers.com/lovable-integration/payoneer
© RapidDev — https://www.rapidevelopers.com/lovable-integration/payoneer
