API and MCP
The Sway API is a JSON REST API. The brand console, the integrator console, the operator page and the @sway/mcp package all use it, and you can call it directly.
| Address | |
|---|---|
| Base URL | https://api.swayoffers.com |
| OpenAPI 3.1 document | https://api.swayoffers.com/openapi.json |
| Protocol parameters | https://api.swayoffers.com/v1/protocol |
| MCP tool definitions | https://api.swayoffers.com/v1/mcp/tools |
| Encryption and chain parameters | https://api.swayoffers.com/v1/encryption |
| Health | https://api.swayoffers.com/health |
The OpenAPI document is generated from the same schemas the server validates against, and it marks each operation's role with x-sway-auth. These addresses serve the API's documents and parameters; the endpoint tables below list its operations.
Conventions
- Request and response bodies are JSON. A request body may be at most 16,384 bytes, and every object rejects fields it does not declare.
- Money is a decimal string of USDC base units:
"15000000"is 15 USDC. It is never a floating-point number. - Public keys and wallet addresses are base58. Signatures, ciphertext and 128-byte messages are standard padded base64. Hashes are 64 lowercase hex characters.
- IDs are opaque strings of letters, digits,
_and-. - Lists take
cursorandlimit(1 to 100) and return{ items, nextCursor, sync }, wheresync.statusisready,indexingorfailed. - Browsers can call the API only from the origins the deployment allows. Server-side callers send no
Originheader. - Every response carries
Cache-Control: no-storeandX-Sway-Mode.
Response envelope
A success wraps the result in data:
{
"data": { "id": "…", "status": "confirmed" },
"meta": { "mode": "live", "scenario": "default", "requestId": "…" }
}
A failure carries error instead, with the HTTP status set to match:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request does not match the contract.",
"retryable": false,
"details": { "fields": [{ "field": "amount", "message": "This field has an invalid format." }] }
},
"meta": { "mode": "live", "scenario": "default", "requestId": "…" }
}
details may also hold retryAfterSeconds, requiredConfiguration or resourceId. A 429 response also sets Retry-After.
| Code | Meaning |
|---|---|
VALIDATION_ERROR | The request does not match the schema or a domain rule |
UNAUTHORIZED, WRONG_WALLET, FORBIDDEN | Missing or invalid credentials, the wrong wallet, or a missing role, scope or approval |
INVALID_SIGNATURE, CHALLENGE_EXPIRED, REPLAY_DETECTED | A signature does not verify, has expired, or was already used |
IDEMPOTENCY_REQUIRED, IDEMPOTENCY_CONFLICT | The Idempotency-Key header is missing, or was used for a different request |
NOT_FOUND, RATE_LIMITED | No such resource for this caller; too many requests |
BOND_LOCKED | The merchant bond cannot be released yet: the merchant is not suspended, a receipt awaits verification, or the last lock plus the dispute window has not passed |
INSUFFICIENT_FUNDS, CAP_EXCEEDED | Not enough unreserved funds; a protocol or campaign cap would be exceeded |
REFUND_WINDOW_CLOSED, DISPUTE_WINDOW_CLOSED | The conversion has unlocked; the dispute window has passed |
CLAIM_LOCKED, ALREADY_CLAIMED | The conversion is still in its lock; it was already paid |
TRANSACTION_SIMULATION_FAILED, TRANSACTION_FAILED | A signed transaction could not execute, or failed on chain |
COMPUTATION_FAILED | An MPC computation did not complete |
CONFIGURATION_REQUIRED, DEPENDENCY_UNAVAILABLE, PROGRAM_NOT_DEPLOYED | The service is missing a setting or a dependency for this operation |
ADDRESS_BLOCKED, ORIGIN_NOT_ALLOWED, INTERNAL_ERROR | The address is refused; the browser origin is not allowed; an unexpected server error |
Authentication
Each operation belongs to one role. The role decides how a request proves who sent it.
| Role | Proof |
|---|---|
| public | None |
| merchant | Authorization: Bearer <session token> from a wallet sign-in |
| integrator | A signed SwayIntegrator request header from the integrator authority: the wallet in the integrator console, or a key on a machine |
| agent | A signed SwayAgent request header |
| claim | A one-use challenge signed by the intent's claim key, inside the body |
| receipt | The merchant's receipt signature over a 128-byte message, inside the body. Anyone may relay it |
| admin | A signed SwayAdmin request header from an operator key the deployment allows |
| oracle | A signed SwayOracle request header from the affiliate oracle key the deployment allows |
One-use challenges
Sign-in, registrations, claims and disputes use challenges. Request one with POST /v1/auth/challenges:
{ "purpose": "claim", "publicKey": "<signer>", "resourceIds": ["<claimable id>"], "requestHash": "<sha256 hex>" }
The response holds id, purpose, publicKey, expiresAt and the exact message to sign:
Sway authorization v1
Audience: <audience origin>
Purpose: <purpose>
Public-Key: <signer public key>
Nonce: <43 random base64url characters>
Issued-At: <ISO 8601 time>
Expires-At: <ISO 8601 time>
Request-SHA256: <requestHash>
Resources-SHA256: <SHA-256 of the canonical JSON of the sorted resource IDs>
Sign the UTF-8 bytes of message with Ed25519 and send the proof as the body field authorization:
{ "challengeId": "<id>", "publicKey": "<signer>", "signatureBase64": "<64-byte signature, base64>" }
A proof works for one operation, one payload and one set of resources, once, until expiresAt. requestHash is the SHA-256 of the canonical JSON of the request body without its authorization field, except for sign-in.
| Purpose | Operation | Signer | resourceIds |
|---|---|---|---|
merchant_session | POST /v1/auth/merchant/session | Merchant wallet, or the Config admin wallet | []. requestHash hashes {"action":"merchant_session","publicKey":"<wallet>"} |
integrator_registration | POST /v1/integrators | Integrator authority | [] |
agent_registration | POST /v1/integrator/agent-keys | Integrator authority | [<integrator id>] |
claim_read | POST /v1/claims/query | Claim key | [] |
claim_read | POST /v1/claims/{id}/query | Claim key | [<claim id>] |
claim_read | POST /v1/disputes/{id}/query | Claim key | [<dispute id>] |
claim | POST /v1/claims | Claim key | [<claimable id>] |
open_dispute | POST /v1/disputes | Claim key | [<intent id>] |
Merchant session
- Request a
merchant_sessionchallenge for the wallet and sign its message with the wallet. - Send
POST /v1/auth/merchant/sessionwith anIdempotency-Keyand{ "authorization": <proof> }. - The response carries
accessToken, a 43-character token, andexpiresAt. Send it asAuthorization: Bearer <accessToken>.
GET /v1/auth/merchant/session reads the session and DELETE /v1/auth/merchant/session ends it. The Config admin wallet signs in the same way, without registering a merchant, and uses its session to submit the activation transactions it signs.
Signed requests: SwayAgent, SwayIntegrator, SwayAdmin and SwayOracle
Agents, integrators, the operator and the affiliate oracle sign each HTTP request. The header is:
Authorization: <Scheme> <keyId>:<timestamp>:<nonce>:<signatureBase64>
| Part | Value |
|---|---|
Scheme | SwayAgent, SwayIntegrator, SwayAdmin or SwayOracle |
keyId | The agent key's id for SwayAgent; the integrator's id for SwayIntegrator; the signer's base58 public key for SwayAdmin and SwayOracle |
timestamp | Unix seconds. It must be within the deployment's maximum request age of the server's clock |
nonce | 16 to 128 characters from A–Z a–z 0–9 _ -, used once per key |
signatureBase64 | Ed25519 signature of the message below, 64 bytes, standard base64 |
The signed message is these nine lines, joined with \n:
Sway signed request v1
Audience: <audience>
Principal: <Scheme>
Key: <keyId>
Timestamp: <timestamp>
Nonce: <nonce>
Method: <HTTP method in upper case>
Path: <path and query string exactly as sent>
Body-SHA256: <SHA-256 hex of the canonical JSON body>
Audience is the value GET /v1/encryption returns as authorizationAudience. For a request without a body, such as a GET, the body hash is the hash of {}. The agent key signs SwayAgent requests, the integrator authority signs SwayIntegrator requests, and the operator's and oracle's allowed keys sign SwayAdmin and SwayOracle requests. A browser wallet signs the message's UTF-8 bytes as a message signature. Sign a fresh request, with a new nonce, for every attempt, including retries.
Canonical JSON
Hashes cover canonical JSON. Object keys are sorted by UTF-16 code unit, with no whitespace anywhere. Strings, finite numbers, booleans and null are written as standard JSON, and arrays keep their order. {"b":1,"a":[true,null]} becomes {"a":[true,null],"b":1}.
Idempotency keys
Every mutation marked in the tables below requires an Idempotency-Key header of 1 to 128 characters. The key is scoped to the caller and the operation. Repeating a request with the same key returns the original result. Reusing the key for a different request returns 409 IDEMPOTENCY_CONFLICT, and leaving it out returns 400 IDEMPOTENCY_REQUIRED. Use a fresh key for each new action and the same key for each retry of it.
Wallet transactions
Merchant mutations that change chain state, and the operator's merchant activation, suspension, bond release and shopper-side dispute resolution, return a prepared transaction: { id, kind, resourceId, requiredSigner, status, unsignedTransactionBase64, signature, expiresAt, failure, simulationOnly }. Legacy and v0 transactions are both accepted.
- Sign
unsignedTransactionBase64with the wallet named inrequiredSigner. The prepared instructions, their order, the blockhash, the fee payer and the signer set stay unchanged. The wallet may add compute-budget instructions before them and a Lighthouse guard after them, and nothing else; no account may become newly writable. - Send it one of two ways, with the wallet session of the signer:
- Through Sway:
POST /v1/transactions/{id}/submitwith{ "signedTransactionBase64": "…" }. Sway simulates the transaction first and answersTRANSACTION_SIMULATION_FAILEDif it would not execute; prepare a fresh one after correcting balances or state. - From the wallet: when the wallet broadcasts it itself, as with
solana:signAndSendTransaction, report it withPOST /v1/transactions/{id}/reportand{ "signature": "<base58 transaction signature>" }. Sway fetches the transaction, applies the same checks to its instructions and signers, and tracks it without sending it again. Retry with the same signature while the RPC has not yet indexed it.
- Through Sway:
- Poll
GET /v1/transactions/{id}untilstatusisconfirmed,failedorexpired.
Endpoints
The "Key" column marks operations that require Idempotency-Key.
Public
| Method | Path | Purpose | Key |
|---|---|---|---|
| GET | /v1/encryption | MXE public key, authorizationAudience, chain clock, purchaseWindowSeconds, settlementGraceSeconds, allowedShards | |
| POST | /v1/early-access | Request early access as a brand or agent developer | |
| POST | /v1/auth/challenges | Request a one-use challenge | |
| POST | /v1/auth/merchant/session | Exchange a signed merchant_session challenge for a session | Yes |
| POST | /v1/integrators | Register an integrator: name, authority public key, payout address, authority proof | Yes |
| GET | /v1/cashback/rates | Public cashback rates, filterable by shard | |
| GET | /v1/protocol/buyback | {swayMint, treasuryBalance, totalUsdcSpent, totalSwayBurned}: the SWAY mint, the treasury's finalized USDC balance, and the USDC spent and SWAY burned by finalized buybacks |
Merchant (brand session)
| Method | Path | Purpose | Key |
|---|---|---|---|
| GET | /v1/auth/merchant/session | Read the current session and wallet | |
| DELETE | /v1/auth/merchant/session | End the session | Yes |
| GET | /v1/merchant | Merchant profile, receipt signers, bond and status | |
| POST | /v1/merchant | Prepare register_merchant: name, receipt signer, return window | Yes |
| POST | /v1/merchant/receipt-signers | Prepare rotate_receipt_signer with a new Ed25519 key | Yes |
| POST | /v1/merchant/bond | Prepare a USDC bond deposit | Yes |
| GET | /v1/campaigns | List campaigns | |
| POST | /v1/campaigns | Prepare create_campaign: name, cap per conversion, lock days, daily cap | Yes |
| GET | /v1/campaigns/{id} | Campaign with vault balance, pending liability and withdrawable balance | |
| POST | /v1/campaigns/{id}/fund | Prepare a USDC deposit into the vault | Yes |
| POST | /v1/campaigns/{id}/withdraw | Prepare a withdrawal of unreserved USDC | Yes |
| GET | /v1/campaigns/{id}/spend | Daily rebate and fee debits, inclusive UTC from and to | |
| GET | /v1/offers | List offers, filterable by campaignId | |
| POST | /v1/offers | Submit a browser-encrypted offer; returns the offer and a submit_offer transaction | Yes |
| GET | /v1/offers/{id} | One offer's slot and ingest state | |
| GET | /v1/conversions | List conversions, filterable by campaignId and status | |
| GET | /v1/conversions/{id} | One conversion: amounts, fees, unlock time, refund availability | |
| POST | /v1/transactions/{id}/submit | Simulate and send a wallet-signed prepared transaction: { signedTransactionBase64 } | Yes |
| POST | /v1/transactions/{id}/report | Report a prepared transaction the wallet sent itself: { signature }, base58 | Yes |
| GET | /v1/transactions/{id} | Track a prepared transaction |
The three transaction routes also take the Config admin wallet's session, for the activation transactions it signs.
Receipt signer
| Method | Path | Purpose | Key |
|---|---|---|---|
| POST | /v1/receipts | Relay a 128-byte receipt signed by the merchant's receipt signer, with the proposed rebate | Yes |
| POST | /v1/conversions/{id}/refund | Relay a signed refund notice while the conversion is locked | Yes |
A receipt body carries merchantId, signerPublicKey, payloadBase64 (the 128 signed receipt bytes), signatureBase64 and proposedRebateUsdc, the merchant's proposal of the rebate in base units. Arcium's verification checks that the proposal equals the sealed winning offer before the program records or escrows anything. The response is the conversion, at verification_pending; poll GET /v1/conversions/{id} until it is locked. A rejected proposal moves nothing: send the same signed receipt again with the corrected proposal and a new Idempotency-Key.
Integrator (SwayIntegrator)
| Method | Path | Purpose | Key |
|---|---|---|---|
| GET | /v1/integrator | The integrator and its status: pending, active or revoked. Readable while pending | |
| GET | /v1/integrator/agent-keys | List agent keys | |
| POST | /v1/integrator/agent-keys | Register an agent public key with a label and scopes from intent:write, intent:read, purchase:report and cashback:link, plus an agent_registration proof. Requires an active integrator | Yes |
| DELETE | /v1/integrator/agent-keys/{id} | Revoke an agent key | Yes |
Agent (SwayAgent)
| Method | Path | Purpose | Scope | Key |
|---|---|---|---|---|
| POST | /v1/intents | Post an encrypted intent with its claim and x25519 public keys | intent:write | Yes |
| GET | /v1/intents/{id} | Poll an intent; a matched one carries the sealed top three | intent:read | |
| GET | /v1/intents/{id}/book | The public campaign and brand map captured for the intent | intent:read | |
| POST | /v1/intents/{id}/retry | The one retry of an expired computation | intent:write | Yes |
| POST | /v1/intents/{id}/voucher | Issue the checkout voucher for a chosen slot | intent:write | Yes |
| POST | /v1/purchases | Report order and evidence hashes | purchase:report | Yes |
| POST | /v1/cashback/tracking-links | Create an affiliate link with the intent ID as sub-ID. Answers CONFIGURATION_REQUIRED until the deployment connects an affiliate network | cashback:link | Yes |
Claim key
| Method | Path | Purpose | Key |
|---|---|---|---|
| POST | /v1/claims/query | Amounts, unlock times and states for a claim key | |
| POST | /v1/claims | Claim one unlocked conversion to a chosen destination | Yes |
| POST | /v1/claims/{id}/query | Read a claim's relay and transaction status | |
| POST | /v1/disputes | Open a dispute for an intent slot | Yes |
| POST | /v1/disputes/{id}/query | Read a dispute |
A claim body carries claimPublicKey, one ID in claimableIds, destination and the challenge proof. It also carries claimMessageBase64, a 128-byte SWAYCLM1 message that binds the program, the conversion account, the destination wallet, an expiry and a nonce, and claimSignatureBase64, the claim key's signature over it. The program checks that signature before it pays.
Operator (SwayAdmin)
The Sway operator signs each of these with a SwayAdmin request from a key the deployment allows. The operator page at https://swayoffers.com/admin calls them with the Config admin wallet.
| Method | Path | Purpose | Key |
|---|---|---|---|
| GET | /v1/admin/merchants/pending | Merchants waiting for activation, oldest first | |
| GET | /v1/admin/merchants?status=active|suspended | Active or suspended merchants; omit status for both | |
| GET | /v1/admin/integrators/pending | Integrators waiting for approval, oldest first | |
| GET | /v1/admin/disputes | Open and under-review disputes, each with its brand, opening time, evidence hash and compensation address | |
| POST | /v1/admin/merchants/{id}/activate | Prepare activate_merchant for the Config admin wallet to sign. Body {} | Yes |
| POST | /v1/admin/merchants/{id}/suspend | Prepare suspend_merchant for the Config admin wallet to sign. Body {} | Yes |
| POST | /v1/admin/merchants/{id}/bond/release | Prepare release_bond for the Config admin wallet to sign. Body {} | Yes |
| POST | /v1/admin/integrators/{id}/approve | Approve an integrator. Body {}; returns the integrator as active | Yes |
| POST | /v1/admin/disputes/{id}/resolve | Resolve a dispute. Body {outcome, bondDebit, resolutionHash}; returns the dispute and, for a user outcome, a prepared slash_bond transaction | Yes |
Activation, suspension and bond release are prepared only for the program's Config admin and only once the merchant's registration is final on chain. Each returns a prepared transaction: the admin wallet signs it and sends it with POST /v1/transactions/{id}/submit or /report under its own wallet session. The program activates the merchant when its bond holds at least minimum_bond. A bond release takes no amount or destination: Sway computes the whole bond balance and pays it to the merchant wallet's own USDC account, and answers BOND_LOCKED while the program would refuse the release. Approval changes the integrator's status from pending to active at once, and its authority can then register agent keys.
A resolve body carries outcome, one of user, merchant or reject; bondDebit, in USDC base units, positive and at most 50 USDC for user and "0" otherwise; and resolutionHash, 64 lowercase hex characters. The operator page sets resolutionHash to the SHA-256 of the operator's resolution note. A user outcome leaves the dispute under_review and returns transaction, a prepared slash_bond that pays the compensation address from the bond. The admin wallet signs and sends it like the transactions above, and the dispute becomes resolved_user once that debit is final on chain. merchant and reject are recorded by the signed request alone.
Affiliate oracle (SwayOracle)
These routes answer CONFIGURATION_REQUIRED until the deployment connects an affiliate network.
| Method | Path | Purpose | Key |
|---|---|---|---|
| POST | /v1/affiliate/postbacks | Ingest a signed affiliate commission event | Yes |
| GET | /v1/affiliate/events | List commission events | |
| POST | /v1/oracle/batches | Post a batch of cashback conversions | Yes |
| GET | /v1/oracle/batches/{id} | Track a batch |
MCP tools
GET https://api.swayoffers.com/v1/mcp/tools returns the five tools of @sway/mcp, each as { name, description, inputSchema, outputSchema, operationId }, with followUpOperationId where the tool polls. These are the schemas the package validates against. The package performs the encryption, key storage and signing, then calls the REST operations listed here.
| Tool | REST operation | Follow-up |
|---|---|---|
find_offers | postIntent, POST /v1/intents | getIntent |
get_voucher | getVoucher, POST /v1/intents/{id}/voucher | |
report_purchase | reportPurchase, POST /v1/purchases | |
claimable | listClaimables, POST /v1/claims/query | |
claim | claim, POST /v1/claims | getClaim |
Installation, inputs and outputs are in For agent developers.