POST /quotes
Create a quote request. Seismic prices every eligible rail in parallel and returns a ranked snapshot.
For the conceptual model (directionality, rail fan-out, fee invariants), see the Quotes overview.
Query params
| Name | Default | Meaning |
|---|---|---|
wait | 0 | 1 / true / yes / on → block until every option has been priced. |
named | 0 | 1 / true / yes / on → return only named options. |
Named payouts
A named payout reaches the beneficiary under your customer's own legal name, so the recipient's bank shows the customer as the sender instead of an intermediary. Whether a payout is named depends on the route it takes, so each Quote reports it in is_named.
- Without
named, every option is returned and ranked by price, named or not. Readis_namedto tell them apart. - With
named=1, only named options are returned, sobest_quote_idis the best named option. The snapshot remembers the choice (request.named: true): reading it again withGET /quotes/:idlists only named options, and accepting it only takes a named one. That holds both when you omitquote_idand when you pass one. An unnamedquote_idis rejected with400 QuoteIdInvalid. - With
named=1, only routes that can be named are priced. If none of the routes that would price this payment for your organization can be named, the request fails at once with422 NoNamedQuoteand no snapshot is created; if one could but was briefly unavailable, it fails at once with a retryable503(withRetry-After), and the same request can be sent again. - A
pendingsnapshot lists no options until pricing finishes. If pricing finishes without a named option, the snapshot (in the create response withwait=1, or fromGET /quotes/:id) returns the error of a named route that failed (the same error you'd get withoutnamedwhen every option fails), a503 ServiceUnavailablewith noRetry-Afterheader when a named route couldn't be priced right now, or422 NoNamedQuotewhen no named route priced. That snapshot won't change, so create a new quote, withoutnamedto see every option. namedturns the filter on with1,true,yesoron, and off with0,false,nooroff(case-insensitive). Any other value, including a bare?named, is a400with the message"`named` must be one of `1`, `true`, `yes`, `on`, `0`, `false`, `no`, `off` (case-insensitive).".is_namedis always present. A payout that lands in a wallet rather than a bank account is never named.
curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/quotes?wait=1&named=1" \
-H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \
-H "content-type: application/json" \
-d '{
"end_customer_id": "0d8a7f30-…",
"amount": "1000",
"source": "acc_usdc_base_1a2b",
"destination": "acc_eur_sepa_9c8d"
}'Request body
QuoteRequest. The source says where the funds come from: an attached account, or an inline { currency, rails? } descriptor when no funding account exists yet. The destination is always a registered account: a bare account-id string, or { account, rails } when pinning a rail.
| Variant | Source | Destination | Fan-out |
|---|---|---|---|
| Paying from a virtual account | virtual account id | account id | rails read off both accounts |
| Generating funding instructions | { currency, rails? } | account id | every matching source rail |
Funding with a virtual account
Source is a virtual account Seismic provisioned for the customer. Destination is the customer's registered payout account. Both are bare account-id strings. Rails are read off each account. Accept the response to place a real order. No further account collection needed.
{
"end_customer_id": "0d8a7f30-…",
"end_customer_type": "individual",
"amount": "500",
"source": "acc_usdc_base_1a2b",
"destination": "acc_usd_ach_9c8d"
}end_customer_id + end_customer_type identify the customer this quote is for. The org-member caller is membership-checked against the customer's owning org. end_customer_type is one of individual, business, or org — but payments can only be created for end customers, so naming your org rejects the request with OrgSelfTransactingDisabled (see Customer types). end_customer_id is required on every variant below. It's omitted from the remaining bodies for brevity, but a request without it is rejected. end_customer_type is optional (Seismic resolves the id when it's absent), but pass it: naming the wrong type fails immediately with the correct type in the error, instead of quoting a record you didn't mean. The full example at the bottom of this page shows a complete body.
The source account must be one Seismic provisioned for the customer (origin: virtual). An origin: external account can't fund an order, and the request is rejected with a 400 at quote creation:
- To quote a transfer out of an external wallet, omit
accountand pass the wallet inline via the source leg'saddressfield. - For an external bank account, use an inline source leg with just
currency/rails(see Generating funding instructions). The funding instructions returned with the quote tell the customer where to send the fiat.
The destination account is the normal payout target and may be either origin: external (an account the customer brought) or origin: virtual. When it supports more than one rail, omitting rails prices every rail it supports: one quote per rail.
A payout, meaning a stablecoin source paying into a fiat currency, must go to an origin: external bank account. A fiat virtual account converts what it receives back into stablecoin, so it can't receive a payout, and the request is rejected with a 400 at quote creation. If the customer has no external bank account in the destination currency, the error says so; register one first. The rule applies in sandbox too, except for the sandbox-only SUSDC: a sandbox virtual account keeps what it receives, so an SUSDC payout into one is accepted for testing.
Generating funding instructions
No funding account yet: describe the source as an inline { currency, rails? } descriptor and Seismic fans out across the matching rails. Omit rails to compare every rail for that currency, list several to compare exactly those, or use schemas ("us_bank", "evm", …) to name a whole group at once. The destination stays a registered account.
{
"amount": "1000",
"source": {
"currency": "USD",
"rails": ["ach", "wire"]
},
"destination": "acc_usdc_base_3c92"
}Fixing the destination amount (exact-out)
By default amount fixes the source leg: you say what the customer pays, and each quote tells you what the destination receives. Pass "amount_leg": "destination" to flip that: amount is then denominated in the destination currency, the recipient lands exactly that amount, and each quote's in.amount tells you what the customer must pay to deliver it. Works with either source form above.
{
"amount": "50000",
"amount_leg": "destination",
"source": "acc_usdc_base_1a2b",
"destination": "acc_inr_imps_9c8d"
}Every quote in the snapshot delivers the same fixed payout, so ranking flips with the mode: exact-out snapshots order by cheapest in.amount first (for exact-in it's highest out.amount first). The request echo carries the amount_leg the snapshot was priced with. See Fixing an amount on the overview.
Add your own fee
Set developer_fee_bps to charge your own markup on top of Seismic's pricing, in basis points (100 = 1%, capped at 10000 = 100%). It is collected as part of the quote and remitted to you. Settlement is manual (today, monthly). The fee is denominated in the source-leg currency and surfaces as a developer_fee-described line on fees.items[] (see Developer fee), so the customer sees exactly what you're adding. Stack it on top of any variant above.
{
"amount": "1000",
"source": {
"currency": "USD"
},
"destination": "acc_usdc_base_3c92",
"developer_fee_bps": 100
}Set developer_fee_flat to charge a fixed amount per payment instead of — or alongside — the percentage. It's a decimal string in the source-leg currency, and it gets its own line on fees.items[] so your customer sees the two parts separately.
{
"amount": "1000",
"source": {
"currency": "USDC"
},
"destination": "acc_inr_imps_9c8d",
"developer_fee_bps": 100,
"developer_fee_flat": "1.50"
}The flat fee comes off the source amount first, and the percentage applies to what's left — so the two combined are always less than charging both against the full amount.
Flat fees are supported on stablecoin-funded payments. Requesting one on a corridor that can't carry it returns a 400 naming developer_fee_flat, rather than quietly pricing the quote without it.
Response 200
Each priced option in quotes[] includes acceptance_requirements. Wait for a ready snapshot and check the option you intend to accept; see Quote acceptance requirements.
Below: the response to Generating funding instructions request above. The server fanned out across the requested USD-in rails and returned three options, ranked by out.amount. The request echo comes back resolved: each leg is the { account_id?, currency, rails } corridor the server priced: account_id is present whenever the leg was account-backed (always, for destinations), and the destination's currency / rails are filled in from the account even though the request only sent its id.
{
"id": "9d8c…",
"status": "ready",
"created_at": "2026-04-23T10:20:00Z",
"request": {
"end_customer_id": "0d8a7f30-…",
"end_customer_type": "individual",
"amount": "1000",
"amount_leg": "source",
"source": {
"currency": "USD",
"rails": ["ach", "wire"]
},
"destination": {
"account_id": "acc_usdc_base_3c92",
"currency": "USDC",
"rails": ["base"]
},
"named": false
},
"quotes": [
{
"id": "q_2c1b…",
"in": {
"amount": "1000",
"currency": "USD",
"rail": "ach",
"account_id": "acc_8a01…"
},
"out": {
"amount": "995",
"currency": "USDC",
"rail": "base",
"account_id": "acc_usdc_base_3c92"
},
"fees": {
"items": [
{
"type": "flat",
"amount": "2.50",
"currency": "USD"
}
],
"total": "2.50",
"currency": "USD"
},
"acceptance_requirements": {
"files_required": false,
"source_account_number_required": false
},
"rate": "0.995",
"is_named": false,
"supported_expired_quote_behaviors": [
"refund_payment",
"flexible_destination_amount",
"flexible_source_amount"
]
},
{
"id": "q_9e4d…",
"in": {
"amount": "1000",
"currency": "USD",
"rail": "wire",
"account_id": "acc_5d77…"
},
"out": {
"amount": "974",
"currency": "USDC",
"rail": "base",
"account_id": "acc_usdc_base_3c92"
},
"fees": {
"items": [
{
"type": "flat",
"amount": "25.00",
"currency": "USD",
"description": "Wire transfer fee"
}
],
"total": "25.00",
"currency": "USD"
},
"acceptance_requirements": {
"files_required": false,
"source_account_number_required": false
},
"rate": "0.974",
"is_named": false,
"supported_expired_quote_behaviors": []
}
],
"best_quote_id": "q_7f3a…"
}Fees
in.amount is fee-inclusive: what the user pays. out.amount is what lands at the destination, with fees already priced in (nothing is added on top of in.amount). fees.items[] breaks down the difference between in.amount × rate and out.amount for transparency. See Amounts and fees on the overview.
Developer fee
When you pass developer_fee_bps, your markup appears as its own line on fees.items[], denominated in the source-leg currency:
{
"type": "percentage",
"bps": 100,
"amount": "10",
"currency": "USD",
"description": "Developer fee"
}amount is developer_fee_bps / 10000 of in.amount net of the other fees on the quote — so with no other fees on a 1000 source, a 100 bps fee is 10.
A developer_fee_flat arrives as a second, separate line:
{
"type": "flat",
"amount": "1.50",
"currency": "USD",
"description": "Developer flat fee"
}When you set both, the flat amount is deducted before the percentage is computed, so a 100 bps + 1.50 fee on a 1000 source is 1.50 + 9.985, not 1.50 + 10.
Both are already included in the quote's out.amount and total. Like every other fee line, they're priced in, not added on top.
Errors
400 (invalid request, including an origin: external account on the source leg, see Funding with a virtual account for the accepted shapes, a destination.account that isn't a registered account, a destination.rails entry outside the account's rails[], a payout from a stablecoin whose destination isn't an external bank account, and an end_customer_type that doesn't match the id, where the error names the id's actual type, see Customer types), 422 (no eligible options for this corridor, no named option when named=1 is set, or a destination.account that isn't registered for payouts yet because its holder is missing account_holder.relationship or account_holder.industry; the error carries a missing violation for each field and links the account's dashboard page, and adding them there or with PATCH /v0/customers/{id}/accounts/fiat/{account_id}/holder registers the account).
Minimum amounts can differ between the options available to a customer. An option whose minimum the amount does not clear is left out of the response, and the options it does clear are still offered. If no option clears its minimum, the request returns 422 with a message naming the minimum. A quote whose fees exceed the amount sent counts as below the minimum.
If a payment minimum applies but the amount received cannot be valued in USD, the affected quote is unavailable. Other valid quotes remain in the response. If no valid quote remains and minimum verification is unavailable, the snapshot returns the 422 of any route that rejected the request for a reason you can fix (for example, an amount below that route's minimum). Otherwise it returns a 503 ServiceUnavailable with no Retry-After header, whose message says to create a new quote: the snapshot is final and polling it does not reprice its quotes. The same answer comes back when a route was briefly unavailable while pricing and nothing else priced. Submit a new POST /v0/quotes request.
Example
curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/quotes?wait=1" \
-H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \
-H "content-type: application/json" \
-d '{
"end_customer_id": "0d8a7f30-…",
"end_customer_type": "individual",
"amount": "1000",
"source": {
"currency": "USD"
},
"destination": "acc_usdc_base_3c92"
}'