Satsgate API reference
REST over HTTPS, JSON in and out. The checkout API and webhooks are Satsgate interfaces. The x402 examples follow the submitted native Bitcoin proposal, which remains under review.
- Base URL
- https://api.satsgate.io/v1
- API version
- 2026-09-16
- Protocol
- x402 v2 · exact · bip122 · onchain
- Identifiers
- co_ checkout, evt_ event, sk_ secret key
Overview
A checkout binds an order to a fixed satoshi amount and a fresh Bitcoin address derived from your extended public key. For x402, issue those terms with a private payment identifier and deadlines. The payer signs and broadcasts. Your server checks the selected payment outputs and records their use before running the purchased operation.
Settlement can run on your server or through a provider. A separate facilitator is optional. A hosted checkout and its webhooks are Satsgate product interfaces; x402 defines the payment messages and acceptance rules, without requiring either interface.
Payments go to your wallet. Satsgate needs your public wallet information to derive addresses, never your spending keys. All addresses, identifiers and dates on this page are examples. The Bitcoin address is a public test vector: never send funds to it.
Authentication
Pass a secret key as a bearer token on every request. Keys carry their mode, so a test key cannot reach mainnet and a live key never returns simulated data.
curl https://api.satsgate.io/v1/checkouts/co_3PmQ2r8kLxN1 \
-H "Authorization: Bearer sk_test_4eC39Hq"sk_test_…string- Test mode. Runs against the simulated chain.
sk_live_…string- Live mode. Real Bitcoin, real addresses.
Keys belong on your server. A key in browser or agent code can read every checkout on the account.
Quickstart
1. Create a checkout
This request prices the order at USD 42.00. The example quote fixes that price at 412,900 satoshis.
curl https://api.satsgate.io/v1/checkouts \
-H "Authorization: Bearer sk_test_4eC39Hq" \
-H "Idempotency-Key: order_4821" \
-d amount=4200 \
-d currency=usd \
-d reference=order_4821 \
-d min_confirmations=1 \
-d expires_in=36002. Issue the payment requirements
Bind the requirements to the requested operation and keep the payment identifier private. Over HTTP, return 402 with the complete base64-encoded PaymentRequired in PAYMENT-REQUIRED. The body does not replace that header. The client must support this proposed Bitcoin method and its issued policy.
HTTP/1.1 402 Payment Required
Content-Type: application/json
PAYMENT-REQUIRED: <base64-encoded PaymentRequired JSON>
{}{
"x402Version": 2,
"resource": {
"url": "https://example.com/report",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "bip122:000000000019d6689c085ae165831e93",
"amount": "412900",
"asset": "BTC",
"payTo": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
"maxTimeoutSeconds": 3600,
"extra": {
"assetTransferMethod": "onchain",
"paymentFlow": "upfront",
"paymentId": "7b9e81c023d46fa598e6d304ca1f7260ef083cb519ad47e26f953a80d642c1bb",
"issuedAt": 1790000000,
"expiresAt": 1790003600,
"recoveryUntil": 1790090000,
"minConfirmations": 1,
"maxPaymentOutputs": 8,
"latePaymentPolicy": "no-automatic-refund"
}
}
]
}3. Broadcast and submit the proof
The payer validates and saves the requirements, then signs and broadcasts. Mining fees are additional to the amount paid to the recipient. Retry the original operation with the complete base64-encoded PaymentPayload in PAYMENT-SIGNATURE. Its accepted entry must match the original requirements, and its proof lists the selected outputs by transaction ID and output index. Sort the unique references by transaction ID, then output index, within the issued output limit.
GET /report HTTP/1.1
Host: example.com
PAYMENT-SIGNATURE: <base64-encoded PaymentPayload JSON>Decoded PAYMENT-SIGNATURE
{
"x402Version": 2,
"resource": {
"url": "https://example.com/report",
"mimeType": "application/json"
},
"accepted": {
"scheme": "exact",
"network": "bip122:000000000019d6689c085ae165831e93",
"amount": "412900",
"asset": "BTC",
"payTo": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
"maxTimeoutSeconds": 3600,
"extra": {
"assetTransferMethod": "onchain",
"paymentFlow": "upfront",
"paymentId": "7b9e81c023d46fa598e6d304ca1f7260ef083cb519ad47e26f953a80d642c1bb",
"issuedAt": 1790000000,
"expiresAt": 1790003600,
"recoveryUntil": 1790090000,
"minConfirmations": 1,
"maxPaymentOutputs": 8,
"latePaymentPolicy": "no-automatic-refund"
}
},
"payload": {
"outputs": [
{
"txid": "1111111111111111111111111111111111111111111111111111111111111111",
"vout": 0
}
]
}
}4. Settle before running the operation
Authenticate the original requirements and operation. Check that the selected outputs pay the issued address, total exactly the requested amount and all meet the confirmation policy in one consistent Bitcoin view. A pending or failed attempt consumes nothing. Once the checks pass, atomically record every selected output and the payment identifier as used, together with the operation record, before the expiry deadline. Only then run the operation.
A payment monitor can construct the proof and make the same commitment before a client retries. A webhook or address balance alone cannot authorise access. Retries must recover the bound operation; use application idempotency for external side effects.
HTTP/1.1 200 OK
Content-Type: application/json
PAYMENT-RESPONSE: <base64-encoded SettleResponse JSON>
{"report": "The purchased result"}Payment policy
amountsatoshis- A positive decimal string. Selected outputs must total exactly this amount. Fees are additional; exchange rates cannot change an issued quote.
minConfirmationsinteger- The merchant chooses the minimum depth, including zero if supported. Zero confirmations carries replacement and double-spend risk; rejecting RBF signalling does not remove that risk.
maxPaymentOutputsinteger- The maximum number of selected outputs. Set 1 for a single output; a larger bound permits exact totals across several outputs or top-ups.
issuedAt / expiresAtUnix seconds- The first durable commitment must happen strictly before expiresAt, which equals issuedAt + maxTimeoutSeconds. Broadcast or observation before expiry is insufficient.
recoveryUntilUnix seconds- An existing commitment remains recoverable through this deadline. It does not extend the window for first acceptance.
latePaymentPolicyenum- no-automatic-refund. An unaccepted payment at expiry has no automatic resource entitlement or refund under this method.
For an underpayment, reconcile the existing transactions before sending more. A top-up must complete the original satoshi amount within the output limit and deadline. Submit a new complete proof set; every output must meet the policy. Partial funds do not carry over into a new quote, and an excessive selected total is rejected. Refunds require a separate agreement and merchant-authorised transaction.
Recovery and receipts
After commitment, an authorised retry for the same original requirements and operation recovers the existing purchase through recoveryUntil, even after payment expiry. The response uses invalid_exact_bip122_already_settled and returns the original receipt. It consumes no new output and authorises no second purchase. Current confirmations or a temporary node outage do not undo the recorded commitment.
On first success and authenticated recovery, the PAYMENT-RESPONSE header includes the complete committed output set in bip122-payment-set. This response-only extension is not advertised in the payment requirements or echoed in the payload. It always describes the stored set, even if a retry submits different outputs. The core transaction field contains the first transaction ID in canonical order.
Decoded success receipt
{
"success": true,
"transaction": "1111111111111111111111111111111111111111111111111111111111111111",
"network": "bip122:000000000019d6689c085ae165831e93",
"extensions": {
"bip122-payment-set": {
"info": {
"version": 1,
"outputs": [
{
"txid": "1111111111111111111111111111111111111111111111111111111111111111",
"vout": 0
}
]
},
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": [
"version",
"outputs"
],
"additionalProperties": false,
"properties": {
"version": {
"type": "integer",
"const": 1
},
"outputs": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "object",
"required": [
"txid",
"vout"
],
"additionalProperties": false,
"properties": {
"txid": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"vout": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
}
}
}
}
}
}
}
}Keep the returned output set with the original requirements and check the response network. The receipt records payment acceptance. It does not certify service delivery or irreversible confirmation. Keep payment identifiers out of public logs and links; public transaction details do not establish who may recover an operation.
Test mode
Test mode runs a simulated chain you control. Commands act on one checkout and fire the same webhooks as live mode. They are Satsgate interfaces, separate from the x402 protocol.
satsgate listen --forward-to localhost:3000/webhooks/satsgate
satsgate test pay co_3PmQ2r8kLxN1
satsgate test advance co_3PmQ2r8kLxN1 --confirmations 1| Command | Effect |
|---|---|
| satsgate test pay <id> | Puts a transaction in the mempool at 0 confirmations. |
| satsgate test advance <id> -c <n> | Confirms to depth n. |
| satsgate test revert <id> --rbf | Replaces a seen transaction. |
| satsgate test underpay <id> --sat <n> | Pays less than the amount due. |
| satsgate test expire <id> | Passes the deadline for a first commitment. |
Checkouts
These snake_case fields belong to the Satsgate API. The x402 wire messages use the camelCase fields shown above. An address total is useful for monitoring, but settlement accounts for the selected output set and its durable commitment.
The checkout object
{
"id": "co_3PmQ2r8kLxN1",
"object": "checkout",
"status": "settled",
"amount": 4200,
"currency": "usd",
"amount_sat": 412900,
"amount_received_sat": 412900,
"address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
"confirmations": 1,
"min_confirmations": 1,
"txid": "1111111111111111111111111111111111111111111111111111111111111111",
"reference": "order_4821",
"rate_locked_until": "2026-09-21T15:13:20Z",
"expires_at": "2026-09-21T15:13:20Z",
"hosted_url": "https://pay.satsgate.io/co_3PmQ2r8kLxN1",
"created": 1790000000,
"livemode": false
}Selected fields
statusenum- See the status table below.
amountinteger- Charged amount in the smallest unit of currency.
amount_satinteger- Satoshi amount, fixed at the locked rate.
amount_received_satinteger- Observed total at the address. This is not the accepted output set or proof of settlement.
addressstring- Derived from your xpub for this checkout alone.
confirmationsinteger- Depth of the transaction in this single-payment example. Combined proofs require a check of every selected output.
min_confirmationsinteger- Minimum depth required before commitment. Depth alone does not establish settlement.
hosted_urlstring- Checkout page for a human. Answers 402 to Accept: application/json.
livemodeboolean- false for objects created with a test key.
| status | Meaning |
|---|---|
| pending | Created. No transaction has been seen at the address. |
| seen | A transaction is in the mempool at 0 confirmations. |
| settled | A qualifying output set and its payment identifier were committed before expiry. |
| underpaid | The selected outputs total less than amount_sat. A top-up may complete the same quote before expiry. |
| reverted | A seen transaction was replaced or dropped before it settled. |
| expired | The deadline passed without a settlement commitment. |
Create a checkout
POST/v1/checkouts
Parameters
amountintegerrequired- In the smallest unit of currency.
currencystringrequired- Three-letter ISO code, or sat to price in satoshis.
referencestring- Your own order identifier. Returned on every event.
min_confirmationsinteger- 0 to 12. Defaults to 1.
expires_ininteger- Seconds until the rate lock ends. 60 to 3600, defaults to 900.
metadataobject- Up to 20 key-value pairs, returned on the object and its events.
Send Idempotency-Key on every POST. Retrying with the same key returns the original checkout rather than deriving a second address.
Map the checkout lifetime to the x402 completion deadline explicitly. Choose a window that allows for the required confirmations, and keep the issued satoshi amount fixed throughout it. Neither an HTTP retry nor a pending payment extends that deadline. Fee estimates and average block times cannot guarantee acceptance before expiry.
Retrieve a checkout
GET/v1/checkouts/:id
Returns the checkout in its current state. Everything a webhook reports is readable here, so a missed delivery never loses you the fact.
List checkouts
GET/v1/checkouts
Parameters
limitinteger- 1 to 100. Defaults to 10.
statusenum- Filter to one status.
referencestring- Exact match on your own identifier.
starting_afterstring- Cursor. A checkout id from the previous page.
curl -G https://api.satsgate.io/v1/checkouts \
-H "Authorization: Bearer sk_test_4eC39Hq" \
-d limit=20 \
-d status=settled \
-d starting_after=co_3PmQ2r8kLxN1Webhooks
Verify a signature
Every delivery carries a Satsgate-Signature header: a timestamp and an HMAC-SHA256 of {t}.{raw body} under your endpoint secret. Compare in constant time and reject anything older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
// Verify against the raw body. Parsed JSON will not match.
export function verify(raw, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (age > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${raw}`)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Event types
{
"id": "evt_1NnQ4kLxP2mT",
"object": "event",
"type": "checkout.settled",
"created": 1790000600,
"livemode": false,
"data": {
"object": { "...": "the checkout, as returned by GET /v1/checkouts/:id" }
}
}| type | Sent when |
|---|---|
| checkout.payment_seen | A transaction appeared at 0 confirmations. |
| checkout.settled | The payment met the issued requirements and was committed. |
| checkout.underpaid | Less than the full amount arrived. |
| checkout.payment_reverted | A seen payment was replaced or dropped. |
| checkout.paid_late | Payment arrived after expiry. |
| checkout.expired | The deadline passed without a settlement commitment, even if funds arrived. |
Delivery
Retries3 days- Exponential backoff. Every attempt and its response code is kept on the event.
Success2xx- Any 2xx within 10 seconds. Anything else is retried.
Orderingnot guaranteed- Reconcile with the current checkout and durable settlement record; event arrival order does not authorise fulfilment.
ReplayGET /v1/events- Cursor-paginated log of every event on the account.
Errors
The checkout API uses this envelope for failures. x402 settlement errors use the protocol response described above. Branch on code, which is stable. Messages are written for a human reading a log and may change.
{
"error": {
"type": "invalid_request_error",
"code": "min_confirmations_out_of_range",
"message": "min_confirmations must be between 0 and 12.",
"param": "min_confirmations",
"doc_url": "https://satsgate.io/docs#errors"
}
}| HTTP | type | Meaning |
|---|---|---|
| 400 | invalid_request_error | A parameter is missing, malformed or out of range. |
| 401 | authentication_error | The key is missing, revoked or the wrong mode. |
| 404 | not_found_error | No object with that identifier in this mode. |
| 409 | idempotency_error | The key was reused with a different body. |
| 429 | rate_limit_error | Too many requests. Retry after the header says. |
| 5xx | api_error | Our fault. Safe to retry with the same idempotency key. |
Rate limits
Live mode100 req/s- Per account, burst to 200.
Test mode25 req/s- Per account.
Retry-Afterheader- Seconds to wait, sent with every 429.
Reading the scheme rather than integrating? The Bitcoin scheme for x402 covers the proposal, wire format and implementation requirements.