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.

1 Quote
2 Settle
3 Redeem

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.

create-quote.js
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.

settle.js
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"
// }
Idempotency: The 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:

  1. Create vendor token account (if the vendor's USDC account doesn't exist yet)
  2. Transfer to vendor — the net amount after fees
  3. 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.

redeem.js
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.

verify.js
// 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.

handler.js
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.

Timing and expiry

Quote expiry: 10 minutes by default. Customizable up to 24 hours via expires_in_seconds.

Redeem window: Configurable via redeem_window_seconds (30s to 7 days, default 15 min). After this, the settlement status becomes expired.

Expired settlements: Funds remain with the vendor on-chain, but the settlement token becomes invalid. APISettle marks them as expired automatically via a background job.

Retry safely: If a POST /settle call times out, retry with the same payment_attempt_id. The backend is idempotent — same ID always returns the same settlement, never double-charges.