Webhooks
Webhooks are the source of truth for fulfillment. FastStar POSTs signed JSON events to your endpoint whenever something important happens — payments, refunds, subscriptions, settlements — so your systems stay in sync even when the buyer never returns to your site.
Configuration
Configure your webhook in the merchant dashboard: set the callback endpoint URL and your signing secret, and optionally subscribe to a subset of event types (an empty subscription means all events). Your endpoint must respond with a 2xx status to acknowledge delivery.
Delivery format
Each delivery is an HTTP POST with these headers:
| Header | Description |
|---|---|
X-Webhook-Signature | Signature: t=<unix timestamp>,v1=<hex HMAC-SHA256> |
X-Webhook-ID | Unique event id — use it for idempotency |
X-Webhook-Timestamp | Event creation time (unix seconds) |
The body is a JSON event envelope:
{
"id": "evt_xxx",
"type": "payment.succeeded",
"created": 1769900000,
"data": { "payment_id": "pi_xxx", "amount": 1999, "currency": "USD", "status": "succeeded" },
"livemode": true,
"api_version": "v1"
} Event types
| Event | Fired when |
|---|---|
payment.succeeded | A payment completed successfully — fulfill the order |
payment.failed | A payment attempt failed |
payment.canceled | A payment was canceled |
payment.refunded | A charge was refunded |
refund.succeeded | A refund completed |
subscription.created / subscription.updated / subscription.canceled | Subscription lifecycle changes |
invoice.paid / invoice.payment_failed | Subscription invoice outcomes |
dispute.created / dispute.updated / dispute.closed | Chargeback / dispute lifecycle |
settlement.succeeded | A settlement (payout of your balance) completed |
Verifying signatures
The signature header has the form t=<timestamp>,v1=<signature>. The
signature is an HMAC-SHA256 over the string <timestamp>.<raw body>
using your webhook secret, hex-encoded. Verify on the raw request body, before any
JSON parsing:
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('='))
);
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
return (
parts.v1 &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
);
}
// Express: keep the raw body for verification
app.post('/webhooks/faststar', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyWebhook(req.body.toString(), req.get('X-Webhook-Signature'), process.env.WEBHOOK_SECRET)) {
return res.status(400).send('bad signature');
}
const event = JSON.parse(req.body);
if (event.type === 'payment.succeeded') {
// fulfill the order (idempotently, keyed on event.id)
}
res.sendStatus(200); // acknowledge quickly; do heavy work async
});
Reject events whose timestamp is too old to limit replay. You can also verify a signature
through the API with POST /public/verify-signature
({ provider_name, payload, signature } →
{ valid: true }), but local HMAC verification is recommended.
Retries & idempotency
- A delivery counts as successful only when your endpoint returns a 2xx status. Any other status, or a network error, queues a retry.
- Retries use increasing backoff (on a schedule of roughly 1 minute, 5 minutes, 30 minutes, 2 hours, 24 hours) up to your configured maximum attempts (default 3).
-
Because retries can deliver the same event more than once, process events
idempotently — key your processing on the event
id(X-Webhook-ID). - Respond fast (acknowledge, then process asynchronously) to avoid timeouts being treated as failures.
Front-end events are not fulfillment. SDK callbacks such as
checkout.success only confirm the buyer's UI experience. Always fulfill from
payment.succeeded webhooks.