# Send payouts with the API

This guide takes a payout from your Glomo balance to a beneficiary's bank account over the API. You create the beneficiary, price the payout if you need a locked rate, create the payout, and follow it to a final status. To do the same job in the dashboard, see [Create a Payout](/payout/create).

All request and response bodies on this page are examples. IDs, amounts and rates are illustrative, and the response bodies are trimmed to the fields this guide uses. The [API reference](/api-reference/openapi) has the full schemas.

The v1 endpoints are under `https://api.glomopay.com/api/v1` and the v2 beneficiary endpoints are under `https://api.glomopay.com/api/v2`.

## Before you start

- **Secret key.** Every call is made from your server, with your secret key as the bearer token. See [Authentication](/platform/authentication). Use your test key while you build against sandbox.
- **Balance.** A payout is paid from your balance in its `source_currency`. Check it with [Retrieve balances](/api-reference/openapi/balance/getbalance) (`GET /v1/balances`). [Balances and holds](#balances-and-holds) explains when funds are held.
- **Webhooks.** Register a webhook URL so that you receive `payout` events. See [Webhooks](/platform/webhooks).
- **Account settings.** Two per-account settings change how this flow behaves. Glomo switches them on for each account, so ask Glomo which ones are on for your account, and ask Glomo to enable any that you need.


| Setting | What it changes when it is on |
|  --- | --- |
| Maker-checker approval | A payout created over the API is returned as `pending_approval`. It waits there until a user on your account approves it in the dashboard, and there is no API call to approve it. Nothing is held from your balance before approval. Glomo's approval emails and dashboard notifications go out only for payouts that a dashboard user created, so arrange approval of API payouts yourself. `GET /v1/payouts?status=pending_approval` lists the payouts that are waiting. Approve within 30 minutes of creating the payout, or by the quote's `expires_at` if you sent a `quote_id`. A later approval cancels the payout with `error_code` set to `QUOTE_EXPIRED`. |
| Payout queueing | Every payout is created `queued`, including one that your balance already covers, and a payout that your balance can't cover is accepted instead of refused. Glomo releases queued payouts once your balance covers them, checking every 15 minutes, so a payout can wait up to 15 minutes before processing starts. When the setting is off, a payout that your balance cannot cover is refused with `400`. See [Queued Payouts](/payout/queued-payouts). |


## Step 1: Create the beneficiary

**`createBeneficiaryV2`** · `POST /v2/beneficiaries` · [API reference](/api-reference/openapi/beneficiary_v2/createbeneficiaryv2)

The request needs these fields:

- `category`: set it to `payout`.
- `beneficiary.name`, `beneficiary.type` (`individual` or `business`) and `beneficiary.address`. The address needs `address_line1`, `city`, `state` and `country_code`. `address_line2` and `postal_code` are optional.
- Either `bank_account` or `upi_account`. A `bank_account` needs `account_number` and `currency`, plus exactly one routing identifier: either `local_routing_number` (an IFSC for INR, a sort code for GBP, a BSB for AUD) or `swift_code` for SWIFT. Sending both returns `400`. For an AED or EUR IBAN, put the IBAN in `account_number` and send neither. A `upi_account` needs `upi_id`.


A beneficiary can be paid on a rail only if it holds that rail's identifier. [Rails](/payout/rails#beneficiary-requirements) lists what each corridor needs. Because a beneficiary carries one routing identifier, create it with the identifier for the rail you intend to pay on. For a `payout` beneficiary, Glomo fills in the bank name and address from the routing identifier. If they can't be resolved, the request returns `400`, and you then need to send `bank_account.name` and `bank_account.address` yourself.

Example request:

```json
{
  "category": "payout",
  "beneficiary": {
    "name": "Rahul Sharma",
    "type": "individual",
    "address": {
      "address_line1": "12 MG Road",
      "city": "Bengaluru",
      "state": "Karnataka",
      "country_code": "IND",
      "postal_code": "560001"
    }
  },
  "bank_account": {
    "account_number": "9876543210",
    "local_routing_number": "KKBK0000261",
    "currency": "INR"
  }
}
```

Example response, `201 Created`, trimmed:

```json
{
  "id": "bene_a1b2c3d4e5",
  "name": "Rahul Sharma",
  "type": "individual",
  "category": "payout",
  "status": "active",
  "bank_account": {
    "account_number": "9876543210",
    "local_routing_number": "KKBK0000261",
    "currency": "INR"
  },
  "upi_account": null
}
```

Store the `id`. If the same bank account already exists on your account, the request returns `409 Conflict` with a message saying that a beneficiary with this account number already exists. Reuse the existing beneficiary instead, which you can find with [List beneficiaries](/api-reference/openapi/beneficiary_v2/listbeneficiariesv2) (`GET /v2/beneficiaries`).

### Check that the beneficiary is `active`

A new beneficiary is created `active`, so you can continue to the next step, unless Glomo has enabled beneficiary review on your account. With review enabled, a new beneficiary starts `pending` and moves to `active` or `rejected` once reviewed; the `beneficiary` webhook reports the change. A payout to a `pending` or `rejected` beneficiary is refused with `400`. In sandbox, approve a `pending` beneficiary with `PATCH /v2/beneficiaries/{id}/mock-review` and the body `{"review_action": "approve"}`.

## Step 2 (optional): Get a quote

**`createQuote`** · `POST /v1/quotes` · [API reference](/api-reference/openapi/quote/createquote)

A quote locks the exchange rate and fees before you commit to the payout. Use one when someone has to approve the exact amount first. Without a quote, Glomo prices the payout when you create it, and the payout's `quote_id` then refers to that price.

The request needs these fields:

- `resource`: set it to `payout`.
- `source_currency` and `destination_currency`.
- Exactly one of `source_amount` or `destination_amount`, in the smallest unit of its currency.
- `payment_method`: `type` set to `bank_transfer`, and `subtype` set to `local_transfer` or `swift_transfer`.


The response holds a `quotes` array. A `swift_transfer` quote returns a single `swift` option. A `local_transfer` quote returns one option for each local rail of the destination currency whose limits admit the amount. Each option names its rail in `payment_method.rail`. A quote is valid for 30 minutes (see `expires_at`) and can be used for one payout.

**The quote doesn't look at the beneficiary.** The request has no beneficiary field, so it prices only the corridor and the amount. A quoted rail can therefore be one that your beneficiary can't receive. Choose the option whose rail matches the identifiers the beneficiary holds. If you create a payout with a quote whose rail the beneficiary doesn't support, the request returns `400` with a message that the quote is not compatible with the selected beneficiary.

Example request:

```json
{
  "resource": "payout",
  "source_currency": "USD",
  "destination_currency": "INR",
  "destination_amount": 882000,
  "payment_method": {
    "type": "bank_transfer",
    "subtype": "local_transfer"
  }
}
```

Example response, `201 Created`, trimmed to one option:

```json
{
  "quotes": [
    {
      "id": "quote_3m4n5o6p7q",
      "resource": "payout",
      "expires_at": "2026-06-12T08:16:50Z",
      "payment_method": {
        "type": "bank_transfer",
        "subtype": "local_transfer",
        "rail": "neft"
      },
      "source_currency": "USD",
      "destination_currency": "INR",
      "fx_rate": 90.0,
      "source_amount": 10000,
      "destination_amount": 882000,
      "fees": {
        "txn_fee": { "currency": "USD", "amount": 100 },
        "fx_fee": { "currency": "USD", "amount": 100 }
      }
    }
  ]
}
```

Here $100.00 (USD) pays ₹8,820.00 (INR) after $2.00 in fees.

An LRS repatriation payout (USD to INR, back to an investor) can't use a quote. Follow [Withdraw an LRS remittance back to the investor](/payin/resident-india-remittance-under-lrs/set-up/withdrawal-flow-guide) instead.

## Step 3: Create the payout

**`createPayout`** · `POST /v1/payouts` · [API reference](/api-reference/openapi/payout/createpayout)

The required fields depend on whether you use a quote:

|  | Without a quote | With a quote |
|  --- | --- | --- |
| Required | `beneficiary_id`, `purpose_code`, `source_currency`, `destination_currency`, and exactly one of `source_amount` or `destination_amount` | `quote_id`, `beneficiary_id`, `purpose_code` |
| `payment_rail` | Set it (see below) | Don't send it |
| Also send | `request_id`, and `documents` if your purpose code needs them | `request_id`, and `documents` if your purpose code needs them |


With a quote, the quote fixes the amounts, the currencies and the rail. Sending any of `source_amount`, `destination_amount`, `source_currency`, `destination_currency` or `payment_rail` with a `quote_id` returns `400`. The quote must also be a payout quote that hasn't expired and hasn't already been used.

### `request_id`: make it deterministic

Send a `request_id` of at most 64 characters with every payout, and derive it from your own record, such as your internal payout ID. A second create with the same `request_id` returns `409 Conflict` with the message `Record already exists`. No second payout is created, and the existing payout is not returned.

If the create call times out, fails on the network or returns a `5xx`, keep retrying with the **same** `request_id`, and look the payout up with `GET /v1/payouts?request_id=`. **Never switch to a new `request_id` after a timeout or network error.** The original call may still be screening the payout and holding funds, and the payout doesn't appear in the list until that call finishes, so a new `request_id` can pay twice. If a retry keeps returning `409` and the lookup never finds a matching payout, contact Glomo support with the `request_id`. The `request_id` is optional, but without one nothing stops a duplicate.

### `payment_rail`: set it without a quote, never with one

- **Omitted means SWIFT.** Without a quote, a payout with no `payment_rail` goes on `swift`. That only works for a beneficiary with SWIFT details, meaning a `swift_code`, or an AED or EUR IBAN. Any other beneficiary, such as an INR beneficiary with an IFSC or a UPI ID, gets `400` with the message `No rails available for this beneficiary`. Where SWIFT does work, you get a SWIFT payout even when a local rail was available.
- **The rail must fit the payout.** It must serve the `destination_currency`, it must be a rail whose identifier the beneficiary holds, and the amount must be within the rail's limits. [Rails](/payout/rails) lists the rails, their limits and the errors. Confirm with Glomo which corridors and rails are enabled for your account.
- **RTGS is not accepted at the moment.** A payout with `payment_rail: rtgs` returns `400` with a message that `rtgs` is temporarily unavailable for INR payouts and that you should use `neft`. INR quotes don't offer `rtgs` either.
- **With a quote,** the rail comes from the quote. Don't send `payment_rail`.


### `purpose_code`

Choose the code from [Payout purpose codes](/payout/purpose-codes) and pass it exactly as listed. Payin codes are rejected on a payout. INR payouts on a local rail (`upi`, `imps`, `neft`, `rtgs`) accept only some of the payout codes. A code outside that set returns `400` with a message saying the code is not a valid purpose code for the selected payment rail. Confirm with Glomo which code applies if you get this error. LRS payouts follow their own guide.

### `documents`

If your purpose code needs supporting documents, upload each one first with [Upload a document](/api-reference/openapi/document/createdocument) (`POST /v1/document`). The upload is sent as `multipart/form-data`, with `file` and `document_name`. Then pass the returned `doc_` IDs in `documents`. Each ID must be a document on your account, or the request returns `400`. The purpose code list names the documents to collect for each code.

Example request, without a quote:

```json
{
  "purpose_code": "S0802",
  "beneficiary_id": "bene_a1b2c3d4e5",
  "source_currency": "USD",
  "destination_currency": "INR",
  "source_amount": 100000,
  "payment_rail": "neft",
  "invoice_number": "INV-2026-001",
  "request_id": "req-7f3a9c20",
  "documents": ["doc_11aa22bb33"],
  "notes": { "internal_ref": "batch-42" }
}
```

Example request, with a quote:

```json
{
  "quote_id": "quote_3m4n5o6p7q",
  "beneficiary_id": "bene_a1b2c3d4e5",
  "purpose_code": "S0802",
  "request_id": "req-7f3a9c20",
  "documents": ["doc_11aa22bb33"]
}
```

Example response, `201 Created`, trimmed:

```json
{
  "id": "payout_9z8y7x6w5v",
  "beneficiary_id": "bene_a1b2c3d4e5",
  "status": "in_progress",
  "source_amount": 100000,
  "source_currency": "USD",
  "destination_amount": 8235000,
  "destination_currency": "INR",
  "purpose_code": "S0802",
  "payment_rail": "neft",
  "request_id": "req-7f3a9c20",
  "fees": {
    "txn_fee": { "currency": "USD", "amount": 500 },
    "fx_fee": { "currency": "USD", "amount": 250 }
  },
  "utr": null,
  "error_code": null,
  "error_description": null,
  "quote_id": "quote_8h2k4m6p9r"
}
```

A payout created without a quote still carries a `quote_id`: the price Glomo fixed when it created the payout.

**Always read `status` in the create response.** The call returns `201` even when the payout is already `pending_approval`, `queued`, `action_required` or `failed`.

Without payout queueing, a payout that your balance can't cover returns `400` with `code` set to `INSUFFICIENT_BALANCE`. The message ends with `Additional amount required: N.`, where `N` is the shortfall in the smallest unit of the source currency. A payout whose fees equal or exceed its source amount is also refused with `400`.

## Step 4: Track the payout to a final status

Glomo sends a `payout` webhook each time a payout's status changes, with `event_type` set to the new status. See [Payout webhooks](/platform/webhooks/payout). The same status can arrive more than once. For example, `in_progress` arrives again after an RFI response goes under review. So handle events idempotently, keyed on the payout `id` and `status`.

To reconcile, such as after a missed webhook, a restart or a timed-out create, read the payout back:

- [Retrieve a payout](/api-reference/openapi/payout/getpayoutbyid): `getPayoutById` · `GET /v1/payouts/{id}`
- [List all payouts](/api-reference/openapi/payout/getpayouts): `getPayouts` · `GET /v1/payouts?request_id=req-7f3a9c20`


A `request_id` filter returns at most one payout. A payout whose create call is still running doesn't appear yet, so an empty `data` array doesn't prove that nothing will pay. After a timeout, keep retrying the create with the same `request_id` as described in [`request_id`: make it deterministic](#request_id--make-it-deterministic).

Example response, `200 OK`, trimmed:

```json
{
  "data": [
    {
      "id": "payout_9z8y7x6w5v",
      "status": "success",
      "request_id": "req-7f3a9c20",
      "payment_rail": "neft",
      "error_code": null
    }
  ],
  "page_meta": {
    "current": 1,
    "previous": null,
    "next": null,
    "per_page": 20,
    "pages": 1,
    "count": 1
  }
}
```

## Status lifecycle

| Status | What it means for your integration |
|  --- | --- |
| `pending_approval` | Maker-checker is on, and the payout is waiting for approval in the dashboard. Nothing is wrong, so don't recreate it. |
| `queued` | Payout queueing is on. Every payout starts here and waits for the next release, which runs every 15 minutes and releases a payout once your balance covers it. |
| `in_progress` | The payout is being processed. This includes a payout that is back under review after you responded to an RFI. |
| `action_required` | Compliance screening didn't clear, and Glomo may raise an RFI. Answer it in the dashboard, or with [`respondRfi`](/api-reference/openapi/rfi/respondrfi) (`PATCH /v1/rfis/{id}/respond`) if you have the RFI `id`. The payout object and webhook don't include the RFI `id` today. See [Handle RFI for a Payout](/request-for-information/handle-rfi-for-a-payout) for the steps, and [Action Required State](/payout/action-required-state). |
| `success` | Final. The payout was paid, and `utr` carries the bank reference where one is available. |
| `failed` | Final. Read `error_code`. |
| `cancelled` | Final. The payout was cancelled by you, rejected by an approver in the dashboard (no `error_code`), rejected by compliance (`PAYOUT_REJECTED`), or cancelled because its quote expired (`QUOTE_EXPIRED`). |


[Payout Life Cycle](/payout/life-cycle) has the full diagram.

## Failures: retry, fix or escalate

Read `error_code` and `error_description` on a `failed` or `cancelled` payout. A payout in a final status never changes again, and the same `request_id` can't be used a second time. So a retry is always a **new** payout with a **new** `request_id`, and you create it only after you have fixed the cause.

| `error_code` | Final status | Cause | What to do |
|  --- | --- | --- | --- |
| `INSUFFICIENT_BALANCE` | `failed` | Your balance no longer covered the payout when it resumed after review. | Fund the balance, then create a new payout. |
| `QUOTE_EXPIRED` | `cancelled` | The payout's quote expired before the payout was approved and submitted. | Create a new payout, and approve it within 30 minutes of creating it, or by the quote's `expires_at` if you send a `quote_id`. If you used a quote, get a new quote first. |
| `QUEUE_EXPIRED` | `failed` | The payout stayed `queued` for 30 days. | Fund the balance, then create a new payout. |
| `INVALID_BENE_ACCOUNT_NUMBER`, `INVALID_BENE_ACCOUNT_TYPE`, `INVALID_BENE_IFSC`, `INVALID_VPA`, `INVALID_BENE_NAME`, `INVALID_BENE_ADDRESS`, `INVALID_BENE_EMAIL`, `INVALID_BENE_CONTACT` | `failed` | The bank or rail rejected the beneficiary's details. | Fix the details. Beneficiaries can't be edited over the API, so create a new beneficiary with the corrected details, then create a new payout. |
| `NAME_MISMATCH` | `failed` | The remitter and beneficiary names don't match for the selected purpose. | Check the beneficiary name and the purpose code before you create a new payout. Contact Glomo if both are correct. |
| `PAYOUT_REJECTED` | `cancelled` or `failed` | The payout was rejected by compliance after review (`cancelled`), or by the bank for a reason that Glomo doesn't map to a specific code (`failed`). | Don't retry. Escalate to Glomo support. |
| none | `failed` | Compliance screening could not complete when the payout was screened. | Retry once later with a new `request_id`. If it happens again, escalate to Glomo support. |
| none | `cancelled` | An approver rejected the payout in the dashboard, or you cancelled it. | Create a new payout with a new `request_id` only if the payment is still wanted. |


## Cancelling a payout

**`cancelPayout`** · `PATCH /v1/payouts/{id}/cancel` · [API reference](/api-reference/openapi/payout/cancelpayout)

You can cancel a payout while it is `pending_approval`, `queued` or `action_required`. You can also cancel it while it is `in_progress` under review after an RFI response. An `in_progress` payout that has been released for processing can't be cancelled: the call returns `400` with the message `Payout cannot be cancelled in its current state`. Don't build a flow that relies on cancelling a payout in flight. See [Cancel a Payout](/payout/cancel).

## Queued payouts

With payout queueing on, every payout is created `queued`. Queued payouts are released first in, first out, separately for each source currency, and Glomo checks the queue every 15 minutes, so a payout can wait up to 15 minutes even when your balance already covers it. A payout at the front of the queue that your balance can't cover holds back the payouts behind it in the same currency. A payout that stays queued for 30 days fails with `QUEUE_EXPIRED`. See [Queued Payouts](/payout/queued-payouts).

## Balances and holds

A payout is paid from your balance in its `source_currency`. The funds are held once screening clears and the payout is released for processing. On an account without maker-checker or queueing, that happens inside the create call. Nothing is held while a payout is `pending_approval`, `queued`, `action_required` or under review. A hold is released if the payout fails or is cancelled.

To pay from a different currency, convert first with [Create a balance conversion](/api-reference/openapi/balance_conversions/createbalanceconversion) (`POST /v1/balance_conversions`). See [Balances](/multi-currency-account/balances) and [Balance conversion](/multi-currency-account/balance-conversion).

## Timing

No cut-off times or SWIFT delivery times are published, so confirm processing windows with Glomo. Don't promise a delivery date from the rail alone. The [settlement holiday calendar](/settlement-holidays) lists the dates on which timelines may be affected.

## Test it in sandbox

Use your test secret key. The mock endpoint below is not available in production.

### Payouts that complete on their own

In sandbox, a payout on a local rail (`upi`, `imps` or `neft` for INR, and the local rails for AED, EUR, GBP and AUD) moves through `in_progress` to `success` by itself once it has been released for processing. This usually takes a few seconds, but don't rely on the timing. A SWIFT payout stays `in_progress` until you move it. A payout held in `pending_approval`, `queued` or `action_required` moves only when it is approved, released from the queue or reviewed, or when you move it.

### Move a payout between statuses

**`mockUpdatePayoutStatus`** · `PATCH /v1/payouts/mock` · [API reference](/api-reference/openapi/payout/mockupdatepayoutstatus)

Use this endpoint on a SWIFT payout, or on a payout held in `pending_approval`, `queued` or `action_required`. A local-rail payout that has been released completes on its own, so a mock call on it can find the payout already `success` and return `400`.

For example, to complete a SWIFT payout that is `in_progress`:

```json
{ "id": "payout_E602dMzgjpDC", "status": "success" }
```

`status` is the target of the transition, not a value that the payout's `status` field returns. The `pending` target reads back as `in_progress`. Each transition sends the same `payout` webhook as a real one would. A transition must be one the payout could make for real. Otherwise the call returns `400` with `State change not allowed`, which usually means the payout isn't in the status you assumed.

| Target | Allowed when the payout is |
|  --- | --- |
| `pending` | `pending_approval`, `queued`, `action_required`, or `in_progress` under review |
| `queued` | `pending_approval`, `action_required`, or `in_progress` under review, on an account with payout queueing. Without payout queueing the call returns `400`. |
| `success` | `in_progress`, and not under review. Move a held payout or one under review to `pending` first: a `queued` payout mocked straight to `success` returns `400` with `State change not allowed`. The payout moved to `pending` reads back as `in_progress`, so don't wait for `pending`. It doesn't complete on its own, so follow with `success`. See [Testing queued payouts in sandbox](/payout/queued-payouts#testing-queued-payouts-in-sandbox). |
| `action_required` | `pending_approval`, `queued`, or `in_progress` under review |
| `failed` | any status that isn't final |
| `cancelled` | any status that isn't final, unless the payout has already been sent to the bank |


A payout failed through this endpoint has no `error_code`, so you can't produce the codes in the failure table this way. To test the `action_required` path from creation, name the beneficiary with one of the reserved sandbox values listed on `beneficiary.name` in [Create a beneficiary](/api-reference/openapi/beneficiary_v2/createbeneficiaryv2). Those values force a sanctions-screening hit.