Withdrawals and off-ramps¶
Two ways to take money out of your JET balance:
| Way out | Arrives as | Start with |
|---|---|---|
| Withdrawal | USDC, in a wallet you control | GET /treasury/withdrawals/quote |
| Off-ramp | Fiat, in a bank account you registered | POST /treasury/beneficiaries |
Both are funded from your home wallet, both need the
treasury.move permission and an approved KYB status, and both are durable
operations you follow like a payment. Every field is in the
API reference.
To pay someone else in their local currency, send a payment instead.
Withdrawals¶
Sends USDC from your balance to an address you give, on the network you choose. This and USDC deposits are the only places you name a network.
An on-chain send cannot be reversed
Check the address, and check that it is on the network you name. USDC sent to the right address on the wrong network, or to a mistyped address, cannot be recovered by JET or anyone else.
GET /treasury/withdrawals/quote¶
net is what arrives. A withdrawal on your home network costs the network's gas;
one on another network also costs moving the USDC there. The networks you can
use are the ones listed by
GET /treasury/crypto-deposit.
POST /treasury/withdrawals¶
- It is re-priced when you send it, with no quote ID. The fee includes headroom for gas moving between your quote and the send, and the response carries the figure that applied.
idempotencyKeybecomes the withdrawal ID. Retrying the same request is safe. Reusing the key with a different amount, network or address is409 idempotency_key_reused; a key already used for someone else's withdrawal is409 idempotency_key_conflict.
GET /treasury/withdrawals/:withdrawalId¶
{
"withdrawalId": "wd-2026-08-21-a",
"state": "SENT",
"running": false,
"net": { "amount": "249820000", "currency": "USDC", "scale": 6 },
"txHash": "0x…",
"timeline": [
{ "state": "INITIATED" },
{ "state": "PULLING" },
{ "state": "HELD" },
{ "state": "CROSSING" },
{ "state": "SENDING" },
{ "state": "SENT" }
]
}
state |
Meaning |
|---|---|
SENT |
Confirmed on chain. txHash is the transaction. Done |
PENDING_RETRY |
The send has not been confirmed yet and is being retried |
NEEDS_ATTENTION |
Waiting for our operations team. The funds are held, not lost |
RETURNING, RETURNED |
The withdrawal was cancelled and the USDC is going, or has gone, back to your balance |
FAILED |
It stopped before any USDC moved |
IN_DOUBT |
We cannot yet tell whether the send landed. Parked for reconciliation; do not send a replacement |
A withdrawal counts as done only when the send is confirmed on chain. A send that cannot be confirmed is parked rather than repeated, because repeating a send that did land would pay twice.
GET /treasury/withdrawals lists your withdrawals, newest first, cursor-paginated
(limit up to 200). Rows carry no state.
Off-ramps¶
Converts USDC from your balance into fiat and pays it to a bank account. The account must be registered first, as a beneficiary: an off-ramp names a beneficiary by ID and never carries bank details itself.
Info
Off-ramps are not enabled on every environment. Where they are not, these
endpoints answer 501. Check /capabilities.
POST /treasury/beneficiaries¶
curl -X POST "$JET_API/treasury/beneficiaries" \
-H "Authorization: Bearer $JET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Northwind Payments GmbH",
"accountId": "DE89370400440532013000",
"bic": "COBADEFFXXX",
"bankName": "Commerzbank",
"city": "Frankfurt am Main",
"addressLine": "1 Kaiserstrasse",
"country": "DE",
"currency": "EUR"
}'
{
"id": "6e40ab02-1054-42be-86f3-98f5423105c2",
"name": "Northwind Payments GmbH",
"accountId": "DE89370400440532013000",
"bic": "COBADEFFXXX",
"bankName": "Commerzbank",
"city": "Frankfurt am Main",
"addressLine": "1 Kaiserstrasse",
"country": "DE",
"currency": "EUR",
"createdAt": "2026-08-21T10:02:00.000Z"
}
Needs the beneficiary.manage permission. Every field is required: a bank
payment rejected for an incomplete creditor costs a whole new off-ramp, so the
check happens here. The beneficiary is paid in its currency.
Warning
Registering is your assertion that the account is right. Nothing here confirms the account exists or belongs to the name you gave. Registration has no idempotency key, so a retried request registers the beneficiary twice; list first if you are unsure.
GET /treasury/beneficiaries lists them. They cannot be edited or deleted
through the API today.
GET /treasury/offramp/quote¶
What the beneficiary receives for this much USDC.
{
"pulled": { "amount": "500000000", "currency": "USDC", "scale": 6 },
"settlementCurrency": "EUR",
"atPar": false,
"feeBps": 0,
"fee": { "amount": "0", "currency": "EUR", "scale": 2 },
"deliveryFee": { "amount": "203", "currency": "EUR", "scale": 2 },
"minimum": { "amount": "204", "currency": "EUR", "scale": 2 },
"net": { "amount": "42612", "currency": "EUR", "scale": 2 },
"fxBufferBps": 75,
"quoteId": "oq_5b1c7e0a-…",
"expiresAt": "2026-08-21T10:05:00.000Z"
}
netis what the beneficiary receives. Where the conversion rate is not yet known, the quote holdsfxBufferBpsback from the expected rate so thatnetis a figure JET can honour.quoteIdis present whennetis. Pass it toPOST /treasury/offrampand the beneficiary is paid exactly thatnet. Without it, the off-ramp pays whatever the conversion produces.
POST /treasury/offramp¶
curl -X POST "$JET_API/treasury/offramp" \
-H "Authorization: Bearer $JET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "offramp-2026-08-21-a",
"amount": { "amount": "500.00", "currency": "USDC" },
"beneficiaryId": "6e40ab02-1054-42be-86f3-98f5423105c2",
"quoteId": "oq_5b1c7e0a-…"
}'
The amount and beneficiaryId must match the quote, or the request is refused
with 422 before anything moves. An expired or already-used quote is refused the
same way; request a new one. chain is accepted only as your home network; omit
it.
GET /treasury/offramp/:offRampId¶
{
"offRampId": "offramp-2026-08-21-a",
"stage": "PAID",
"state": "PAID",
"net": { "amount": "42612", "currency": "EUR", "scale": 2 },
"fee": { "amount": "0", "currency": "EUR", "scale": 2 },
"deliveryFee": { "amount": "203", "currency": "EUR", "scale": 2 },
"timeline": [
{ "stage": "PULLING", "at": "1" },
{ "stage": "PULLED", "at": "2" },
{ "stage": "DEPOSITING", "at": "3" },
{ "stage": "CREDITED", "at": "4" },
{ "stage": "CONVERTING", "at": "5" },
{ "stage": "SETTLING", "at": "6" },
{ "stage": "PAYING", "at": "7" },
{ "stage": "PAID", "at": "8" }
]
}
stage is where it is now; state is how it ended, once it has: PAID,
REFUNDED (your USDC came back), FAILED, or IN_DOUBT. In the timeline, at
orders the events. It is not a timestamp.
You also get an offramp.paid
webhook when the beneficiary is
paid. GET /treasury/offramps lists your off-ramps, newest first,
cursor-paginated (limit up to 200).