Skip to main content

Overview

This guide walks through the complete flow of integrating Yala payout APIs into your application, from your end user’s perspective to your backend implementation.

Integration Flow

The integration follows this pattern:
  1. User initiates payout in your app
  2. Your frontend sends request to your backend
  3. Your backend calls Yala API
  4. Yala processes and responds
  5. Your backend updates your database
  6. Your frontend displays status to user
  7. User sees updated status

Step-by-Step Flow

1. Your user selects a payout corridor

What happens:
  • User visits your app/website
  • User selects source currency (e.g., NGN) and destination currency (e.g., CNY)
  • User enters amount they want to send
Your implementation:
  • Display available currency pairs (you can cache this or call GET /pairs periodically)
  • Call GET /wallets to show the user’s balance per currency. Debit is always from the source currency wallet—when they initiate a payout in NGN, the NGN wallet is debited.
  • Validate user has sufficient balance in the selected source currency before calling initiate

2. Your backend calls Yala API to get rates and methods

What happens:
  • Your backend calls GET /methods with the selected corridor
  • Yala returns current exchange rate, available payment methods, and required beneficiary fields
Your implementation:
// Your backend: GET /methods to show rates and payment options
const url = 'https://gateway.staging.useyala.com/v1/payout-api/payouts/methods?sourceCurrency=NGN&destinationCurrency=CNY&destinationCountryCode=CHN';
const methodsResponse = await fetch(url, { 
  headers: { 
    'x-api-key': apiKey
  } 
});

const { methods, rate, corridor } = await methodsResponse.json();

// Return to your frontend:
// - rate.rate (exchange rate)
// - methods array (each with code, name, settlement, fee, requiredFields, optionalFields)

3. Your frontend displays options to user

What happens:
  • Your frontend receives rates and methods from your backend
  • User sees exchange rate and available payment methods
  • User selects a payment method (e.g., ALIPAY)
Your implementation:
  • Display exchange rate: “1 NGN = [rate.rate] CNY” (replace [rate.rate] with the actual rate value from the response)
  • Show available methods with settlement times and fees
  • Let user select preferred method
  • Show estimated destination amount: sourceAmount multiplied by the rate value

4. User fills beneficiary form

What happens:
  • Your frontend renders a dynamic form based on requiredFields from the selected method
  • User enters beneficiary details (name, Alipay ID, address, etc.)
  • User confirms payout
Your implementation:
// Get required fields for selected method
const selectedMethod = methods.find(m => m.code === 'ALIPAY');

// Render form fields dynamically
selectedMethod.requiredFields.forEach(field => {
  // Create input based on field.type (string, email, phone, number)
  // Apply validation based on field.validation (e.g., "email_or_phone")
  // Mark as required if field.required === true
});

// Also include optionalFields if you want to collect them
Form fields example:
  • accountName (string, required)
  • alipayId (string, required, validation: “email_or_phone”)
  • address (string, required)
  • city (string, required)
  • postCode (string, required)
  • email (email, optional)
  • phoneNumber (phone, optional)
What happens:
  • Before the user clicks “Confirm”, call POST /quote with the same amount, currencies, and method
  • Yala returns totalDeductible (amount debited from wallet), destinationAmount (amount beneficiary receives), and includedFees
  • Your frontend shows: “You will pay X. Recipient gets Y. Fee: Z.”
Your implementation:
  • Call POST /v1/payout-api/payouts/quote with sourceAmount, sourceCurrency, destinationCurrency, destinationCountryCode, and method
  • Display quote.totalDeductible, quote.destinationAmount, and quote.includedFees to the user
  • When the user confirms, proceed to POST /initiate (see Get Payout Quote)

5. Your backend creates payout

What happens:
  • Your backend calls POST /initiate with beneficiary details
  • Yala creates payout and returns payout details including id
  • Your backend saves payout.id to your database
Your implementation:
// Your backend: POST /initiate
const idempotencyKey = crypto.randomUUID(); // Generate unique key per payout

const payoutResponse = await fetch(
  'https://gateway.staging.useyala.com/v1/payout-api/payouts/initiate',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey, // Required!
      'x-api-key': apiKey,
    },
    body: JSON.stringify({
      sourceAmount: userEnteredAmount,
      sourceCurrency: 'NGN',
      destinationCurrency: 'CNY',
      destinationCountryCode: 'CHN',
      method: 'ALIPAY',
      beneficiary: {
        accountName: formData.accountName,
        accountCurrency: 'CNY',
        alipayId: formData.alipayId,
        address: formData.address,
        city: formData.city,
        postCode: formData.postCode,
        // Include optional fields if provided
        email: formData.email,
        phoneNumber: formData.phoneNumber,
      },
      narration: 'Payment for services',
      supportingDocument: 'https://example.com/invoice.pdf',
    }),
  }
);

