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:

HeaderDescription
X-Webhook-SignatureSignature: t=<unix timestamp>,v1=<hex HMAC-SHA256>
X-Webhook-IDUnique event id — use it for idempotency
X-Webhook-TimestampEvent 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

EventFired when
payment.succeededA payment completed successfully — fulfill the order
payment.failedA payment attempt failed
payment.canceledA payment was canceled
payment.refundedA charge was refunded
refund.succeededA refund completed
subscription.created / subscription.updated / subscription.canceledSubscription lifecycle changes
invoice.paid / invoice.payment_failedSubscription invoice outcomes
dispute.created / dispute.updated / dispute.closedChargeback / dispute lifecycle
settlement.succeededA 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.