Payment Flow
Every payment in APISettle follows the same deterministic cycle: create a quote, settle the payment, and redeem the receipt. This page covers each step in detail.
Vendor creates quote → Consumer settles on-chain → Vendor redeems to confirm → Delivers service
Creating quotes
POST /v1/quote
A quote is a signed pricing commitment. It specifies the amount, currency, expiry, and optional scope. The consumer uses it to initiate payment.
const res = await fetch('https://api.apisettle.com/v1/quote', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
service_id: 'svc_abc123',
quote_amount: '2000000', // $2.00
currency: 'USDC', // default
expires_in_seconds: 600, // 10 min (default)
redeem_window_seconds: 900, // 15 min redeem window
scope: { endpoint: '/data/enrich' }, // optional metadata
}),
})
const quote = await res.json()
// {
// quote_id: "q_...",
// quote_token: "eyJ0eX...",
// quote_amount: "2000000",
// fee_amount: "10000",
// currency: "USDC",
// expires_at: "2026-03-27T12:10:00Z",
// status: "pending"
// } Quote parameters
| Field | Type | Description |
|---|---|---|
| service_id | string | Your service UUID. Required. |
| quote_amount | string | Amount in USDC micro-units. Defaults to service price. |
| expires_in_seconds | number | Quote validity window. Default 600 (10 min), max 86400. |
| redeem_window_seconds | number | How long the settlement stays redeemable. 30 – 604800. |
| scope | object | Arbitrary metadata (max 4096 bytes). Signed into the token. |
Quote tokens
The quote_token is a compact signed string: base64url(payload).base64url(signature). It contains the quote ID, amount, vendor, expiry, and scope. The signature is Ed25519 — tamper-proof and verifiable.
Settling payments
POST /v1/settle
The consumer submits the quote token along with a unique payment attempt ID. APISettle validates the quote, executes the on-chain transfer, and returns a settlement token.
const res = await fetch('https://api.apisettle.com/v1/settle', {
method: 'POST',
headers: {
'Authorization': `Bearer ${CONSUMER_JWT}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
quote_token: quote.quote_token,
payment_attempt_id: 'pay_order_12345',
}),
})
const settlement = await res.json()
// {
// settlement_id: "stl_...",
// settlement_token: "eyJ0eX...",
// status: "confirmed",
// tx_hash: "5UzH3...",
// redeem_expires_at: "2026-03-27T12:25:00Z"
// } payment_attempt_id is your idempotency key. If you retry with the same ID, you get the same settlement back. The consumer is never double-charged.
What happens on-chain
When you call /settle, APISettle builds and submits a Solana transaction with up to three instructions:
- Create vendor token account (if the vendor's USDC account doesn't exist yet)
- Transfer to vendor — the net amount after fees
- Transfer to fee wallet — the platform fee
The transaction is signed by the platform fee payer and executed using the consumer's delegated spending authority. No private key leaves the consumer's wallet.
Redeeming settlements
POST /v1/settlements/:id/redeem
Redeem is the authorization gate. Call it before delivering your service. It atomically transitions the settlement to "redeemed" — it can only succeed once.
const res = await fetch(
`https://api.apisettle.com/v1/settlements/${settlement.settlement_id}/redeem`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
settlement_token: settlement.settlement_token,
}),
},
)
const result = await res.json()
// { settlement_id: "stl_...", status: "redeemed", redeemed_at: "..." }
if (result.status === 'redeemed') {
// Safe to deliver the service
} Single-use. Once redeemed, the settlement cannot be redeemed again. Enforced by database uniqueness constraint.
Idempotent retries. If your service crashes after redeeming but before delivering, retrying returns the same "redeemed" result.
Expiry enforced. Settlements expire after the redeem window. Attempting to redeem an expired settlement returns settlement_expired.
Verifying without redeeming
POST /v1/settlements/:id/verify
Need to check a settlement's status without consuming it? Use verify. This is useful for dashboards, audit logs, or pre-flight checks.
// Non-consuming check — does NOT mark as redeemed
const res = await fetch(
`https://api.apisettle.com/v1/settlements/${settlementId}/verify`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}` },
},
)
const info = await res.json()
// { status: "issued", tx_hash: "...", quote_amount: "2000000" } The 402 Payment Required pattern
The recommended integration pattern uses HTTP 402 to signal that payment is needed. When a caller hits your endpoint without a settlement token, return 402 with a quote. The caller pays and retries with the token.
export default async function handler(req, res) {
const token = req.headers['x-settlement-token']
const id = req.headers['x-settlement-id']
if (!token || !id) {
// No payment — issue a quote
const quote = await createQuote({ service_id: SVC, quote_amount: '500000' })
return res.status(402).json({
error: 'payment_required',
quote_token: quote.quote_token,
amount: '$0.50',
})
}
// Payment present — redeem
const { status } = await redeemSettlement(id, token)
if (status !== 'redeemed') {
return res.status(402).json({ error: 'payment_failed' })
}
return res.json({ data: 'paid result' })
} Fee structure
Fees are calculated per quote and shown in the fee_amount field of the quote response.
fee = max(quote_amount × bps, min_fee)
Default: 0.5% (50 bps) with a $0.01 minimum. The consumer pays the quote amount; the vendor absorbs the fee from the gross.
Settlement lifecycle
| Status | Meaning |
|---|---|
| issued | Payment confirmed on-chain. Ready to be redeemed. |
| redeemed | Vendor has consumed the settlement. Service should be delivered. |
| expired | Redeem window elapsed. Cannot be redeemed. |