Skip to content
SATSGATE

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.

Authorization header
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.

Request
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=3600

2. 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.

Response from your route
HTTP/1.1 402 Payment Required
Content-Type: application/json
PAYMENT-REQUIRED: <base64-encoded PaymentRequired JSON>

{}
Decoded PAYMENT-REQUIRED
{
  "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.

Client retry
GET /report HTTP/1.1
Host: example.com
PAYMENT-SIGNATURE: <base64-encoded PaymentPayload JSON>
Decoded PAYMENT-SIGNATURE
PaymentPayload
{
  "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.

Result after settlement
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
SettleResponse
{
  "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.

Local CLI workflow
satsgate listen --forward-to localhost:3000/webhooks/satsgate
satsgate test pay      co_3PmQ2r8kLxN1
satsgate test advance  co_3PmQ2r8kLxN1 --confirmations 1
CommandEffect
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> --rbfReplaces 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

checkoutTest mode
{
  "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.
statusMeaning
pendingCreated. No transaction has been seen at the address.
seenA transaction is in the mempool at 0 confirmations.
settledA qualifying output set and its payment identifier were committed before expiry.
underpaidThe selected outputs total less than amount_sat. A top-up may complete the same quote before expiry.
revertedA seen transaction was replaced or dropped before it settled.
expiredThe 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.
Request
curl -G https://api.satsgate.io/v1/checkouts \
  -H "Authorization: Bearer sk_test_4eC39Hq" \
  -d limit=20 \
  -d status=settled \
  -d starting_after=co_3PmQ2r8kLxN1

Webhooks

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.

node
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

event
{
  "id": "evt_1NnQ4kLxP2mT",
  "object": "event",
  "type": "checkout.settled",
  "created": 1790000600,
  "livemode": false,
  "data": {
    "object": { "...": "the checkout, as returned by GET /v1/checkouts/:id" }
  }
}
typeSent when
checkout.payment_seenA transaction appeared at 0 confirmations.
checkout.settledThe payment met the issued requirements and was committed.
checkout.underpaidLess than the full amount arrived.
checkout.payment_revertedA seen payment was replaced or dropped.
checkout.paid_latePayment arrived after expiry.
checkout.expiredThe 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 response
{
  "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"
  }
}
HTTPtypeMeaning
400invalid_request_errorA parameter is missing, malformed or out of range.
401authentication_errorThe key is missing, revoked or the wrong mode.
404not_found_errorNo object with that identifier in this mode.
409idempotency_errorThe key was reused with a different body.
429rate_limit_errorToo many requests. Retry after the header says.
5xxapi_errorOur 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.