Skip to content

Webhooks

GRYPHEM PAY sends an HTTPS POST to your registered endpoints whenever something you subscribed to happens. Deliveries are signed, at-least-once and retried with backoff — so your receiver must verify, dedupe and acknowledge quickly.

POST/v1/webhook-endpointslive · after account activation
{
"url": "https://merchant.example/webhooks/gryphem",
"enabled_events": ["payment.confirmed", "payment.review_required", "payment.reorged", "payment.expired"],
"description": "Order fulfilment"
}

The response includes the signing secret (whsec_…) once. Use ["*"] to receive every event type. Endpoints are per environment and can optionally be limited to one store. Live events are delivered only to endpoints registered in live mode, so register yours once your account is activated.

Endpoint URLs must be public HTTPS. They are checked when you register them and again at delivery time: the destination IP is validated when connecting (private, loopback, link-local and metadata ranges are refused), redirects are not followed, and request/response sizes and timeouts are capped. In local development a private URL such as http://localhost:4400 works only when the platform operator enables an explicit development flag.

event_type Sent when
payment_intent.created An intent is created (API, dashboard, invoice or payment link)
invoice.awaiting_payment An invoice’s intent has a locked quote and address
payment.detected A transfer was seen but is not final
payment.confirming The transfer is in a block, collecting confirmations
payment.confirmed The expected amount is final — check data.fulfillable
payment.expired The quote expired without payment
payment.review_required underpaid, overpaid, late_payment, manual_review or an extra payment after confirmation
payment.reorged A block with the payment was replaced — pause fulfilment
payment.resolved A merchant or operator resolution was recorded
payment.canceled You cancelled the intent
webhook.test You pressed Send test event (or called POST /v1/webhook-endpoints/{id}/test)
merchant.verification_updated Your business verification status changed
payout.requested A payout was requested and its amount held from your custodial balance
payout.approved A second member approved the payout; a GRYPHEM operator reviews it next
payout.rejected A GRYPHEM operator rejected the payout; the amount is available again
payout.canceled The payout was canceled before submission; the amount is available again
payout.sent The platform’s hot wallet broadcast the payout (data.tx_hash)
payout.completed Bitcart reports the payout transaction as confirmed
payout.failed The payout could not be sent; the amount stays held while GRYPHEM investigates unless nothing can have been broadcast

Payment and payout events start once your account is activated for live payments. The payout.* events have a store_id of null because balances and payouts belong to the whole organization.

The body is the same object you can fetch from GET /v1/events/{event_id}:

{
"event_id": "evt_01J…",
"event_type": "payment.confirmed",
"api_version": "2026-10-01",
"created_at": "2026-10-20T12:00:00Z",
"merchant_id": "org_…",
"store_id": "store_01J…",
"environment": "live",
"object": { "type": "payment_intent", "id": "pi_01J…" },
"data": {
"status": "confirmed",
"previous_status": "confirming",
"status_reason": "bitcart_complete",
"amount": "59.90",
"currency": "EUR",
"merchant_order_id": "order-123",
"fulfillable": true,
"quote": null,
"totals": null
}
}

data is deliberately minimal. For live payments, quote and totals are null (they describe watch-only processing); read the intent’s processor object with GET /v1/payment-intents/{id} for the paid asset and amount. payment_intent.created carries status, amount, currency, merchant_order_id and checkout_url; webhook.test carries message and endpoint_id; merchant.verification_updated carries kyb_status and previous_kyb_status.

Header Value
Gryphem-Signature t=<unix>,v1=<hex> — several v1 values during secret rotation
Gryphem-Event-Id Same as event_id
Gryphem-Event-Type Same as event_type
Gryphem-Delivery-Id The delivery (whd_…) — one per event and endpoint
Gryphem-Delivery-Attempt 1 for the first attempt, then 2, 3, …
Content-Type application/json
  1. Verify the signature over the raw body before parsing it (how). Reject with 400 on failure.
  2. Dedupe on event_id. Deliveries are at-least-once; the same event can arrive more than once, including after a manual retry.
  3. Acknowledge fast with any 2xx (within a few seconds). Do slow work afterwards.
  4. Do not rely on ordering. Use data.status and, when it matters, re-read the intent.
  5. Query before irreversible actions. Before shipping goods or granting value, call GET /v1/payment-intents/{id} and check fulfillable. A webhook is a signal; the API is the source of truth.
  • A delivery succeeds on any 2xx. Anything else — non-2xx, timeout, connection error — is retried with exponential backoff and jitter.
  • After the maximum number of attempts the delivery is marked dead and listed as a dead letter for operators; it is never silently dropped.
  • Every attempt is recorded: start time, duration, response status and a short sanitised excerpt of the response. Inspect them in the dashboard or with GET /v1/webhook-deliveries/{id}.
  • Retry now: POST /v1/webhook-deliveries/{id}/retry re-queues a delivery immediately. The same event_id is delivered again, and the action is written to the audit log.
POST/v1/webhook-deliveries/{webhook_delivery_id}/retrylive · after account activation

POST /v1/webhook-endpoints/{id}/rotate-secret returns a new secret (shown once). The previous secret keeps signing in parallel for previous_secret_ttl_hours (default 24, up to 168, or 0 to expire it immediately), so during the window each delivery carries two v1 signatures. Deploy the new secret — or configure both — then let the old one expire.

Webhooks never contain API keys, signing secrets, full customer personal data or wallet extended public keys. Logs on both sides should never record the Gryphem-Signature value together with your secret.