One2Pays

Create Withdraw

Creates a new withdraw/transfer to a customer bank account. The withdraw will be processed asynchronously.

Endpoint

POST /api/v1/withdraws

Authentication

This endpoint requires HMAC Integration Authentication.

Request Parameters

ParameterTypeRequiredDescription
amountstringYesAmount as decimal string (e.g., "1000.00" = 10.00 THB)
currencystringNoCurrency code. Defaults to "THB"
referenceIdstringYesYour unique reference ID (alphanumeric, underscores, hyphens, dots, 1-255 chars)
destinationTypestringYesDestination type: "bank_account" or "wallet"
customerBankAccountNamestringYesCustomer's bank account name
customerBankAccountNumberstringYesCustomer's bank account number
customerBankCodestringYes3-digit Thai bank code (e.g., "004" for KBANK)
descriptionstringNoWithdraw description (max 255 characters)
metadataobjectNoAdditional metadata (key-value pairs)
idempotencyKeystringNoUnique key to prevent duplicate withdraws (recommended)

Integration Context

Your merchantId and integrationId are automatically provided from your integration credentials. You don't need to include them in the request.

Example Request

import crypto from 'crypto';

const API_KEY = process.env.PAYMENT_API_KEY!;
const API_SECRET = process.env.PAYMENT_API_SECRET!;

async function createWithdraw() {
  const method = 'POST';
  const path = '/api/v1/withdraws';
  const body = JSON.stringify({
    amount: '1000.00',
    currency: 'THB',
    referenceId: `payout-${Date.now()}`,
    destinationType: 'bank_account',
    customerBankAccountName: 'John Doe',
    customerBankAccountNumber: '1234567890',
    customerBankCode: '004',
    description: 'Payout for order #12345',
    metadata: {
      order_id: '12345',
    },
    idempotencyKey: `payout-12345-${Date.now()}`,
  });

  const timestamp = Date.now().toString();
  const message = `${timestamp}.${body}`;
  const signature = crypto
    .createHmac('sha256', API_SECRET)
    .update(message)
    .digest('hex');

  const response = await fetch(`https://api.example.com${path}`, {
    method,
    headers: {
      'X-API-Key': API_KEY,
      'X-Timestamp': timestamp,
      'X-Signature': `sha256=${signature}`,
      'Content-Type': 'application/json',
    },
    body,
  });

  const result = await response.json();

  if (result.success) {
    console.log('Withdraw created:', result.data.id);
    console.log('Status:', result.data.status);
    return result.data;
  } else {
    console.error('Error:', result.error);
    throw new Error(result.error.message);
  }
}

createWithdraw();

Response

Success Response (201 Created)

{
  "success": true,
  "data": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "amount": "1000.00",
    "currency": "THB",
    "status": "pending",
    "referenceId": "payout-12345",
    "destinationType": "bank_account",
    "bankAccountNumber": "1234****7890",
    "bankAccountName": "John Doe",
    "bankCode": "004",
    "destinationId": null,
    "description": "Payout for order #12345",
    "metadata": {
      "order_id": "12345"
    },
    "paidAt": null,
    "canceledAt": null,
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-01-01T00:00:00.000Z"
  }
}

Error Response (400 Bad Request)

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "amount must be a valid decimal number",
    "metadata": {
      "field": "amount"
    }
  }
}

Matching Payout Conflict (409 Conflict)

Within one merchant account, a payout is considered a match when it has the same decimal amount and the payout account holder names are more than 90% similar after removing only formatting such as case, whitespace, and punctuation. Titles and other name content are not removed. A matching payout blocks creation when either condition is true:

  • its status is awaiting_verification, pending, or processing, regardless of age; or
  • it was created less than 15 minutes ago, including a recent paid, failed, or canceled payout.

A terminal match stops blocking after 15 minutes. Different amounts and different merchants are independent. A different bank account number does not bypass the guard when the merchant, amount, and payout name still match.

{
  "success": false,
  "error": {
    "code": "STATE_CONFLICT",
    "message": "A matching payout is still active or was created within the last 15 minutes",
    "metadata": {
      "existingWithdrawId": "8f00bae0-d586-49d2-8355-d03d0351fce8",
      "existingWithdrawStatus": "processing",
      "reason": "active_payout"
    }
  }
}

For a recent terminal match, reason is recent_payout and metadata also includes retryAfter, an ISO timestamp for when the 15-minute window ends.

Withdraw Status

After creation, withdraws typically start in pending status and progress to processing as they're handled by the payment provider. awaiting_verification, pending, and processing are active statuses and keep blocking a matching payout regardless of age. An indeterminate provider result remains processing; do not retry it as a new withdrawal until BroPay has reconciled the provider outcome.

Idempotency

Use Idempotency Keys

Always include an idempotencyKey in your requests to prevent duplicate withdraws if you need to retry a request.

If you make the same request twice with the same caller-provided idempotencyKey, you'll receive the same withdraw object with HTTP 200. This is useful for handling network errors and retries.

If you don't provide an idempotencyKey, the system will auto-generate one, but you won't be able to replay the original request safely. A new or omitted key does not bypass the matching-payout guard and returns HTTP 409 when that guard matches.

Error Codes

CodeDescription
VALIDATION_ERRORInvalid request parameters
INVALID_AMOUNTAmount must be a valid decimal number
INVALID_BANK_CODEBank code must be a valid 3-digit Thai bank code
INSUFFICIENT_BALANCEMerchant account has insufficient balance
STATE_CONFLICTA matching payout is active or less than 15 minutes old
DUPLICATE_REFERENCE_IDReference ID already exists for this merchant
INVALID_CREDENTIALSAuthentication failed

See Also

On this page