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.
Register an endpoint
Section titled “Register an endpoint”{ "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.
Events
Section titled “Events”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.
Payload
Section titled “Payload”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.
Headers
Section titled “Headers”| 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 |
Your receiver, in five rules
Section titled “Your receiver, in five rules”- Verify the signature over the raw body before parsing it (how). Reject with
400on failure. - Dedupe on
event_id. Deliveries are at-least-once; the same event can arrive more than once, including after a manual retry. - Acknowledge fast with any
2xx(within a few seconds). Do slow work afterwards. - Do not rely on ordering. Use
data.statusand, when it matters, re-read the intent. - Query before irreversible actions. Before shipping goods or granting value, call
GET /v1/payment-intents/{id}and checkfulfillable. A webhook is a signal; the API is the source of truth.
Retries, dead letters and replay
Section titled “Retries, dead letters and replay”- 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
deadand 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}/retryre-queues a delivery immediately. The sameevent_idis delivered again, and the action is written to the audit log.
Rotating the signing secret
Section titled “Rotating the signing secret”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.
What we never send
Section titled “What we never send”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.