Webhooks#
Receive push notifications when receipt status changes instead of polling.
Overview#
BasaltSurge can send signed webhook events to your server when a receipt's payment status changes. This eliminates the need to poll
markup
and provides real-time updates for order fulfillment.GET /api/receipts/statusWhen to Use Webhooks vs. Polling#
| Approach | Best For |
|---|---|
| Webhooks (recommended) | Production systems, order fulfillment, real-time status updates |
| Polling markup | Rapid prototyping, environments without public endpoints |
Setup#
1. Configure Your Webhook Endpoint#
Set
markup
when creating a receipt:webhook_urlbashcurl -X POST "https://surge.basalthq.com/api/receipts" \ -H "Content-Type: application/json" \ -H "Ocp-Apim-Subscription-Key: $basaltsurge_API_KEY" \ -d '{ "id": "order_abc", "lineItems": [{ "label": "Widget", "priceUsd": 25.00 }], "totalUsd": 25.00, "webhook_url": "https://your-server.com/api/basaltsurge-webhook" }'
2. Requirements#
- HTTPS required in production (HTTP allowed in development)
- No localhost in production
- Must return markupstatus within 5 seconds
2xx - Must be idempotent (the same event may be delivered twice)
Webhook Payload#
When a receipt status changes, BasaltSurge sends a
markup
request to your POSTmarkup
:webhook_urlHeaders#
markupContent-Type: application/json X-BasaltSurge-Signature: sha256=<hmac_hex> X-BasaltSurge-Event: receipt.status_updated X-BasaltSurge-Delivery: <uuid> X-BasaltSurge-Timestamp: <unix_ms> User-Agent: BasaltSurge-Webhook/1.0
Body#
json{ "event": "receipt.status_updated", "receiptId": "order_abc", "status": "paid", "previousStatus": "checkout_initialized", "transactionHash": "0xabc123...", "buyerWallet": "0x1234...abcd", "merchantWallet": "0x5678...efgh", "totalUsd": 25.00, "token": "USDC", "timestamp": 1713200000000, "brandKey": "myshop" }
Status Values#
The
markup
field will contain one of the following values:status| Status | Description |
|---|---|
markup | Buyer opened the payment portal |
markup | Buyer connected their wallet |
markup | Buyer started the checkout flow |
markup | Payment submitted through the widget |
markup | Payment confirmed on-chain |
markup | Funds verified and split distribution executed |
markup | Refund has been requested |
markup | Refund has been processed |
Signature Verification#
Every webhook is signed using HMAC-SHA256. The signing secret is your existing API key (the same
markup
or Ocp-Apim-Subscription-Keymarkup
you use to call the API). No extra key to manage.x-api-keyVerification Example (Node.js)#
javascriptimport crypto from 'crypto'; function verifyWebhookSignature(body, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); const received = signature.replace('sha256=', ''); return crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(received, 'hex') ); } // Express.js handler app.post('/api/basaltsurge-webhook', (req, res) => { const signature = req.headers['x-basaltsurge-signature']; const rawBody = JSON.stringify(req.body); // Use your same API key for verification if (!verifyWebhookSignature(rawBody, signature, process.env.basaltsurge_API_KEY)) { return res.status(401).json({ error: 'Invalid signature' }); } const { event, receiptId, status, transactionHash } = req.body; // Process the event (ensure idempotency!) console.log(`Receipt ${receiptId} is now ${status}`); // Example: fulfill order when payment is confirmed if (status === 'paid' || status === 'reconciled') { fulfillOrder(receiptId, transactionHash); } res.status(200).json({ ok: true }); });
Verification Example (Python)#
pythonimport hmac, hashlib, json, os from flask import Flask, request, jsonify app = Flask(__name__) def verify_signature(body: str, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), body.encode(), hashlib.sha256 ).hexdigest() received = signature.replace('sha256=', '') return hmac.compare_digest(expected, received) @app.route('/api/basaltsurge-webhook', methods=['POST']) def webhook(): sig = request.headers.get('X-BasaltSurge-Signature', '') raw = request.get_data(as_text=True) # Use your same API key for verification if not verify_signature(raw, sig, os.environ['basaltsurge_API_KEY']): return jsonify(error='Invalid signature'), 401 data = request.json receipt_id = data['receiptId'] status = data['status'] if status in ('paid', 'reconciled'): fulfill_order(receipt_id, data.get('transactionHash')) return jsonify(ok=True), 200
Delivery & Retry#
- Timeout: 5 seconds per attempt
- Retries: 1 automatic retry after 5 seconds if the first attempt fails
- Retry conditions: Non-2xx response, network error, or timeout
- Idempotency: Use the markupheader as a unique delivery ID to deduplicate events
X-BasaltSurge-Delivery
If both attempts fail, the event is logged but not retried further.
Platform / Partner Container Compatibility#
Webhook signing is container-stable: the API key used for signing is captured from the request header at receipt creation time and stored on the receipt document. This means:
- If a receipt is created on a partner container (e.g., markup) using API key
partner.basaltsurge.commarkup, that key is stored on the receipt.pk_abc... - When Thirdweb or Stripe webhooks later fire on the platform container, the dispatch reads the signing secret from the receipt document — not from the platform's environment.
- Result: The developer always verifies webhooks with their same API key, regardless of which container processes the event.
Key point: You use one key for everything — API authentication and webhook verification. No separate webhook secret needed.
Redirect URL (Stripe Only)#
The
markup
parameter is passed through to the Stripe Crypto Onramp session. After the buyer completes the Stripe-hosted onramp flow, Stripe redirects them to this URL.redirect_urlImportant:
markup
only works with Stripe. Other onramp providers (Coinbase, Transak, MoonPay, Ramp) open in new tabs managed by thirdweb and do not support external redirect injection. There is no portal-level auto-redirect.redirect_url| Provider | Redirect Support |
|---|---|
| Stripe Crypto Onramp | ✅ Passed through session metadata |
| Coinbase Onramp | ❌ Requires CDP domain allowlisting |
| Transak / MoonPay / Ramp | ❌ Managed internally by thirdweb |