Skip to main content
Webhooks push HTTP notifications to your server when key events happen: balance changes, redemptions, tier transitions, and more. Instead of polling the API for changes, you register a URL and Scrip delivers signed payloads the moment each event occurs.

Choose an endpoint

Create an endpoint

Register a URL and specify which events you want to receive. Subscribe only to the types you handle. "enabled_events": ["*"] receives every type.
The response includes a secret starting with whsec_. Store it immediately. It cannot be retrieved later. You’ll use it to verify signatures.
URLs must use HTTPS with a publicly resolvable hostname. Private IPs, localhost, .local, and .internal domains are rejected.

Event types

Every event type is one of two kinds.

Movement and lifecycle events

A movement event announces one journal entry: balance.*, transfer.completed, and program.*. Every journal action has exactly one movement event. A participant or group balance changes only through movement events that name it, as the subject, as the counterparty, or as a transfer source or recipient. A handler that applies those events in order reconstructs every participant and group balance. Program wallet balances cannot be reconstructed from webhooks: issuance from a prefunded wallet, voids that return value to it, and settlement differences post to the wallet without an event naming it. Reconcile wallets from Get program balance or Get program statement. A lifecycle event announces a resource changing state: redemption.*, fulfillment_account.*, reward.*, participant.*, and event.*. It carries the resource id, its status where the resource has one, and the ids of related resources. It never carries a journal entry id; the matching movement event does.

Movement events

Lifecycle events

Authorization emits redemption.pending and balance.held. Capture emits redemption.completed and balance.redeemed with bucket: "HELD". An instant redemption emits redemption.completed and balance.redeemed with bucket: "AVAILABLE". Failure, timeout, and cancellation emit redemption.failed or redemption.cancelled, plus balance.released when live reserved value was released. If you maintain a participant or group balance mirror from webhooks, apply the movement events; the lifecycle events tell you why each one happened.

Reward status events

reward.status_changed fires when a reward’s status changes between DRAFT, ACTIVE, OUT_OF_STOCK, and ARCHIVED. PATCH /v1/programs/{programId}/rewards/{rewardId} emits it when status changes. Same-status writes and non-status field updates do not. Creating a reward does not emit it; use the create response. For UNIT_BASED rewards with max_total, authorization and instant redemption can move ACTIVE to OUT_OF_STOCK. Fail, cancel, timeout, and reversal can move OUT_OF_STOCK back to ACTIVE. Under async fulfillment that cycle can repeat. Treat OUT_OF_STOCK as live inventory state, not a terminal catalog state. See Rewards catalog.

Gift-card events

Use gift-card events to update your application when an order finishes. On redemption.fulfillment_succeeded, retrieve a claim link for the redemption and make it available to the participant. On redemption.fulfillment_failed, show the failed outcome and inspect failure_reason. If you receive fulfillment_account.funds_low, add funds to the account in the dashboard. Scrip retries the order automatically. This alert is sent once per fulfillment, even if several attempts encounter a low balance. fulfillment_account.status_changed tells you when the account’s status changes, for example when it becomes ACTIVE, is disabled, or needs to be reconnected. Check status_reason and act on the account in the dashboard. These events include the redemption and program IDs so you can find the purchase in your application. They do not include a claim link. See Gift cards for handling orders that finish after the initial request. For the data object each type delivers, see Event payloads.

Read the payload

Every delivery sends a JSON envelope:

Versioning

Adding a field to a payload never changes api_version, so ignore fields you do not recognize. A change that renames, removes, or retypes a field gets a new api_version date. The value is fixed when the event is created. A retried delivery keeps the value it was created with, so a delivery labeled with an older date carries that older contract’s shapes.

Verify a signature

Every delivery includes a Scrip-Signature header so you can verify it came from Scrip and wasn’t tampered with.

Header format

Verification steps

1

Extract components

Parse the t and v1 values from the Scrip-Signature header.
2

Construct signed payload

