Quotes
Quotes price a transfer before you commit to it. Describe a source, destination, and amount, and Seismic fans out across every eligible rail and returns snapshots. Accept a snapshot to place an order.
Endpoints
GET /quotes/requirements: discover document rules before requesting a quote.POST /quotes: create a quote request, fan out across rails.GET /quotes/:id: re-read a snapshot by id.POST /quotes/:id/accept: turn a snapshot into an order.
Quote acceptance requirements
What accepting a quote requires is returned on each priced quote as acceptance_requirements. No additional request is needed to retrieve the evaluated requirements:
POST /quotes: use?wait=1to wait for priced quotes, and?named=1to receive only named options.GET /quotes/:id: poll an asynchronous snapshot until itsstatusisready.
Read the field on the option you intend to accept in snapshot.quotes[]. Each option includes an object such as:
{
"acceptance_requirements": {
"files_required": true,
"source_account_number_required": false
}
}files_required:
| Value | Before accepting |
|---|---|
true | Collect at least one supporting document and include it in the accept request's files array. |
false | No supporting document is required by this quote's preflight check. |
| Absent on an older server | The requirement is unknown; keep handling the typed document-required rejection. |
source_account_number_required:
| Value | Before accepting |
|---|---|
true | Collect the bank account number the payment will be funded from and send it as the accept request's source_account_number. The local network needs the originating account declared before funds are sent. |
false | Omit source_account_number; accept rejects it with a 400. |
Requirements depend on the priced quote, so check the selected option again when you request a new quote. These payment requirements are separate from the fields needed to register an external fiat account.
To collect documents before pricing, use GET /quotes/requirements. It returns the rule and any machine-readable condition without requiring a customer or recipient account.
| Destination currency | Supporting-document rule |
|---|---|
| INR, CNY, GHS | Required at every amount. |
| BRL | Required when the destination amount is at least 50,000 BRL, including exactly 50,000. |
| Other catalog currencies | Not required by the current payment policy. |
For source-denominated requests, rates and fees determine the destination amount; check the priced quote before acceptance. The discovery response describes document policy, not whether a customer or corridor is eligible for payment.
See Compliance attachments for a preflight example and Document required for file constraints and the typed rejection. Additional documents may be requested during processing.
Directionality
The quote API is currency agnostic. On-ramps, off-ramps, and stablecoin swaps are all the same shape. You describe both sides of the trade and the amount to price.
| Scenario | source | destination |
|---|---|---|
| On-ramp | fiat (USD via ach / wire, …) | stablecoin (USDC on base, …) |
| Off-ramp | stablecoin | fiat (EUR via sepa, …) |
| Swap | stablecoin | stablecoin (often a different rail) |
Not every currency/rail pair settles both ways. Several local payout rails are destination-only — INR via imps, for instance: payouts to an Indian bank account are supported, but sourcing funds from one (an India-based off-ramp) is coming soon. Quoting such a pair as the source returns CorridorUnsupported.
Read the restriction off the catalog rather than hard-coding it: each entry in GET /currencies carries a direction of source, destination, or both.
The source is one of three forms:
- A bare virtual account ID string:
"acc_1a2b…". Shorthand for an attachedAccount. Currency is pinned by the account row (every account has a singlecurrency). A virtual account's rails describe how it can be funded, not how a payment drawn from its balance moves, so there's nothing to pin here: if it supports several, the server prices each and returns one quote per rail. See Narrowing an account's rails. - A virtual account ID with disambiguation:
{ account, rails?, schemas? }. Use this for fiat accounts that settle over multiple rails (e.g. a US bank reachable via bothachandwire). The fields you set must be in the account'srails[]. - An inline source descriptor:
{ currency, rails?, schemas?, address? }. When there's no virtual account to debit, describe what the end customer will send instead. Accepting quotes from this form returns funding instructions.
The destination is one of two forms:
- A bare account ID string:
"acc_9c8d…". Shorthand for an attachedAccount. Currency is pinned by the account row (every account has a singlecurrency). An account that settles over several rails is priced on each, one quote per rail — see Narrowing an account's rails. Note that unlike the corresponding form under source, this account ID may point to virtual or external accounts. - An account ID with disambiguation:
{ account, rails }. When you only care about a subset of the account's availablerails[].
The source leg decides settlement timing. If a virtual account is provided, the account is debited immediately upon quote acceptance and the payment settles to the destination. Otherwise, funding instructions are returned upon quote acceptance and the payment only settles when funds arrive.
rails is the settlement path set and unifies fiat + crypto into one field. Always plural: each side of a quote can ask for multiple rails at once and the server fans out one quote per rail.
rails takes any rail name from GET /rails, for example ach, sepa or pix for fiat and base or ethereum for crypto. See Local rails for the full list.
schemas lets you name a group of rails: "us_bank" instead of ["ach", "wire", "swift"], "evm" instead of ["base", "ethereum", "polygon", "arbitrum"]. Server-side, the schema is expanded and unioned with rails to form the fan-out set. See GET /schemas.
Fan-out and rails
On a descriptor, rails and schemas are both optional:
- Specify
rails: ["ach"]to pin a single settlement path. Multiple entries (rails: ["ach", "wire"]) fan out across exactly those rails. - Specify
schemas: ["us_bank"]to ask for every rail in that group. Schemas and rails get unioned:rails: ["base"]+schemas: ["us_bank"]would price[base, ach, wire, swift](assumingus_bank → [ach, wire, swift]). - Omit both to compare every rail that corridor supports. The server returns one quote per rail.
When the leg is an account, the account's rails are used directly (no fan-out on that side), unless the account supports multiple rails, in which case omitting rails / schemas fans out across all of them. To pin a subset, pass either field with the rails / schemas you want.
Narrowing an account's rails
A single account can support multiple rails (a US bank account that accepts both ach and wire). Referencing it by bare id is never an error on either leg — the server prices every rail the account supports and returns one quote per rail. Currency is unambiguous from the account side too: every account has a single currency today.
Narrow the fan-out by replacing the bare-string form with { account, currency?, rails?, schemas? }:
{
"amount": "200",
"source": {
"account": "acc_evm_wallet_1a2b",
"currency": "USDC",
"rails": ["base"]
},
"destination": "acc_usd_ach_9c8d"
}You can also omit rails and let the server fan out across the account's rails[] for that currency, e.g. { account, currency: "USDC" } will return one quote per supported chain. Or pin a subset via schemas: ["evm"] to get every EVM chain at once.
On the response, every quote's in / out has rail populated (singular, each quote is for exactly one rail). The caller never has to disambiguate. For a crypto leg, out also carries the destination address once known.
amount is denominated in the currency of the leg named by amount_leg: the source currency unless you say otherwise. See Fixing an amount.
Fixing an amount
amount_leg picks which side of the trade amount fixes. The other side is what gets priced:
"source"(the default), "exact-in": you fix what the customer pays, and each quote'sout.amounttells you what the destination receives. Quotes rank highest-payout-first."destination", "exact-out": you fix what the recipient receives, and each quote'sin.amounttells you what the customer must pay to deliver exactly that. Quotes rank cheapest-pay-in-first.
Exact-out is the natural fit for payroll and invoicing flows, where the payout is the contract: the recipient must land exactly amount, and the pay-in absorbs the fees and rate.
{
"amount": "50000",
"amount_leg": "destination",
"source": "acc_usdc_base_1a2b",
"destination": "acc_inr_imps_9c8d"
}Amounts and fees
in.amount: what the user pays. Fee-inclusive.out.amount: what lands at the destination. Fees are already priced in. Nothing is added on top ofin.amountto get here.fees.items[]: a breakdown of the difference betweenin.amount × rateandout.amount, surfaced for transparency.
These invariants hold in both amount modes. amount_leg only decides which of the two amounts you fixed and which one Seismic priced.
Fee breakdown
Each fees block has:
| Field | Type | Meaning |
|---|---|---|
items[] | Array of tagged fee lines (flat, percentage, network, other) | One entry per distinct charge. When no itemised breakdown is available, a single flat line equal to total is emitted. |
total | String | Sum across items, denominated in currency. |
currency | String | Fee currency: typically the source currency (what the caller pays with). |
items[] is a tagged discriminated union:
flat:{ type, amount, currency, description? }. Fixed charge.percentage:{ type, bps, amount, currency, description? }. Basis points off the source amount minus any flat fees. Note that flat fees are deducted before percentage fees are applied.network:{ type, amount, currency, description? }. On-chain gas / settlement cost for crypto legs.other:{ type, amount, currency, description }.descriptionis required so the caller can at least render a label.
Types
QuoteRequest
The body shape for POST /quotes.
| Field | Type | Required | Notes |
|---|---|---|---|
end_customer_id | string (UUID) | yes | The customer this quote is for. The caller is membership-checked against the customer's owning org. |
end_customer_type | "individual" | "business" | "org" | no, but pass it | Discriminator for end_customer_id, see Customer types. When omitted, Seismic resolves the id automatically. Passing it catches a mistyped id immediately. |
amount | string | yes | Denominated in the currency of the leg named by amount_leg: the source currency by default. |
amount_leg | "source" | "destination" | no | Which leg amount fixes, see Fixing an amount. Defaults to "source" (exact-in). Pass "destination" to guarantee the payout amount (exact-out). |
source | Source | yes | Account-id string, or inline descriptor. |
destination | Destination | yes | Account-id string, or { account, rails }, always a registered account. |
developer_fee_bps | integer (bps) | no | Your own markup on top of Seismic's pricing, in basis points (100 = 1%, max 10000). It is collected and sent to you. Surfaces as a Developer fee line on the quote's fees.items[]. See Add your own fee. |
Customer types
end_customer_type tells Seismic which kind of record end_customer_id refers to:
individual: a person onboarded as aCustomerwithtype: "individual".business: a company onboarded as aCustomerwithtype: "business".org: your own organization. The value remains on the wire for compatibility, but payments can only be created for end customers: a quote or accept that names your org — viaend_customer_type: "org", your org id asend_customer_id, or a leg funded through an organization-owned account — is rejected withOrgSelfTransactingDisabled(403). Create the payment for an end customer instead. Existing organization-owned accounts stay readable (details, balances, payment history).
end_customer_type may be omitted. Seismic resolves the id across the types above. Pass it anyway: it documents intent, and naming the wrong type fails immediately with the correct type in the error instead of quoting a record you didn't mean. For example, an org id sent as business returns a 400 (correcting the type won't make the quote succeed — an org as end customer is then rejected with OrgSelfTransactingDisabled):
{
"ok": false,
"error": {
"code": "InvalidRequest",
"violations": [
{
"field": "end_customer_type",
"reason": "not_in_allowed_set",
"description": "0d8a7f30-… is an org; specify \"end_customer_type\": \"org\"",
"allowed": [
"org"
]
}
],
"message": "0d8a7f30-… is an org; specify \"end_customer_type\": \"org\""
}
}Ids that don't exist, or that belong to a different org, return the uniform CustomerNotFound regardless of the type passed.
Source
The source-leg shape: a discriminated union, one of string, account-with-disambiguation, or inline-descriptor.
account-with-disambiguation:
| Field | Type | Required | Notes |
|---|---|---|---|
account | string | yes | Attached account UUID. Currency is pinned by the account row (single currency per account today). |
rails | string[] | no | Subset of the account's rails[]. Omit to fan out across all of them. |
schemas | string[] | no | Named rail groups, see GET /schemas. Unioned with rails to form the fan-out set. |
inline-descriptor:
| Field | Type | Required | Notes |
|---|---|---|---|
currency | string | yes | ISO-4217 ("USD") or stablecoin symbol ("USDC"). |
rails | string[] | no | Pin a set of rails. Omit to fan out across every rail this corridor supports. |
schemas | string[] | no | Named rail groups: ["us_bank"], ["evm"], etc. Unioned with rails. |
address | string | no | Crypto sources only. The sending wallet's address. |
Destination
The destination-leg shape: a discriminated union, one of string or account-with-disambiguation. Unlike Source, there is no inline-descriptor form (a destination is always a registered account) and the object form takes only account and an optional rails. Any other field is rejected.
account-with-disambiguation:
| Field | Type | Required | Notes |
|---|---|---|---|
account | string | yes | Attached account UUID. Currency is read off the account row. |
rails | string[] | no | Subset of the account's rails[]. Omit to fan out across all of them. |
To pay out somewhere the customer has no account yet, register the account first via POST /customers/:id/accounts. This applies both to first and third party payments.
AsyncQuoteSnapshot
Returned by POST /quotes and GET /quotes/:id.
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | Snapshot id. Pass to /accept. |
status | "pending" | "ready" | "expired" | pending only with ?wait=0. ready once every option has been priced. |
quotes | array of Quote | Ranked best-first. One entry per rail. |
best_quote_id | string (optional) | The server's recommended quote id. Matches quotes[0].id today. |
request | object | Echo of the original request, resolved: each leg comes back as { account_id?, currency, rails }, account_id present when the leg was account-backed (always, for destinations). named is true when the quote was created with ?named=1. |
created_at | string (ISO-8601) |
Quote
A single quote. Its acceptance_requirements tells you what to collect before accepting this option; it is separate from account-registration requirements.
| Field | Type | Notes |
|---|---|---|
id | string | Opaque. Pass to /accept to pick this exact option. |
in | Leg | Source side. amount is fee-inclusive (what the user pays). |
out | Leg | Destination side. amount is what lands. |
fees | QuoteFees | Itemized fees + total. |
rate | string | Effective out.amount / in.amount. |
expires_at | string (ISO-8601, optional) | When this specific quote stops being honored. |
is_named | boolean | true for a named payout: the payment reaches the beneficiary under the customer's own legal name. Always present, whether or not the quote was created with named. |
acceptance_requirements | object | What accepting this quote requires; see Quote acceptance requirements. |
supported_expired_quote_behaviors | string[] | Expired-quote behaviors /accept will honor for this quote via its expired_quote_behavior field. Empty when only the default applies: the payment is refunded on expiry. A quote whose source leg is funded manually omits flexible_source_amount. See Expired-quote behavior. |
Leg
The resolved-side shape: used by Quote.in / Quote.out here, and by RampOrder.source / destination on orders. The order is the settled view of the accepted quote, so the per-leg shape is canonical across both surfaces. Distinct from the request-side Source / Destination unions: by the time a quote comes back, every leg is resolved to one concrete corridor.
| Field | Type | Notes |
|---|---|---|
amount | string | Denominated in currency. Fee-inclusive on in. What lands on out. |
currency | string | ISO-4217 or stablecoin symbol. |
rail | string | Always present (singular): the one rail this option priced. |
address | string (optional) | Crypto legs only. Echoed from the descriptor or from the resolved Account's wallet. |
account_id | string (UUID) | Always present: every leg resolves to a real account (caller-attached or server-side). |
For the full account record, look up account_id via GET /accounts/:id.
