Partner API

Put Spout inside your product

Your users borrow stablecoins at 0% interest against tokenized US equities, or lend into a tranched pool funded by systematic covered call premium. One API, non-custodial throughout, and you never touch a key.

>_ open a loan, three steps sandbox
# 1. Spout builds. Nothing executes.
POST /v1/borrow/loans
{ "asset": "AAPL", "collateral_amount": "45.4",
  "borrow_amount": "4222.20" }

# response
{
  "transaction": {
    "unsigned": "AQABA0pWZmZmZm...b64",
    "id": "txn_5pR8mK",
    "expires_at": "2026-08-24T14:32:30Z"
  },
  "simulation": {
    "summary": "Borrow 4,222.20 USDC against 45.4 AAPL"
  }
}

# 2. Your wallet adapter signs it.
# 3. You submit it. Only now does the loan exist.

Read this first

The API builds transactions. It does not execute them.

Spout is a non-custodial protocol on Solana. The API never moves user funds and never holds a private key. If you have integrated a custodial fintech API before, this is the one structural difference that shapes your whole integration, so it is worth understanding before you read anything else.

01

You ask for a transaction

POST /v1/borrow/loans returns an unsigned, base64 serialized Solana transaction and a transaction id. No loan exists yet.

02

Your user signs it

Your wallet adapter deserializes it, shows the user what they are approving, and submits it to Solana. Spout is not in this step.

03

You confirm

Poll GET /v1/transactions/{id}, or take the transaction.confirmed webhook and skip the polling entirely.

Two consequences worth internalising now.

Built transactions expire. They carry a recent blockhash. If your user takes too long to sign, expires_at passes and the submit fails. Rebuild rather than retrying the stale one, and never build a transaction and hold it while the user reads a confirmation screen.

A 200 means "here is a valid transaction", not "it happened." Nothing is real until it confirms on chain.

Every write endpoint that builds a transaction also returns a simulation block: a plain-language summary and a list of before and after effects. Render that as your confirmation screen. Users who cannot read what they are signing do not sign.

Quickstart

Running in about a minute

The sandbox is served from the specification itself, so it validates your requests the same way the live API will and returns the shapes the spec promises. During the preview any bearer token is accepted, but the header itself is still required: send it and a missing-key bug shows up now rather than on launch day.

Call the hosted sandbox

>_ shell sandbox
# protocol state, a good call to make on app boot
$ curl -H "Authorization: Bearer sk_test_anything" \
     https://sandbox.api.spout.finance/v1/protocol/stats

# what a partner sees before building a borrow screen
$ curl -H "Authorization: Bearer sk_test_anything" \
     "https://sandbox.api.spout.finance/v1/assets?limit=5"

Or run the whole thing locally

The repository carries the same sandbox, an annotated reference integration and an MCP server. It is private during the preview, so ask us for access and we will add you.

>_ shell local
$ git clone https://github.com/SpoutFinance/spout-builders
$ cd spout-builders
$ npm install

$ npm run mock        # terminal 1: the sandbox on 127.0.0.1:4010
$ npm run example     # terminal 2: an annotated borrow flow against it

The example walks the full flow and prints what a real integration would do at each step, including the parts it cannot do without a wallet. It is written to be read as much as run.

Test your error paths early

Most integrations that fail in production fail because nobody exercised the error branches, and those are the ones a user hits at the worst moment. The sandbox honours the Prefer header so you can force any status or named example on demand.

>_ shell force a 422
$ curl -H "Authorization: Bearer sk_test_anything" \
     -H "Prefer: code=422, example=protocol_paused" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: manual-test-1" \
     -d '{"wallet":"7Np41oe...","asset":"AAPL","collateral_amount":"10","borrow_amount":"1000"}' \
     https://sandbox.api.spout.finance/v1/borrow/loans

Named examples available on the 422: exceeds_max_ltv, insufficient_collateral, asset_not_accepting_deposits, protocol_paused, stale_price_feed.

Concepts

Three things partners get wrong

None of these are API quirks. They are product facts that your UI has to reflect honestly, and each one has bitten someone.

Interest is 0%, and there is still a cost

The 0% is not promotional. Lender yield comes from covered calls written against the collateral, not from the borrower. But when a written call finishes in the money, the upside above the strike is given up, and that is a real cost to the borrower.

estimated_borrow_cost_apr on the asset object is our estimate of that cost, annualised from historical cycles. It is an estimate, it is not charged, and it is not deducted from the balance. Show it next to the 0% rate, never instead of it. Presenting 0% alone is misleading; presenting the cost alone is also misleading.

Health is per position, never pooled

Every partner arrives assuming one account-wide number. There is one health factor per loan, and liquidation reads only the loan that crossed its own 1.00. Drag the AAPL price and watch the NVDA rail stay exactly where it is.

AAPL Apple Inc. Healthy
Health Factor 1.50
1.00
45.4 shares at $232.50 debt $4,222.20
NVDA NVIDIA Corporation Close to liquidation
Health Factor 1.05
1.00

