Appearance
Authentication
The RFQ endpoints don't need a login. A firm quote is signed to a single taker address and only that wallet can execute it on chain, so you need a wallet, not a credential. That's how the swap page works: connect a wallet, quote, settle. The one signature in the flow — proof of control, on the firm request — proves you hold that wallet; it isn't a sign-in and grants nothing beyond that request.
Two things can identify a caller:
| What it is | What it's for | |
|---|---|---|
| Claim token | Returned once per RFQ by POST /v2/rfq/request | Owning that one RFQ: cancel, submit, status |
| API key | A partner credential we issue | Your own rate-limit budget, webhooks, partner-scoped RFQ ownership |
Proof of control
IMPORTANT
This whole section is for keyless callers. If you send Authorization: Bearer <key>, you do not sign TakerControl at all: leave takerProof out of the request body. Your key already makes you accountable for the taker you name, so the proof is not checked, and one sent anyway is ignored. Preview never needs it either, with or without a key.
POST /v2/rfq/request locks real maker inventory for the taker you name, and we check that wallet holds the sell token (proof of funds). The funds check only means anything if you actually control that wallet — otherwise anyone could name a big public wallet they've never touched. So a firm request carries takerProof: an EIP-712 signature by the taker wallet over a short-lived challenge. Preview never needs it, and neither do partner-key calls (your key already makes you accountable) or Textile admin sessions.
This is deliberately not a session or a SIWE login. A login wouldn't gate anything here — signatures from throwaway wallets are free, so a session proves no scarcity. Control + funds together are what bite: passing both means quoting against a wallet you hold that's actually funded.
Sign this typed data with the taker wallet:
domain: { name: "Textile Taker Control", version: "1", chainId: <chainId> }
type: TakerControl { taker: address, chainId: uint256,
nonce: bytes32, issuedAt: uint256 }
message: taker = the wallet your request names
chainId = the chain your request names
nonce = 32 random bytes
issuedAt = Unix milliseconds nowSend { nonce, issuedAt, signature } as takerProof in the request body.
Sign the proof (TypeScript)
With viem. The taker wallet signs the challenge; send the result as takerProof on POST /v2/rfq/request.
IMPORTANT
Skip this entire block if you hold an API key. It exists so a browser wallet with no credential can prove itself. A keyed integration sends the bearer header and nothing else.
ts
import { randomBytes } from 'node:crypto'
import { createWalletClient, http, type Hex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { bsc } from 'viem/chains'
const chainId = 56
const account = privateKeyToAccount(process.env.TAKER_PRIVATE_KEY as Hex)
const wallet = createWalletClient({ account, chain: bsc, transport: http() })
const nonce = `0x${randomBytes(32).toString('hex')}` as Hex
const issuedAt = Date.now()
const signature = await wallet.signTypedData({
domain: {
name: 'Textile Taker Control',
version: '1',
chainId: BigInt(chainId),
},
types: {
TakerControl: [
{ name: 'taker', type: 'address' },
{ name: 'chainId', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
{ name: 'issuedAt', type: 'uint256' },
],
},
primaryType: 'TakerControl',
message: {
taker: account.address,
chainId: BigInt(chainId),
nonce,
issuedAt: BigInt(issuedAt),
},
})
const takerProof = { nonce, issuedAt, signature }
// POST /v2/rfq/request body: { …, taker: account.address, takerProof }In a browser, call the same signTypedData on your connected wallet client instead of a server-side private key. You only sign once per session: after the first proof verifies, requests for that wallet + chain go out without takerProof for about 12 hours (see One signature per ~12 hours). Inside the 60-second window the identical signed proof is also accepted again from the same caller, so a quick re-quote never needs a second prompt.
Rules:
issuedAtmust be within 60 seconds of our clock, either side.- The nonce is bound to that one signed challenge and to the caller that first sent it (your API key, session, or client IP). Re-sending the identical proof yourself while it's fresh is fine — but a nonce reused with different contents, or the same proof arriving from a different caller, is refused.
- The signature must verify as
takerexactly. EOAs verify by ecrecover; smart accounts (Safe, 4337) verify on-chain via EIP-1271, including counterfactual ones through ERC-6492.
A missing or bad proof is a 401 with a machine-readable details.reason; see Errors.
One signature per ~12 hours
You don't sign every request. A verified proof earns a grant: for about 12 hours, firm requests naming the same taker on the same chain may omit takerProof entirely. The grant is bound to the caller that proved it — your session, or for anonymous callers your client IP — so nobody else can ride it, and proving control of your own wallet never lets anyone name it from elsewhere. (Partner keys skip the proof altogether, so they never hold one.)
Don't track the window client-side. Send the request unsigned; if the grant has lapsed (TTL, an IP change, our cache forgetting) you get 401 proof_of_control_required, which is your cue to sign once and resend. That 401 is answered before any maker is solicited or inventory locked, so the retry costs nothing but the rate-limit slot the refused call used. The signature itself still expires after 60 seconds — the grant is state on our side, never a long-lived credential on the wire.
The claim token
POST /v2/rfq/request returns a claimToken alongside the rfqId. Send it as X-Rfq-Claim on cancel, submit and status for that RFQ:
bash
curl -X POST https://api.textilecredit.com/v2/rfq/rfq_abc.../cancel \
-H "X-Rfq-Claim: rfqc_your_token_here"It comes back exactly once, so store it with the quote. Without an API key it's the only thing that authorizes those calls; a wrong or missing token is a 404. With a key, either works.
The rfqId is not a secret and authorizes nothing. Every maker we solicit receives it in their quote request, losers included, so an id-only rule would let a maker cancel or report someone else's RFQ. The claim token never leaves our side except in that one response, and we store only its hash.
API keys
Send the key as a bearer token:
bash
curl -X POST https://api.textilecredit.com/v2/rfq/request \
-H "Authorization: Bearer tx_live_a1b2c3d4.your_secret_here" \
-H "Content-Type: application/json" \
-d '{ ... }'The X-API-Key: <key> header works too.
A key looks like tx_live_a1b2c3d4.<secret>. The part before the dot is the public prefix, safe to log. The part after is the secret; we only store a hash of it, so we show you the full key exactly once. Lost keys get revoked and reissued, not recovered. Never ship a live secret in client-side code.
What a key buys you:
- A rate-limit budget of your own, keyed to the key rather than to your client IP, and a higher tier if you need one.
- Webhooks: terminal RFQ events delivered to your endpoints.
- Partner-scoped ownership: any key on your partner record can read and manage the RFQs it created, without the claim token.
Maker steering (preferredLiquidityWallets / restrictedLiquidityWallets) works with or without a key. The lists are honoured for any caller so a shared deep link prices the route it names.
Ask us for one. Any live partner key reaches these endpoints.
Maker credentials are the one exception. They look the same (tx_live_…) but are bound to a maker and carry only maker:* scopes, and the taker endpoints refuse them with a 403. They belong on WSS /v2/maker/stream.
Scopes
Each key carries a set of scopes. These are the two that matter here:
| Scope | Grants |
|---|---|
trades:write | POST /v2/rfq/request, /cancel, /submit |
trades:read | GET /v2/rfq/{id} |
A key missing the scope for an endpoint gets 403 forbidden. Scopes only apply to key-authenticated calls. An anonymous caller holding a claim token isn't scoped.
Test vs live
Keys are environment-scoped and the environment is baked into the prefix: tx_live_… for mainnet corridors, tx_test_… for testnet. A test key against a mainnet chain (or the reverse) is a 400.
The BSC testnet cNGN ↔ USDT corridor is the one to integrate against. See Testnet (BSC). A corridor still has to be enabled there with a maker online, so if you get no_makers_online on testnet, tell us rather than assuming your request is wrong.
No credential at all
GET /v2/health and GET /v2/openapi.json are fully public, so monitors and codegen can reach them.