API reference

DigiPay speaks plain REST. All endpoints live under /v1/pay. An account (merchant) owns one or more stores; most per-shop config (receive address, webhook, session defaults) is scoped to a store.

Authentication

Two token kinds share the same Authorization: Bearer … header, distinguished by prefix:

  • dps_…session token minted when you sign in with Digi-ID from the dashboard. 30-day rolling expiry, revoked on sign-out. Used by the browser.
  • dgp_…API key. Long-lived, N per account, revocable individually. Used by your server / CI / SDK. Create them from the API keys tab.

Both look like {prefix}_{secret}. Only a SHA-256 hash of the secret is stored server-side; losing the secret means you mint (or revoke + re-create) the key, not your funds.

Authorization: Bearer dgp_{prefix}_{secret}

Register (SDK)

Unauthenticated. Creates an account + its first store in one call and returns the initial API key. Aimed at self-host installs and CLI bootstrapping — interactive users should sign in with Digi-ID from the dashboard instead.

POST /v1/pay/merchants
{
  "displayName": "My Shop",
  "addressOrXpub": "dgb1q…",           // or an xpub6/zpub
  "network": "mainnet",                 // optional
  "webhookUrl": "https://…/digipay"    // optional, generates webhookSecret
}

Response includes id, storeId, and a one-shot apiKey. Save the key — the secret is never retrievable again.

Digi-ID sign-in

Used by the browser dashboard — servers typically don't need this flow.

GET /v1/pay/auth/challenge

Returns { nonce, uri, expiresAt }. Render uri as a QR for the user's DigiByte wallet to sign.

POST /v1/pay/auth/callback

Wallet-driven: POSTs { address, uri, signature }. DigiPay verifies the signature, creates the merchant + Default Store if new, and marks the challenge as signed.

GET /v1/pay/auth/poll/{nonce}

Browser polls this until it flips to { status: "signed", merchantId, displayName, token }. Store the token — it's your Bearer.

DELETE /v1/pay/auth/session

Sign out: invalidates the current dps_ session token. API keys are untouched.

Account

GET /v1/pay/me

Returns the authenticated account: { id, displayName, digiIdAddress, createdAt, stores: [ … ] }.

PATCH /v1/pay/me

Update the account display name (everything else lives per-store now):

{
  "displayName": "Acme Co"
}

Stores

A store owns its receive address/xpub, webhook URL, default expiry, and sessions. One account can run many stores (multiple shops, tip jars, per-environment setups).

GET /v1/pay/stores

List all stores owned by the account.

POST /v1/pay/stores

Create a new store:

{
  "name": "Acme Tips Jar",
  "network": "mainnet"              // optional: mainnet | testnet | regtest
}
GET /v1/pay/stores/{storeId}

Returns one store. 404 if the store belongs to a different merchant.

PATCH /v1/pay/stores/{storeId}

Update any subset of the below. Setting webhookUrl for the first time auto-generates a webhook secret.

{
  "name": "Main Shop",
  "network": "mainnet",
  "addressOrXpub": "dgb1q…",          // or an xpub; empty string clears
  "webhookUrl": "https://…/digipay",  // empty string clears + revokes secret
  "defaultSessionExpiryMinutes": 45    // 1..1440
}
DELETE /v1/pay/stores/{storeId}

Delete a store. Rejects the request if this is the account's only remaining store.

GET /v1/pay/stores/{storeId}/donation-address

Stable address for donation buttons. In xpub mode this is derive index 0 (reserved — invoice sessions start at 1). In address mode it's the configured address.

POST /v1/pay/stores/{storeId}/webhook/test

Fires a synthetic webhook.test at the configured URL. Response includes the resulting delivery id + status so you can see what happened without opening the dashboard.

Sessions

POST /v1/pay/sessions

Create a payment session. storeId is optional — omit it to use the account's first store.

{
  "amount": 12.5,                 // required, DGB
  "storeId": "sto_…",             // optional, defaults to first store
  "label": "Order #4201",         // optional, shown on checkout
  "memo": "inv-001",              // optional, merchant-private
  "fiatCurrency": "USD",          // optional
  "fiatAmount": 5.00,             // optional
  "expiresInSeconds": 1800        // optional, overrides store default
}

