> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endl.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Transactions

> Moving money: from a quote to a settled payout, how to fund it, the statuses it passes through, and what to do when it fails

A transaction is a quote carried out: Endl takes the money from the source you named
and pays the recipient. Every transaction starts from a quote and gets a `txn_…` id
when you submit it. **Nothing moves before submit.**

> [!NOTE]
>
> This is the conceptual guide. For parameters and schemas see
> [Create pre-transaction](https://docs.endl.io/api-reference/transactions/create-pre-transaction.md),
> [Submit transaction](https://docs.endl.io/api-reference/transactions/submit-transaction.md),
> [Get transaction](https://docs.endl.io/api-reference/transactions/get-transaction.md) and
> [List transactions](https://docs.endl.io/api-reference/transactions/list-transactions.md).

## Before you start

| You need | Why | Where |
| - | - | - |
| A customer (`cus_…`) with the right KYC | Stablecoin → stablecoin and Paywho-routed transfers need Sumsub; anything routed through Bridge needs the full Bridge check. | Onboarding |
| A recipient (`rec_…`) with `statusAccountsProvider: ACTIVE` | Only active recipients can be paid. Not needed for `TOP_UP`. | [Recipients](https://docs.endl.io/guides/recipients.md) |
| A funding source | A wallet (`wlt_…`) for stablecoin-funded transfers, or an Endl account balance for fiat. | [Wallets](https://docs.endl.io/guides/wallets.md), [Accounts](https://docs.endl.io/guides/accounts.md) |
| A fresh quote (`qut_…`) | It fixes the amounts and fees for ten minutes, and backs one transaction only. | [Quotes](https://docs.endl.io/guides/quotes.md) |
| An API key with the `transactions` permission | Every endpoint on this page checks it. | Dashboard |

## The flow

**1. Price it**

[Generate quote](https://docs.endl.io/api-reference/quotes/generate-quote.md) returns a `qut_…` id. The
ten-minute clock starts now and runs through submit.

**2. Check it**

[Create pre-transaction](https://docs.endl.io/api-reference/transactions/create-pre-transaction.md) checks
the quote, recipient, wallet and KYC together. It creates nothing and moves nothing.

**3. Attach an invoice (only if required)**

[Upload invoice](https://docs.endl.io/api-reference/transactions/upload-invoice.md) returns `file_…` ids.

**4. Submit**

[Submit transaction](https://docs.endl.io/api-reference/transactions/submit-transaction.md) creates the
`txn_…` and moves the money. This is the only call that does.

**5. Track it**

Webhooks tell you something changed;
[Get transaction](https://docs.endl.io/api-reference/transactions/get-transaction.md) tells you what.

## Step by step

The example sends 100 USDC from a customer's wallet on BaseSepolia to a VND bank
account in Vietnam. Every call carries `Api-Key` and `Api-Version: 2026-09.1`.

### Check it with a pre-transaction

```bash theme={null}
curl -X POST https://api-sandbox.endl.io/api/v0/transactions/cus_3HfG8kLp2QwE7rTy9UiO/pre-txn \
  -H "Api-Key: $ENDL_API_KEY" -H "Api-Version: 2026-09.1" -H "Content-Type: application/json" \
  -d '{
    "quoteId": "qut_6lGsPUP860cEUxAqIOse",
    "recipientId": "rec_A9s8D7f6G5h4J3k2L1z0",
    "walletId": "wlt_51DpwsftZjTUZ7rK3xAT",
    "transferPurposeCode": "P0102"
  }'
```

```json theme={null}
{
  "status": "VALIDATED",
  "quoteId": "qut_6lGsPUP860cEUxAqIOse",
  "expiresOn": "2026-10-23T10:20:31.512Z",
  "source": { "amount": 101.50, "currency": "USDC", "chain": "BaseSepolia" },
  "destination": { "amount": 2500000.00, "currency": "VND" },
  "fees": {
    "total": 1.50,
    "currency": "USDC",
    "breakdown": [
      { "feeType": "SERVICE_FEE", "feeCategoryTypeEnum": "SERVICE_FEE", "feeName": "Service fee", "feeAmount": 1.50, "feeAmountActual": 1.50, "feeCurrency": "USDC", "feePercent": null, "tax": false }
    ]
  },
  "settlement": "SERVER_SIDE"
}
```

Show `source.amount` and `destination.amount` to the user here for a final
confirmation. Pre-transaction is safe to repeat for the same quote.

### Submit

```bash theme={null}
curl -X POST https://api-sandbox.endl.io/api/v0/transactions/cus_3HfG8kLp2QwE7rTy9UiO/submit \
  -H "Api-Key: $ENDL_API_KEY" -H "Api-Version: 2026-09.1" -H "Content-Type: application/json" \
  -d '{
    "quoteId": "qut_6lGsPUP860cEUxAqIOse",
    "recipientId": "rec_A9s8D7f6G5h4J3k2L1z0",
    "walletId": "wlt_51DpwsftZjTUZ7rK3xAT",
    "endlTransactionMode": "STABLE_COIN_TO_FIAT",
    "nameOrAlias": "Invoice 1042 payout",
    "fundsSource": "BUSINESS_REVENUE",
    "transferPurpose": "Payment for services",
    "transferPurposeCode": "P0102",
    "invoiceFileIdList": ["file_Q8w7E6r5T4y3U2i1O0pA"]
  }'
```

Submit answers `200` with the transaction object. Store `txnId`. For a
wallet-funded transfer Endl sends from the wallet during this call, so the first
status you see is often already `IN_PROGRESS`:

```json theme={null}
{
  "txnId": "txn_9ZkQ2mLr7TbV4xWc1NpA",
  "quoteId": "qut_6lGsPUP860cEUxAqIOse",
  "recipientId": "rec_A9s8D7f6G5h4J3k2L1z0",
  "sourceAmount": "101.50",
  "sourceCurrency": "USDC",
  "destinationAmount": "2500000.00",
  "destinationCurrency": "VND",
  "endlTransactionMode": "STABLE_COIN_TO_FIAT",
  "status": "IN_PROGRESS",
  "subStatus": "PROCESSING_PAYOUT",
  "createdOn": "2026-10-23T10:16:01.907Z"
}
```

Trimmed: the full object also carries fees, `recipientData` and `depositData`. See
[Get transaction](https://docs.endl.io/api-reference/transactions/get-transaction.md).

### Picking `endlTransactionMode`

| Mode | Money comes from | Pays out to | Needs |
| - | - | - | - |
| `STABLE_COIN_TO_FIAT` | Customer wallet | A bank recipient | `walletId`, `recipientId` |
| `STABLE_COIN_TO_STABLE_COIN` | Customer wallet | A crypto recipient | `walletId`, `recipientId` |
| `TOP_UP` | Customer wallet | The customer's own Endl account | `walletId`, `accountId`; no `recipientId` |
| `FIAT_TO_FIAT` | Endl account balance | A bank recipient | `recipientId` |
| `FIAT_TO_STABLE_COIN` | Fiat deposit | A crypto recipient | `recipientId` |

The mode must match the quote: a `USDC → VND` quote is `STABLE_COIN_TO_FIAT`.

## Funding

You never send funds to a deposit address on this surface. You name the source, and
Endl moves the money server-side when you submit. That is what
`settlement: "SERVER_SIDE"` on the pre-transaction means.

| Funding | When | What you send | What Endl does at submit |
| - | - | - | - |
| **From a wallet** | The quote's `depositRail` is `CRYPTO` | `walletId` on pre-transaction **and** submit | Debits the amount and fees from that wallet, then pays out |
| **From an Endl account** | Fiat source, e.g. `depositRail: ENDL_ACCOUNT` | Nothing extra | Debits the account balance, then pays out |

> [!WARNING]
>
> **Send `walletId`, never `fromAddress`.** The wallet must be one of this customer's
> active wallets; Endl reads its address itself. A missing `walletId` on a
> wallet-funded transfer is `400 ERRTXN_1002`; a wallet that is not the customer's,
> or not active, is `400 ERRTXN_1003`. Submit checks this before anything is written.

> [!NOTE]
>
> **The wallet must hold enough.** The debit is `source.amount` from the quote, fees
> included. Check it with [Get wallet balance](https://docs.endl.io/api-reference/wallets/get-wallet-balance.md)
> first. On sandbox, new EVM wallets arrive with 100 test USDC and 100 test USDT on
> their network.

## Statuses

A transaction carries `status` for where it is and `subStatus` for the step inside
it. Decide on `status`, but always read `subStatus` when the status is `IN_REVIEW`:
a failure or refund can show up there first.

| `status` | Means | Final? |
| - | - | - |
| `INITIATED` | Created at submit, waiting for funds to be confirmed. | No |
| `IN_PROGRESS` | Funds confirmed; payout under way. | No |
| `IN_REVIEW` | Held for review. Check `subStatus`. | No |
| `PENDING_TRAVEL_RULE` | Waiting for Travel Rule approval. | No |
| `COMPLETE` | Paid out. | Yes |
| `REJECTED` | Failed or refused. | Yes |
| `REFUNDED` | Funds returned to the source. | Yes |

| `subStatus` | Step |
| - | - |
| `PENDING_DEPOSIT_CONFIRMATION` | Waiting for the funds. First value at submit. |
| `DEPOSIT_CONFIRMED` | Funds received. |
| `DEPOSIT_REJECTED` | Funds refused. |
| `REVIEW_IN_PROGRESS` | Under review. |
| `WITHDRAWAL_INITIATED`, `WITHDRAWAL_IN_PROGRESS`, `WITHDRAWAL_COMPLETED` | The on-chain leg is being sent. |
| `PROCESSING_PAYOUT` | Payout sent to the recipient's rail. |
| `TRANSACTION_PROCESSED_SUCCESSFULLY` | Done. |
| `TRANSACTION_REJECTED` | Failed. Treat as failed even if `status` is still `IN_REVIEW`. |
| `TRANSACTION_REFUNDED` | Refunded. Treat as refunded even if `status` is still `IN_REVIEW`. |

> [!WARNING]
>
> There is no `PENDING`, `PROCESSING`, `COMPLETED`, `FAILED` or `CANCELLED` status.
> Match the values above exactly, and treat an unknown value as "not final yet".

### Webhooks

| Event | Sent when |
| - | - |
| `transaction.initiated` | Once, right after a successful submit. |
| `transaction.updated` | The status becomes `IN_PROGRESS`. |
| `transaction.completed` | The status becomes `COMPLETE`. |
| `transaction.failed` | The status becomes `REJECTED`, or `subStatus` becomes `TRANSACTION_REJECTED`. |
| `transaction.refunded` | The status becomes `REFUNDED`, or `subStatus` becomes `TRANSACTION_REFUNDED`. |

The event's `data.id` is the `txn_…` id and its payload carries `status`,
`sourceCurrency` and `destinationCurrency`. Nothing is sent for `IN_REVIEW` or
`PENDING_TRAVEL_RULE`, and a fast transfer can finish during submit without a
separate `updated` event. **On every event, call Get transaction for the full state**,
and poll it for transfers that go quiet. See [Webhooks](https://docs.endl.io/guides/webhooks.md) for signing
and delivery.

## Purpose codes, invoices and listing

**Purpose codes.** Every transfer except `TOP_UP` needs `transferPurpose` and
`transferPurposeCode`. Take the code from
[List purpose codes](https://docs.endl.io/api-reference/transactions/list-purpose-codes.md); pass
`relation=SELF` when the customer pays themselves.

**Invoices.** When a purpose code or recipient has `invoiceMandatory: true`, upload the
invoice before submit and pass the ids in `invoiceFileIdList`. Upload takes
`multipart/form-data` with part name `file`, 1–5 files, 2 MB each.

**Listing.** [List transactions](https://docs.endl.io/api-reference/transactions/list-transactions.md) is a
`POST` because the filters travel in the body.

| Field | Use |
| - | - |
| `page`, `size` | Zero-based page and page size. `size: 0` returns everything. |
| `sortBy` | `field,direction`. Send `createdOn,desc` for newest first; the default order is not meaningful. |
| `status` | One status, e.g. `COMPLETE`. |
| `endlTransactionMode[]`, `depositType[]`, `recipientType[]`, `sourceCurrency[]` | Match any of the values. |
| `dateRange` | `ALL_TIME` (default), `YESTERDAY`, `LAST_7_DAYS`, `SINGLE` (with `startDate`), `CUSTOM` (with `startDate` and `endDate`, `DD-MM-YYYY`, UTC). |
| `search` | Recipient first or last name. Not the `txn_` id: use Get transaction for that. |

The response is `{ "txns": [ … ], "totalCount": 42 }`; `totalCount` counts every
match, not just this page.

## Errors and retries

Errors use the envelope described in [Errors](https://docs.endl.io/api-reference/errors.md): branch on
`errors[0].code`, not the top-level `message`.

| Code | HTTP | Means | Do this |
| - | - | - | - |
| `ERRREF_1000` | 400 | An id is malformed or the wrong type, or `quoteId` is missing. `field` names it. | Fix the id. |
| `ERRREF_1001` | 404 | The customer, quote, recipient or transaction does not exist under this partner. | Check the customer. |
| `ERRTXN_1002` | 400 | `walletId` missing on a wallet-funded transfer. | Send `walletId`. |
| `ERRTXN_1003` | 400 | `walletId` is not an active wallet of this customer. | Use one from [List wallets](https://docs.endl.io/api-reference/wallets/list-wallets.md). |
| `ERRCORE_1005` | 403 | The customer lacks the KYC this route needs, or the key lacks `transactions`. | Complete KYC, or fix the key. |
| `ERRCORE_1001` | 400 | Any other refusal: expired quote, quote already used, recipient not payable, missing invoice. | Re-check the inputs; usually get a fresh quote. |
| `ERRCORE_1006` | 429 | Rate limit. | Back off and retry. |

> [!WARNING]
>
> **A quote backs one transaction, ever** — including one that failed. Resubmitting
> the same `quoteId` is refused, so a retry after a failed submit starts with a new
> quote. There is no `Idempotency-Key` header: the single-use quote is what stops a
> double payment.

**If submit times out**, do not resubmit straight away. List transactions with
`sortBy: "createdOn,desc"` and look for your `quoteId`. If it is there, the submit
went through; track it. If not, get a fresh quote and start again.

## Common pitfalls

> **400 ERRTXN\_1002 on a stablecoin payout**
>
> Wallet-funded modes need `walletId` on both pre-transaction and submit. Do not send
> `fromAddress`; it is not a field on this surface.

> **My retry failed even though the first submit failed**
>
> Quotes are single-use, and the first attempt consumed this one. Generate a fresh
> quote for every attempt.

> **The transaction sits in IN\_REVIEW and no webhook arrived**
>
> No event is sent for `IN_REVIEW`. Read `subStatus`: `TRANSACTION_REJECTED` or
> `TRANSACTION_REFUNDED` there is the outcome.

> **Amounts on the transaction are strings**
>
> `sourceAmount`, `destinationAmount`, `fxRate` and `chargesTotal` are decimal
> strings on the transaction object. Parse them as decimals, not floats.

> **403 on submit for a customer who can quote**
>
> KYC depth follows the route. A Bridge-routed transfer needs the full Bridge check
> even when a stablecoin → stablecoin transfer works with Sumsub alone.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.