Merchant operations
Merchants publish one signed offer, then use resolutions, webhooks, receipts, and reconciliation records to operate the many payment attempts that offer may produce.
Merchant checkout model
For a fixed-price checkout, create an offer with pricing_type: "fixed" and one amount. Share its payment link or link QR at checkout. The merchant does not need to generate a one-time invoice before the payer begins.
POST /demo/offers
{
"username": "coffee",
"description": "Coffee checkout",
"pricing_type": "fixed",
"amount": "1000",
"assets": [{ "asset_type": "ckb", "symbol": "CKB" }]
}
Scenario: a physical checkout counter
The merchant displays the stable payment link QR. Every customer scans the same code, selects the expected amount in their wallet, and receives a fresh invoice only after readiness succeeds. The merchant tracks individual resolution IDs, not a constantly replaced QR.
Resolution lifecycle
Every successful invoice request creates one resolution. A resolution records the amount, asset, invoice data, history, settlement metadata, and associated webhook events.
| Status | Meaning | Merchant action |
|---|---|---|
invoice_created | The resolver issued an invoice. | Await payer and payment status. |
invoice_received | The invoice lifecycle advanced at its source. | Keep waiting or show pending confirmation. |
invoice_paid | Payment was settled. | Fulfill the order and persist the receipt. |
invoice_expired, invoice_failed, invoice_cancelled | The attempt is terminal without settlement. | Keep the record; allow a new valid payment attempt where appropriate. |
GET /offers/:offer_id/resolutions
GET /offers/:offer_id/resolutions/:resolution_id
POST /offers/:offer_id/resolutions/:resolution_id/sync
Subscribe to webhooks
Register an HTTPS endpoint per offer. An omitted event list subscribes to all supported invoice events. The resolver generates a high-entropy signing secret and returns it only in the creation response; store it immediately in your secret manager.
curl -X POST https://resolver.example/offers/0x.../webhooks \
-H 'content-type: application/json' \
-H 'Authorization: Bearer change-me' \
-d '{
"url": "https://merchant.example/webhooks/fiber",
"events": ["invoice.paid", "invoice.failed"]
}'
{
"id": "wh_...",
"url": "https://merchant.example/webhooks/fiber",
"events": ["invoice.paid", "invoice.failed"],
"disabled": false,
"secret_hint": "whsec...9Kp2",
"signing_secret": "whsec_..."
}
Each POST includes x-fiber-offers-event-id, x-fiber-offers-delivery-id, x-fiber-offers-event-type, x-fiber-offers-timestamp, and x-fiber-offers-signature. A successful 2xx response marks the attempt delivered.
Manage endpoints
| Operation | Endpoint | Effect |
|---|---|---|
| List | GET /offers/:offer_id/webhooks | Returns URLs, event filters, status, secret hints, and timestamps. Secrets are never returned. |
| Test | POST /offers/:offer_id/webhooks/:webhook_id/test | Sends a signed webhook.test event to one active endpoint. |
| Pause or resume | PATCH /offers/:offer_id/webhooks/:webhook_id | Set { "disabled": true } to stop future events without losing configuration or history. |
| Rotate secret | POST /offers/:offer_id/webhooks/:webhook_id/rotate-secret | Invalidates the old secret immediately and returns the replacement once. |
| Delete | DELETE /offers/:offer_id/webhooks/:webhook_id | Permanently removes the subscription. Historical delivery records remain. |
The local console defaults to /demo/webhook-receiver. It is an inspectable development inbox, not a production receiver: use GET /demo/webhook-receiver to see what arrived, but deploy your own HTTPS endpoint with signature verification for real payments.
Receive and verify events
The signature is sha256=HMAC(secret, timestamp + "." + rawBody). Read the untouched request bytes, reject stale timestamps, compare signatures safely, and deduplicate by event ID before queuing work.
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const secret = process.env.FIBER_OFFERS_WEBHOOK_SECRET;
const processedEventIds = new Set(); // Use a database uniqueness constraint in production.
createServer(async (request, response) => {
if (request.method !== "POST" || request.url !== "/webhooks/fiber") {
response.writeHead(404).end();
return;
}
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const rawBody = Buffer.concat(chunks);
const timestamp = request.headers["x-fiber-offers-timestamp"];
const signature = request.headers["x-fiber-offers-signature"];
if (!verifySignature(secret, timestamp, rawBody, signature)) {
response.writeHead(401).end();
return;
}
const event = JSON.parse(rawBody.toString("utf8"));
if (processedEventIds.has(event.id)) {
response.writeHead(204).end();
return;
}
processedEventIds.add(event.id);
response.writeHead(204).end(); // Acknowledge before fulfillment work.
queueMicrotask(() => handleEvent(event));
}).listen(3000);
function verifySignature(secret, timestamp, rawBody, signature) {
if (!secret || !timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = `sha256=${createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex")}`;
const expectedBytes = Buffer.from(expected);
const receivedBytes = Buffer.from(signature);
return expectedBytes.length === receivedBytes.length && timingSafeEqual(expectedBytes, receivedBytes);
}
async function handleEvent(event) {
if (event.type === "invoice.paid") {
// Fulfill once using event.id or payload.resolution.id as an idempotency key.
}
}
Receipts and reconciliation
GET /offers/:offer_id/resolutions/:resolution_id/receipt.json
GET /offers/:offer_id/reconciliation.json
GET /offers/:offer_id/reconciliation.csv
Use the receipt as the individual settlement record and the reconciliation export for accounting, support, or close-of-day reporting. The output deliberately keeps offer ID, resolution ID, status, amount, asset, and settlement context together.
Scenario: fulfillment after webhook delay
A merchant receives a successful webhook but its order system is temporarily unavailable. The webhook is redelivered or the merchant calls the resolution endpoint and receipt endpoint later. The resolution ID gives both systems a stable reference for idempotent fulfillment.
Manual and background delivery
During development, operators can inspect and deliver the outbox manually. In a live deployment, enable workers to poll invoice settlement state and retry pending webhook deliveries.
POST /offers/:offer_id/webhook-events/deliver
POST /offers/:offer_id/webhook-events/:event_id/deliver
RESOLVER_WORKERS_ENABLED=true \
RESOLVER_PUBLIC_URL=https://resolver.example \
FIBER_RPC_URL=http://127.0.0.1:8227 \
npm run dev