Response includes the derived address, storeId, a BIP21 uri, the hosted checkoutUrl, and expiresAt.

GET /v1/pay/sessions

List sessions, newest first. Filters: ?status=pending|seen|paid|confirmed|expired|underpaid, ?storeId=sto_…, ?take=1..100 (default 25), ?skip=0...

GET /v1/pay/sessions/{id}

Public read of a session's current state. No auth — the id is random and contains no secret. Used by the hosted checkout page.

API keys

GET /v1/pay/api-keys

List your keys. Returns prefix + metadata (created, last used, revoked) — never the raw secret.

POST /v1/pay/api-keys

Create a new API key. The raw secret is returned once in the response as apiKey; save it to your secrets manager. DigiPay only stores the hash.

{
  "label": "production"            // optional, 1..80 chars
}
DELETE /v1/pay/api-keys/{id}

Revoke a key. Further auth attempts with it return 401. Already-in-flight sessions are unaffected; webhooks continue to fire.

Webhooks

DigiPay POSTs signed JSON to a store's webhookUrl on every session state change. Every attempt is persisted and visible in the dashboard; failures can be replayed manually. Automatic retries / dead-letter aren't in scope yet — treat webhooks as a hint and reconcile via GET /v1/pay/sessions/{id} if your flow is money-critical.

Body:

{
  "event": "session.paid",
  "timestamp": "2026-04-19T12:34:56Z",
  "session": {
    "id": "ses_…",
    "merchantId": "mer_…",
    "storeId": "sto_…",
    "address": "dgb1q…",
    "amountSatoshis": 1250000000,
    "amount": 12.5,
    "status": "paid",
    "receivedSatoshis": 1250000000,
    "confirmations": 1,
    "paidTxid": "…",
    "createdAt": "…",
    "expiresAt": "…"
  }
}

Delivery log

GET /v1/pay/stores/{storeId}/webhook-deliveries

Recent delivery attempts (newest first, ?take=1..100, default 25). Each row carries status code or error message, duration, response snippet (2KB cap), and attempt number.

POST /v1/pay/stores/{storeId}/webhook-deliveries/{deliveryId}/replay

Re-fire a past delivery using the original session payload. Writes a new row with attempt incremented so the audit trail is preserved. Test deliveries (no session) cannot be replayed — fire a fresh test instead.

Signature verification

Compute HMAC_SHA256(secret, rawBody) as hex, prefix with sha256=, and constant-time compare against the X-DigiPay-Signature header. Example in Node:

const crypto = require('crypto');

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', process.env.DIGIPAY_SECRET)
          .update(req.body).digest('hex');
  const provided = req.headers['x-digipay-signature'];
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString());
  // reconcile event.session.id in your database
  res.status(200).end();
});

Embed widget

The JS embed is hosted at /embed/digipay.js. ~7 KB, no dependencies, derives its own origin from the script src. Try it live →

Programmatic:

DigiPay.checkout({ sessionId: 'ses_abc' });  // opens iframe modal
DigiPay.close();                              // dismiss it
const a = DigiPay.button({ address: 'dgb1…', amount: 5, name: 'Shop' });
document.body.appendChild(a);

Declarative:

<button data-digipay-checkout data-session-id="ses_abc">Pay</button>
<span data-digipay-button data-address="dgb1…" data-amount="5">Tip</span>

Auto-binds on DOMContentLoaded; call DigiPay.bind(rootEl) after injecting new elements dynamically.

Status codes

  • 200 — success.
  • 400 — validation error, body includes an error field.
  • 401 — missing / invalid Bearer token, or key has been revoked.
  • 404 — resource not found, or belongs to a different merchant.
  • 409 — challenge already resolved (Digi-ID flows).

Questions?

The source is open. Clone the repo, file an issue, or just run DigiPay yourself — it's designed to self-host in a single container backed by SQLite or Postgres.

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.