Concepts
This page covers the rules common to every endpoint. For the full details of each field and error code, see the API reference.
Quotes and prepares
There are two kinds of calls for a trade:
- A quote (
POST /v1/quotes) returns the current rate for an amount, fees included. Quotes are public, free to repeat, and not binding. - A prepare (
POST /v1/lend/prepare,/borrow/prepareand so on) returns the transactions for a wallet to sign. This is called a payload. Prepares need an API key with thetradescope.
Every prepare gets its own quote, so you don't need to quote first. Use quotes to show rates, and prepare when the user is ready to go ahead.
Tenor never signs or sends anything. Your wallet, Safe or custodian does.
Payloads
Every prepare returns the same fields:
| Field | Contents |
|---|---|
summary | A one-line description of the action, to show before signing |
status | ready to send, or preview_only for a dry run |
steps | Every transaction, in order. The main one is last |
calls | The same transactions as one batch, for wallet_sendCalls and smart accounts |
transaction | The main transaction alone |
safe_tx | All the transactions as one Safe transaction |
requires | Approvals or one-time authorizations the wallet still needs. Each one is also in steps |
preview | The expected outcome: the amount, the rate with fees, the offers filled, the position before and after, and the wallet's balance changes |
validity, bounds | How long the transactions stay valid, and the limit rate and deadline they enforce |
warnings | Notices that don't block the action, such as OFFER_UNFUNDED or HEALTH_THRESHOLD |
Before returning a payload, Tenor simulates the transactions from the wallet on the latest block and runs these checks: approvals must be exact, the rate and deadline must be enforced onchain, and every call must go to Tenor's contracts. If a check fails, you get an error instead.
requires is based on the wallet's state when you prepared. Wait for one payload to confirm before you prepare the next.
If a Safe, a custodian or your own code changes the calls before signing, check them again with POST /v1/simulate.
Wallet kinds
Every prepare takes a wallet_kind, which sets how the transactions are packaged. Tenor doesn't infer the wallet kind, so always set wallet_kind. A value that doesn't match the wallet returns a WALLET_KIND_MISMATCH warning.
wallet_kind | How to send | Time to sign |
|---|---|---|
eoa | Each item in steps, one at a time, waiting for each to confirm | 5 min |
delegated_eoa (EIP-7702) | calls in one wallet_sendCalls, or like an eoa | 5 min |
safe | safe_tx as one Safe transaction | 30 min |
smart_account (ERC-4337 and others) | calls in one user operation | 30 min |
The time to sign is the longest the transactions can wait before they're confirmed. If you pass wallet_kind on a quote, the quote only uses offers that stay available for the full time to sign.
Limit rate and deadline
Every action that fills offers has two limits, enforced onchain:
limit_rateis the worst rate you accept: the lowest rate for a lend or a repay, and the highest for a borrow or a resell. If the fill would be worse, the transaction fails. Without alimit_rate, Tenor uses the worst rate in the quote, moved 0.10 percentage points against you, or 0.30 for asafeorsmart_account.deadline_atis the latest time the transaction can be confirmed, in Unix seconds. The deadline must be at least 60 seconds away, and within the time to sign. Without adeadline_at, the deadline is the end of the time to sign.
After validity.until_at, the transactions fail instead of filling late. If you can't sign before until_at minus validity.recommended_margin_s, prepare again.
Some actions don't fill offers: redeems, direct repays, collateral changes and variable-rate actions. They have no quote, and their bounds and validity.until_at are null.
Dry runs and test keys
Add dry_run: true to any prepare to preview the action without getting anything to send. Tenor runs the same quote, simulation and checks, as if the wallet had the funds. Dry runs work even for an empty wallet. You get status: preview_only, the full preview and requires.
Use dry runs to show users the outcome of an action before they fund their wallet, and to test an integration.
Test keys start with tnr_sk_test_. They work like normal keys, but every prepare is a dry run, with a TEST_KEY_DRY_RUN warning.
Units and conventions
| Convention | Rule | Example |
|---|---|---|
| Amounts | Whole-number strings in the token's smallest unit | "1000000" is 1 USDC |
| Rates | Annual rates (APR) as decimal strings, in fields ending in _rate | "0.052" is 5.2% |
| Times | Unix seconds. Fields ending in _at are dates, fields ending in _s are durations | matures_at, time_to_maturity_s |
| Chains | chain_id on every request: in the query for a GET, in the body for a POST | 8453 (Base), 1 (Ethereum), 5042 (Arc). GET /v1/chains lists them |
| IDs | Strings, unique on a chain | maturity_id |
| Addresses | Any letter case accepted. Returned checksummed | 0x833589fCD6… |
| Pages | Offers, positions and fills take cursor and limit (default 50, max 200). meta.next_cursor is null on the last page | limit=50 |
Amounts come in two forms. principal is the amount that changes hands now. amount_at_maturity is the amount paid back at maturity. Quotes and previews return both.
API changes only add fields, values and error codes. Ignore fields you don't know, and handle values you don't expect.
Authentication
Send your key in the X-Api-Key header. You don't need a key for reads and quotes. You do for prepares, simulations and readiness checks.
- The Tenor team issues keys for your organization: email contact@tenor.finance.
- Each key has scopes:
readfor private reads, andtradefor preparing transactions. GET /v1/accesschecks a key and returns the key's scopes, mode (liveortest), expiry date and rate limits.- A key sent to a public endpoint identifies you, and an invalid key is rejected there too.
- Keep keys on your servers. Never put one in a browser or an app.
Errors
A successful response looks like { "data": …, "meta": … }. An error looks like { "error": … }:
{
"error": {
"code": "INSUFFICIENT_LIQUIDITY",
"category": "liquidity",
"treatment": "refetch_state",
"message": "The book can't fill the full size: `details.available` is what it can fill now, in the unit of the size you sent.",
"details": { "available": "55307", "requested": "1000000000" },
"request_id": "req_01M6HMZQV115R2PPWSBXW96H4X"
}
}
Check code to handle each error. If you get a code you don't know, use treatment:
treatment | Next step |
|---|---|
fix_request | Change the request. The same request will fail again |
refetch_state | The market or the wallet has changed. Read the latest state, then quote or prepare again |
fund_account | The wallet doesn't have enough of a token, or enough collateral |
retry | A temporary problem. Try again after a short wait |
Every response has an X-Request-Id header. You can send your own to match your logs with ours. Include the request ID when you contact us about a request.
Rate limits
Each request counts against one of three separate limits. A burst of quotes never blocks a cancel. With a key, limits are per key. Without one, they're per IP address.
| Limit | Endpoints | With a key | Without a key |
|---|---|---|---|
read | Reads | 1,200 / 60 s | 25 / 1 s, 150 / 10 s, 1,000 / 60 s |
compute | Quotes, takeable offers, simulations, readiness checks, prepares | 120 / 60 s | 30 / 60 s |
cancel | Offer cancels and curve clears | 600 / 60 s | Key only |
Every response includes X-RateLimit-Policy, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. If you go over a limit, you get 429 RATE_LIMITED with a Retry-After header in seconds. Wait that long before trying again.
Caching
- Data that changes slowly, such as the market list and open offers, comes with a
Cache-Control: max-ageand anETag. Send theETagback inIf-None-Match. If nothing has changed, you get a304with no body. - Quotes, payloads and wallet data are never cached (
no-store). Don't cache them yourself. meta.generated_atis when the data was read, andmeta.block_numberis the block the data was read at.
Live data
Open offers and quotes are streamed every block over a WebSocket at wss://router.tenor.finance/socket, using Phoenix Channels. The topics are orderbook:{maturity_id} and quotes:{maturity_id}. Use the stream to know when to quote again. Always prepare through the API, which checks every transaction before you sign.