Pharos Docs
Collections

Collections Webhooks

Verify checkout lifecycle events and fulfill purchases only after authoritative payment success.

Webhooks are the server-to-server completion path for Payment Links and Cart Checkout. Browser redirects improve the buyer experience, but they are not reliable proof of payment: the browser may close, a redirect may fail, or an asynchronous method may complete later.

Events to understand

EventWhat it meansFulfill?
checkout_session_completedThe hosted buyer flow completed; paymentStatus may still be processingNo
checkout_session_expiredThe session is no longer usableNo
purchase_succeededThe purchase reached authoritative payment successYes, after verification and deduplication

purchase_succeeded may contain checkoutSession.id. Use it to reconcile the Checkout Session, clientReferenceId, invoice, payment, subscription, and your original cart or order.

Delivery flow

  1. Pharos persists the canonical checkout and payment state.
  2. Pharos sends the event JSON to the webhook URL configured for your business.
  3. Your server reads the raw request body and verifies x-pharos-signature with the webhook signing secret.
  4. Your server parses the verified event and checks whether eventId or idempotencyKey was already processed.
  5. Your server records the event identity and applies the business action atomically, or with an equivalent idempotent workflow.
  6. Your server returns a successful response only after accepting the delivery.
  7. Pharos may retry unsuccessful deliveries; duplicate handling must make every retry safe.

Verify the signature

Pharos signs the exact request body with HMAC SHA-256 and sends the digest as x-pharos-signature: sha256=.... Compute the HMAC from the raw bytes before JSON parsing and compare digests with a timing-safe comparison.

import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyPharosSignature(
  rawBody: Buffer,
  signatureHeader: string,
  signingSecret: string,
) {
  const received = signatureHeader.replace(/^sha256=/, '')
  const expected = createHmac('sha256', signingSecret)
    .update(rawBody)
    .digest('hex')

  const receivedBuffer = Buffer.from(received, 'hex')
  const expectedBuffer = Buffer.from(expected, 'hex')

  return receivedBuffer.length === expectedBuffer.length
    && timingSafeEqual(receivedBuffer, expectedBuffer)
}

Reject a delivery if the signature is missing, malformed, or invalid. Do not verify a re-serialized JSON object because whitespace and key ordering may differ from the signed body.

Deduplicate before fulfillment

Treat webhook delivery as at-least-once. Store eventId or the event's stable idempotencyKey under a unique constraint. A repeated delivery must return success without creating a second order, entitlement, email, ledger action, or external side effect.

async function handleVerifiedEvent(event: PharosEvent) {
  if (await events.has(event.eventId)) return

  await database.transaction(async (tx) => {
    await tx.events.insert({
      eventId: event.eventId,
      idempotencyKey: event.idempotencyKey,
    })

    if (event.eventKey === 'purchase_succeeded') {
      await tx.orders.markFulfilled({
        checkoutSessionId: event.payload.checkoutSession?.id,
      })
    }
  })
}

The example is illustrative: use the transaction, unique constraint, or durable workflow mechanism appropriate for your system.

Asynchronous payment methods

Transfers, tickets, cash, redirects, and other asynchronous methods can complete after checkout navigation finishes. A session may therefore be complete while paymentStatus is processing. Keep the order pending and wait for purchase_succeeded; do not infer success from elapsed time or the success page.

If a payment later fails, update the order from the corresponding verified payment event and expose a safe retry path. Reusing the same Checkout Session is valid only when its current lifecycle allows another attempt.

Operational checklist

  • Use HTTPS for the webhook target.
  • Keep the signing secret outside source control and rotate it through the supported configuration flow.
  • Capture x-pharos-event-id, x-pharos-event-key, and request correlation data in structured logs without logging secrets or sensitive payment data.
  • Respond quickly and move slow fulfillment work to an idempotent background workflow.
  • Alert on repeated delivery failures and reconcile successful payments that remain unfulfilled.
  • Test immediate success, redirect success, delayed success, failure, expiration, and duplicate delivery before production.

Next steps

Delivery history and retries

Configured endpoints must use public HTTPS without URL credentials. Private, loopback, link-local and unsafe DNS destinations are rejected. Redirects are not followed. A delivery has a ten-second deadline; Pharos retains a bounded response for diagnosis.

Automatic delivery uses up to eight attempts over a maximum of 24 hours. Retries preserve the event ID and HMAC-signed event body. Deduplicate by the event ID; network failures mean external delivery cannot be promised exactly once.

Integration settings link to delivery history. A manual webhook retry targets that delivery only and does not resend the event to email or analytics consumers. Email acceptance and recipient-server delivery are separate states and neither indicates that a person read the message.

Commercial notifications require activation for the business. Reconciliation of older payments does not automatically send historical purchase notifications. Verify activation and callback configuration before relying on these events for live fulfillment.

On this page