Payments¶
The core of the API: start a payment, list your payments, read one's status. Every field and error code is in the API reference; this page explains how to use them.
POST /transactions¶
Starts a payment. Returns 202 immediately; the payment runs to completion on
our side.
Requires the payment.initiate permission and an approved KYB status.
curl -X POST "$JET_API/transactions" \
-H "Authorization: Bearer $JET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "pay-2026-08-21-0001",
"quoteId": "q_3d6e9932-61c7-49cb-9259-9703dabf3cdc",
"corridor": { "to": "VND", "destinationCountry": "VN" },
"sendAmount": { "amount": "1000.00", "currency": "USDC" },
"recipientReference": "customer-4471",
"beneficiary": {
"name": "NGUYEN VAN A",
"account": "0123456789",
"bankName": "Vietcombank",
"bic": "BFTVVNVX",
"country": "VN"
},
"originator": {
"name": "Jan Novak",
"country": "CZ",
"dateOfBirth": "1988-04-12"
}
}'
What to know about the body:
idempotencyKeybecomes the transaction ID. Retry the same request as often as you like; there is one payment. See Idempotency.- You do not name a wallet. A payment is funded from your home wallet,
where your whole USDC balance lives (see Balance and wallets).
integratorWalletIdis optional; if you send it, it must be your home wallet, or the payment is refused with422 not_home_wallet. quoteIdexecutes at a price you already showed. Optional. Without it the payment is priced when it runs. See Quotes.beneficiaryis where the money lands. Which fields a corridor needs varies: a Vietnamese bank transfer and an Indonesian one do not want the same routing detail, and some corridors need a postal address (addressLine,city,state,postalCode) or aphone. A missing field is a400 beneficiary_incompletethat names it inmissing, before anything moves. The schema callsbeneficiaryoptional, but a payment without one has nowhere to pay. Send it.originatoris the sender, for the Travel Rule. Every field is optional: send as much of your own KYC as you hold.payoutModeis accepted for older integrations and always runs asregular. See How a payment works.
Info
There is no integratorId field. Your organization comes from the API key,
always. A client must not be able to claim to be someone else by editing a body.
Warning
originator.account and originator.idNumber are references: masked or
last-four, never a full card number or an unmasked identifier. The beneficiary
party recorded in our compliance log is derived from beneficiary with its
account masked automatically. The full account is routing detail for the
payout partner, not something the audit log keeps in the clear.
Sanctions screening¶
JET screens the Travel Rule parties and the recipient reference before any value moves. A hit refuses the payment and the payment is never started:
{
"error": "compliance_hold",
"message": "transaction blocked pending compliance review",
"transactionId": "pay-2026-08-21-0001"
}
The response deliberately does not say which list or which name matched. That is the screened party's personal data, and a probing caller must not be able to use JET as a sanctions oracle. Route these to your compliance team, not to an automatic retry.
Errors worth handling¶
| Status | error |
What to do |
|---|---|---|
400 |
beneficiary_incomplete |
Add the fields named in missing, then resend with the same key |
403 |
kyb_required |
Your organization is not verified yet. Nothing to retry |
403 |
compliance_hold |
Route to your compliance team. Do not retry |
422 |
insufficient_funds |
Your home wallet holds less than the payment. available says how much it holds |
422 |
quote_expired, quote_not_found, quote_mismatch |
Request a new quote; for quote_mismatch, send the corridor and amount you quoted |
422 |
not_home_wallet |
Omit integratorWalletId |
The full list per status is in the API reference.
GET /transactions/:id¶
Live status of one payment.
{
"transactionId": "pay-2026-08-21-0001",
"running": false,
"state": "RECONCILED",
"provider": "tazapay",
"payoutMode": "regular",
"receiveAmount": { "amount": "24512750", "currency": "VND", "scale": 0 },
"timeline": [
{ "state": "INITIATED" },
{ "state": "QUOTED", "detail": "locked q_3d6e9932…" },
{ "state": "FUNDED" },
{ "state": "ROUTING" },
{ "state": "SETTLING" },
{ "state": "PAID_OUT" },
{ "state": "RECONCILED" }
]
}
| Field | Notes |
|---|---|
running |
true while the payment is still in flight |
state |
See How a payment works |
provider |
Opaque route label, for support and reconciliation |
receiveAmount |
What the recipient received, once known |
payoutDelta |
Present only if what was paid differs from what was quoted. Absence means they agreed exactly |
timeline |
Ordered state history. Treat detail as text for people, not a field to parse |
failure |
Set when the payment ended without a result |
Warning
A 404 here means no such payment, or not yours. The two are
indistinguishable by design. See
Conventions.
Polling¶
Poll every couple of seconds while running is true. Stop when the state is
terminal (RECONCILED, REFUNDED, FAILED) or needs a person (IN_DOUBT,
NEEDS_ATTENTION).
Better: register a webhook and stop polling entirely.
GET /transactions¶
Your organization's payment history, newest first.
{
"transactions": [
{
"transactionId": "pay-2026-08-21-0001",
"corridor": { "from": "USDC", "to": "VND", "destinationCountry": "VN" },
"sendAmount": { "amount": "1000000000", "currency": "USDC", "scale": 6 },
"recipientReference": "customer-4471",
"createdAt": "2026-08-21T14:02:07.000Z"
}
],
"count": 1,
"nextCursor": "eyJ0IjoiMjAyNi0wOC0yMVQxNDowMjowNy4wMDBaIn0",
"hasMore": true
}
Page through by sending nextCursor back as ?cursor= until it comes back
null. See Pagination.
This is a record of what was initiated. Rows carry no state: call
GET /transactions/:id per row for that, or keep the state your webhooks told
you.