Concatenate the timestamp, a literal dot, and the raw request body: {t}.{raw_body}
3

Compute expected signature

Calculate HMAC-SHA256(your_endpoint_secret, signed_payload) and hex-encode the result.
4

Compare signatures

Use constant-time comparison. Reject the request if they don’t match.
5

Check timestamp

Reject if abs(now - t) exceeds your tolerance. We recommend 5 minutes.

Retry policy

If your endpoint has a retryable failure such as a 5xx response, network error, or timeout, Scrip retries with exponential backoff: After 8 attempts (~10.5 hours), the delivery is marked FAILED. You can manually resend any terminal delivery to send the event again.

Response handling

Return a 2xx quickly (within 30 seconds). Process the payload asynchronously if your handler needs more time. Scrip times out each attempt after 30 seconds.

Delivery error codes

Failed attempts include a stable error_code alongside the human-readable last_error. Use error_code for alerts and dashboards.

Manage an endpoint

Disable and re-enable

Temporarily stop deliveries without deleting the endpoint:
Disabling an endpoint immediately fails queued or in-flight deliveries for that endpoint with error_code: "endpoint_disabled". Set status back to ACTIVE to receive future matching events. Events that occurred while the endpoint was disabled are not retroactively delivered. If you disabled an endpoint temporarily and still want to deliver specific failed events, re-enable the endpoint and resend the terminal delivery records.

Rotate secret

If a secret is compromised, rotate it immediately:
The old secret is invalidated immediately. Update your verification code with the new secret before any in-flight deliveries arrive.

Delete

Deleting an endpoint archives it. It stops receiving deliveries and is removed from list results:

Check endpoint health

Scrip monitors delivery health per endpoint. A bad destination can be paused temporarily, then auto-disabled if it keeps failing or stops draining its backlog. Use delivery stats to see whether an endpoint is healthy, paused, disabled, or building a backlog:
Each row includes the 24-hour delivery rollup (success_24h, fail_24h, success_rate, degraded) plus current backlog and block fields: pending_count, oldest_pending_at, circuit_broken_until, rate_limited_until, blocked_reason, and blocked_until. To re-enable an endpoint after resolving the underlying issue:
HTTP 429 responses do not count toward the failure-rate breaker. However, if rate limiting creates a sustained, non-draining backlog, Scrip can auto-disable the endpoint with error_code: "endpoint_backlog_exceeded". Re-enable the endpoint and resend any failed deliveries you still need.

Recover a delivery

List deliveries for an endpoint

Inspect a delivery

The detail endpoint includes last_response_status, last_response_body (truncated to 4 KB), last_error, error_code, and resend_seq:

Resend a delivery

Resend issues a new delivery of the same event to the same endpoint: a fresh delivery with its own id and an incremented resend_seq. The source delivery is left untouched, so you keep a complete history of every attempt.
The response is the newly created delivery (status PENDING), which you can poll like any other. Use resend to replay a webhook after fixing a bug in your handler, or to confirm your endpoint deduplicates correctly.

If a request fails

Delivery guarantees

Webhook events are created with their domain operations. If the underlying transaction rolls back, no webhook is emitted. Each webhook event is recorded under an idempotency key, so a retried operation such as event reprocessing never produces a second event. The key is type:resource_id, where the resource is the one the event leads with: the journal entry for a movement event, the redemption for a redemption status event, the reversal for redemption.reversed and event.reversed, the fulfillment for the two fulfillment events and fulfillment_account.funds_low, the event for event.completed and event.failed, and the participant for participant.created. Three event types can fire more than once for the same resource, so their keys also name the transition. Delivery is at-least-once: a single event may be delivered more than once if your endpoint returns a 2xx but the acknowledgment is lost in transit. Design your handler to be idempotent using the envelope’s id field to detect duplicates.

Event processing

When events complete or fail, and what to poll.

Redemption lifecycle

redemption.pending, capture, and timeout events.

Testing

Replay deliveries against a development endpoint.