const payout = await payoutResponse.json();

// IMPORTANT: Save payout.id to your database
// Link it to your order/transaction record
await saveToDatabase({
  orderId: userOrderId,
  payoutId: payout.id, // Use this to track status
  status: payout.status,
  createdAt: new Date(),
});
Response includes:
  • id - Save this! Use for tracking and webhook matching
  • status - Initial status (PENDING)
  • sourceAmount, destinationAmount - Conversion details
  • exchangeRate - Rate used for conversion
  • payoutRef - Human-readable reference

6. Track payout status

Option A: Polling (Simple but less efficient)
// Your backend: Periodically check status
const statusUrl = 'https://gateway.staging.useyala.com/v1/payout-api/payouts/' + payoutId;
const statusResponse = await fetch(statusUrl, {
  headers: { 'x-api-key': apiKey }
});

const status = await statusResponse.json();
// Update your database with status.status
// Notify your frontend/user of status change
Option B: Webhooks (Recommended for production)
  1. Activate webhooks in your Yala dashboard
  2. Configure your webhook endpoint URL
  3. Receive webhook notifications when status changes
// Your webhook endpoint (receives POST from Yala)
app.post('/webhooks/yala', async (req, res) => {
  const { eventType, deliveryId, data } = req.body;
  const { payoutId, oldStatus, newStatus, changedAt, reason } = data || {};
  
  // Find payout in your database by payoutId
  const order = await findOrderByPayoutId(payoutId);
  
  // Update status in your database (newStatus is one of PENDING, PROCESSING, SUCCESSFUL, FAILED, REJECTED)
  await updateOrderStatus(order.id, newStatus);
  
  // Notify your frontend/user (via WebSocket, push notification, etc.)
  notifyUser(order.userId, {
    payoutId,
    status: newStatus,
    reason,
  });
  
  res.status(200).send('OK');
});
Webhook payload: We POST a JSON body with eventType, deliveryId, occurredAt, and data. Status values in data.oldStatus and data.newStatus are always one of the five below:
{
  "eventType": "payout.status.changed.v1",
  "deliveryId": "550e8400-e29b-41d4-a716-446655440000",
  "occurredAt": "2024-01-20T10:05:00Z",
  "data": {
    "payoutId": "123e4567-e89b-12d3-a456-426614174000",
    "businessId": "business-id",
    "oldStatus": "PENDING",
    "newStatus": "PROCESSING",
    "changedAt": "2024-01-20T10:05:00Z",
    "changedBy": "system",
    "reason": "Payout approved and processing"
  }
}

7. User sees status updates

What happens:
  • Your backend receives webhook or polls status
  • Your frontend displays status to user
  • Status transitions: PENDING → PROCESSING → SUCCESSFUL
Your implementation:
  • Update your database when status changes (use data.newStatus from webhook payload)
  • Push status update to your frontend (WebSocket, polling, or push notification)
  • Display status in user’s dashboard/order page
Status values (same in API and webhooks):
  • PENDING - Payout created or awaiting approval; not yet sent for processing
  • PROCESSING - Payout is being processed (ops, bank, or treasury)
  • SUCCESSFUL - Completed; funds have been sent
  • FAILED - Processing failed (e.g. bank failure)
  • REJECTED - Rejected (e.g. compliance, initiation checks, or bank)

Key Integration Points

Store payout.id

Always save payout.id from the initiate response:
  • Link it to your order/transaction record
  • Use it to query status via GET /:id
  • Match it with webhook notifications (webhook payload includes payoutId)

Idempotency

Generate idempotency key per payout:
  • Use UUID or order-[orderId] format (replace [orderId] with your actual order ID)
  • Store with your order record
  • Retry with same key + same body if request fails (prevents duplicates)

Error Handling

Handle common errors:
  • INSUFFICIENT_FUNDS - User’s wallet balance too low
  • CORRIDOR_NOT_ENABLED - Currency pair not enabled in dashboard
  • MISSING_REQUIRED_FIELDS - Beneficiary form incomplete
  • IDEMPOTENCY_KEY_CONFLICT - Same key used with different request

Webhook Security

Validate webhook requests:
  • Verify source IP (Yala will provide IP ranges)
  • Verify shared secret (provided during webhook setup)
  • Implement idempotency for webhook processing (avoid duplicate processing)
See the Quickstart guide for a complete code example combining all these steps.

Next Steps