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.
{
"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.
Returns { nonce, uri, expiresAt }. Render uri as a QR for the user's DigiByte wallet to sign.
Wallet-driven: POSTs { address, uri, signature }. DigiPay verifies the signature, creates the merchant + Default Store if new, and marks the challenge as signed.
Browser polls this until it flips to { status: "signed", merchantId, displayName, token }. Store the token — it's your Bearer.
Sign out: invalidates the current dps_ session token. API keys are untouched.
Account
Returns the authenticated account: { id, displayName, digiIdAddress, createdAt, stores: [ … ] }.
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).
List all stores owned by the account.
Create a new store:
{
"name": "Acme Tips Jar",
"network": "mainnet" // optional: mainnet | testnet | regtest
}Returns one store. 404 if the store belongs to a different merchant.
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 a store. Rejects the request if this is the account's only remaining store.
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.
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
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.
List sessions, newest first. Filters: ?status=pending|seen|paid|confirmed|expired|underpaid, ?storeId=sto_…, ?take=1..100 (default 25), ?skip=0...
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
List your keys. Returns prefix + metadata (created, last used, revoked) — never the raw secret.
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
}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
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.
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 anerrorfield.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.