Docs
REST API & API keys
Self-host the Noir API gateway: API keys, scopes, rate limits, endpoints, non-custodial transactions and errors.
The Noir API is a small, self-hostable gateway in front of the contracts, for apps that aren’t written in JavaScript or that want API keys, rate limits and managed webhooks. It is non-custodial: it never holds a secret key. Writes are prepared by the API, signed by your user’s wallet or your agent key, then submitted.
Run it
cd sdk/noir-api
npm run setup && npm run build
npm start # PORT=8787, Noir Testnet by defaultConfiguration is environment variables (.env.example lists them): CORS origins, default rate limit, webhook storage, and your own RPC or contract IDs.
API keys
npm run keygen -- my-shop --scopes read,write,webhooks --rate 300
# Created key "my-shop" (id k3j9x0ab, scopes read,write,webhooks) in ./keys.json.
# noir_sk_k3j9x0ab_4yQ… ← shown once; only its SHA-256 hash is storedSend it as Authorization: Bearer noir_sk_… (or X-API-Key). Keys carry scopes:
| Scope | Allows |
|---|---|
read | Network, devices, policies, balances, payment checks, events |
write | transactions/prepare and transactions/submit |
webhooks | Create, list and delete webhook subscriptions |
Each key has a token-bucket rate limit (default 120/min). Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; over the limit you get 429 with Retry-After. Revoke a key by setting "disabled": true on its record.
Endpoints
Amounts are decimal strings in the asset’s smallest unit (1 XLM = "10000000"). Floats are rejected.
| Method | Path | Scope | Returns |
|---|---|---|---|
| GET | /v1/health | — | { ok: true } |
| GET | /v1/network | read | RPC URL, passphrase, contract IDs |
| GET | /v1/devices/:hash | read | { deviceHash, owner, policy, escrowBalance } |
| GET | /v1/devices/:hash/policy | read | { agent, maxAmount, asset, expiresAt } |
| GET | /v1/wallets/:address/devices | read | { devices: [hash…] } |
| GET | /v1/merchants/:address/pending | read | { pending } |
| POST | /v1/payments/check | read | { ok } or { ok: false, reason } |
| GET | /v1/events?fromLedger=&cursor=&limit= | read | { events, cursor, latestLedger } |
| POST | /v1/transactions/prepare | write | { xdr, networkPassphrase, operation } |
| POST | /v1/transactions/submit | write | { hash } |
| POST | /v1/webhooks | webhooks | subscription + secret (shown once) |
| GET | /v1/webhooks | webhooks | this key’s subscriptions |
| DELETE | /v1/webhooks/:id | webhooks | { deleted: true } |
Look up a card
curl $NOIR_API/v1/devices/$DEVICE_HASH -H "Authorization: Bearer $NOIR_KEY"{
"deviceHash": "ab12…",
"owner": "GBX7…Q4MZ",
"policy": { "agent": "GDK2…", "maxAmount": "50000000", "asset": "CDLZ…", "expiresAt": null },
"escrowBalance": "250000000"
}Transactions: prepare → sign → submit
source is the acting account: the agent for authorize, the merchant for claim, the card owner for registerAgent, revokeAgent, fundEscrow and defundEscrow.
curl -X POST $NOIR_API/v1/transactions/prepare -H "Authorization: Bearer $NOIR_KEY" \
-H "Content-Type: application/json" -d '{
"source": "GDK2…agent",
"operation": {
"kind": "authorize", "deviceHash": "ab12…", "merchant": "GMER…",
"asset": "CDLZ…", "amount": "10000000"
}
}'
# → { "xdr": "AAAAAgAAAAC…", "networkPassphrase": "Test SDF Network ; September 2015" }Sign the XDR with the source account (Freighter, Wallets Kit, any Stellar SDK), then:
curl -X POST $NOIR_API/v1/transactions/submit -H "Authorization: Bearer $NOIR_KEY" \
-H "Content-Type: application/json" -d '{ "xdr": "<signed envelope>" }'
# → { "hash": "9f1c…" } (returned once the network confirms)from stellar_sdk import Keypair, TransactionEnvelope
import requests
H = {"Authorization": f"Bearer {NOIR_KEY}"}
prep = requests.post(f"{API}/v1/transactions/prepare", headers=H, json={"source": agent.public_key, "operation": op}).json()
env = TransactionEnvelope.from_xdr(prep["xdr"], prep["networkPassphrase"])
env.sign(agent)
print(requests.post(f"{API}/v1/transactions/submit", headers=H, json={"xdr": env.to_xdr()}).json())Errors
{ "error": { "code": "contract_refused", "contractError": "DuplicateAuth", "message": "paymentEscrow.authorize failed: DuplicateAuth (#6)" } }| Status | code | When |
|---|---|---|
| 400 | invalid_request | Bad hash, address, amount or body |
| 401 | unauthorized | Missing, malformed, unknown or disabled key |
| 403 | insufficient_scope | Key lacks the endpoint’s scope |
| 404 | not_found · device_not_found · agent_not_found | |
| 422 | contract_refused | Includes contractError, e.g. DuplicateAuth |
| 429 | rate_limited | See Retry-After |
| 502 | upstream_error | Soroban RPC failed |
Production notes
- Run behind TLS; set
NOIR_API_CORS_ORIGINSonly if browsers call it directly (prefer calling from your server so the key stays secret). - Keys, rate-limit buckets and subscriptions are single-process. For several replicas, move them to a shared store.
- Webhook targets must be public
https://URLs — loopback and private networks are refused. See Webhooks.