Unaffected. A separate loan with its own collateral, its own 1.00 and its own liquidation. Nothing you do to the AAPL position moves this rail.

This differs from Aave, Compound and every margin account your users have used, which is exactly why GET /v1/borrow/health returns an object with a positions array, one entry per collateral asset. There is also an aggregate block for a summary line and it is documented as display only. If you compute a single portfolio health number and present it as a safety signal, your users will be liquidated while your UI says they are fine.

The lending pool has two tranches with different exits

SeniorJunior
YieldLowerHigher
Loss positionPaid firstFirst loss
Instant exitYes, at a dynamic haircutNo
Queued exitFree, at full NAV, 7 day noticeFree, at full NAV, 45 day notice

Junior absorbs losses before senior and is compensated for it. There is no instant exit from junior. Call POST /v1/lend/withdrawals with preview: true and show the user the haircut or the estimated wait before they commit to anything. A lender who discovers a notice period after clicking withdraw is a support ticket and a complaint.

APY figures are estimates from trailing realised cycles. Label them as estimates in your UI. They are not guarantees and they will move.

Conventions

How the API behaves

Authentication

Authorization: Bearer sk_test_... for the sandbox, sk_live_... in production. Keys are environment scoped and never interchangeable, so a test integration cannot touch live state.

Idempotency

Every POST accepts Idempotency-Key. Replaying a key returns the original response. Reusing one with a different body returns 409.

Generate the key where you decide to perform the action, not inside your HTTP helper. A key minted per request is new on every retry, which gives you no protection while looking like it does.

Amounts

Decimal strings, never JSON numbers. "1250.75". Use a decimal library: parseFloat will appear to work in testing and lose money in production.

Every amount is paired with an asset, and decimals sits on the asset object, so the scale is never a guess.

Pagination

Cursor based. starting_after walks forward, ending_before walks backward, limit caps at 100.

There are no offsets, deliberately: offset pagination skips or repeats rows when the underlying set changes between pages, and it will change.

Errors

One envelope everywhere, with a stable code, a human message, the offending param, a doc_url and a request_id.

Branch on code, never on message. Codes are enumerated in the spec; messages are written for humans and will be reworded.

Versioning and request IDs

The version is pinned per key and overridable per request with Spout-Version. Additive changes ship without a bump; breaking changes get a new dated version and the old one keeps working.

Every response carries Spout-Request-Id. Log it, and quote it when you ask us for help.

>_ error envelope every endpoint
{
  "error": {
    "type": "invalid_request_error",
    "code": "exceeds_max_ltv",
    "message": "Requested borrow of 6000.00 USDC exceeds the maximum of 5277.75 at 50% LTV for AAPL.",
    "param": "borrow_amount",
    "doc_url": "https://spout.finance/docs/errors#exceeds_max_ltv",
    "request_id": "req_9xKq2mB"
  }
}

Webhooks

Events, and the one to wire first

Wire loan.at_risk before anything else. It is the event that keeps your users from being surprised, and it is the difference between a product that warns people and one that does not.

EventFires when
loan.at_riskA loan's health crossed a warning threshold
loan.liquidatedCollateral was partially or fully liquidated
cycle.settledAn options cycle settled and lender yield was distributed
withdrawal.availableA queued withdrawal is claimable
transaction.confirmedA transaction you submitted confirmed on chain

Delivery is at least once and ordering is not guaranteed. Deduplicate on the event's top-level id, and never infer state from the sequence you receive: re-read the object instead. Verify the Spout-Signature header before you trust the body.

Your webhook endpoint is yours, not ours, so it does not carry a Spout bearer token. The signature is the authentication.

AI agents

Point your coding agent at the spec

If you build with Claude Code, Cursor or anything else that speaks MCP, run our MCP server locally and the agent can query the specification while it writes your integration. That beats pasting the spec into a prompt: the agent pulls only the endpoint it is working on, and it cannot work from a stale copy.

>_ mcp config claude code
{
  "mcpServers": {
    "spout": {
      "command": "node",
      "args": ["/absolute/path/to/spout-builders/mcp/server.js"]
    }
  }
}
ToolWhat it does
integration_notesThe things integrators get wrong. Have the agent read this first.
list_endpointsEvery operation including the inbound webhooks, filterable by tag.
get_endpointOne operation with references resolved inline. A bare path returns every method on it.
get_schemaA named object shape: Loan, Asset, Tranche, HealthSummary, Error.
list_error_codesThe stable error enumeration to branch on.
search_specFull text search when you know the term but not where it lives.

Agents that prefer documents to tools can read llms.txt or fetch openapi.yaml directly.

Next

Where to go from here

API reference

Every operation, parameter, schema and error, generated from the specification.

Open the reference

The specification

OpenAPI 3.1. Generate a client, load it into Postman, or diff it when we version.

Download openapi.yaml

Partner dashboard

Protocol state, your keys, the request log and webhook config, in one place.

Open the dashboard

Talk to us

Integration questions, missing endpoints, or a shape that does not fit your product.

builders@spout.finance