Creating an Endpoint
Register a URL and specify which events you want to receive:secret starting with whsec_. Store it immediately. It cannot be retrieved later. You’ll use it to verify signatures.
"enabled_events": ["*"] to subscribe to all event types.
Event Types
| Event | Description |
|---|---|
balance.credited | Balance increased via rule action, API credit, or refund |
balance.debited | Balance decreased via rule action or API debit |
balance.expired | Lots expired and auto-forfeited |
balance.held | Balance moved from AVAILABLE to HELD |
balance.released | Balance moved from HELD back to AVAILABLE |
redemption.completed | Amount or catalog item redeemed |
redemption.reversed | Redemption fully or partially reversed |
transfer.completed | Transfer between participants or groups completed |
event.completed | Event processing succeeded (all rules evaluated) |
event.failed | Event processing or validation failed |
participant.created | New participant enrolled |
participant.tier_changed | Tier level upgraded, downgraded, or removed |
program.funded | Program wallet funded |
program.burned | Program wallet balance burned |
Payload Format
Every delivery sends a JSON envelope:| Field | Description |
|---|---|
id | Webhook event ID (UUID). Same across all endpoints receiving this event. |
type | The event type string. |
api_version | API version at time of emission. Currently 2026-03-01. |
created_at | When the event was created (RFC 3339). |
organization_id | Organization that owns the event. |
data | Event-specific payload. |
Event Payloads
balance.credited / balance.debited
When the target is a participant:
Exactly one of
participant_id or group_id is present, depending on whether the target is a participant or a group.reference_id is present when the credit or settle was correlated to a hold. settle is present only for settle operations (credit with reference_id to AVAILABLE) and contains held_amount (total previously held) and delta (settle minus held; positive = over-capture, negative = under-capture).
balance.expired
When the target is a participant:
Exactly one of
participant_id or group_id is present, depending on whether the target is a participant or a group.balance.held / balance.released
When the target is a participant:
Exactly one of
participant_id or group_id is present, depending on whether the target is a participant or a group.transfer.completed
When the source is a participant:
The source uses exactly one of
source_participant_id or source_group_id. Each entry in recipients contains exactly one of participant_id or group_id, along with the amount transferred to that recipient. Source and recipient types may differ (e.g., a participant can transfer to a group).redemption.completed (amount redemption)
redemption.completed (catalog item redemption)
redemption.reversed
participant.created
participant.tier_changed
When the target is a participant:
level is null when a tier is removed (downgrade to base level).
Exactly one of
participant_id, group_id, or program_id is present, depending on the target of the tier change. The SET_TIER rule action supports cross-targeting via the target field, so consumers should not assume this is always a participant.program.funded / program.burned
event.completed
event.failed
Signature Verification
Every delivery includes aScrip-Signature header so you can verify it came from Scrip and wasn’t tampered with.
Header Format
| Component | Description |
|---|---|
t | Unix timestamp (seconds) when the signature was generated |
v1 | Hex-encoded HMAC-SHA256 signature |
Verification Steps
Construct signed payload
Concatenate the timestamp, a literal dot, and the raw request body:
{t}.{raw_body}Compute expected signature
Calculate
HMAC-SHA256(your_endpoint_secret, signed_payload) and hex-encode the result.Example (Python)
Example (Node.js)
Retry Policy
If your endpoint doesn’t return a 2xx response, Scrip retries with exponential backoff:| Attempt | Delay | Cumulative |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 5 min | 5 min |
| 3 | 10 min | 15 min |
| 4 | 20 min | 35 min |
| 5 | 40 min | 1h 15m |
| 6 | 80 min | 2h 35m |
| 7 | 160 min | 5h 15m |
| 8 | 320 min | 10h 35m |
FAILED. You can manually retry failed deliveries via the API.
Response Handling
| Your Response | Scrip’s Behavior |
|---|---|
| 2xx | Marked DELIVERED |
| 429 (rate limited) | Retried on the backoff schedule. Does not count toward the endpoint’s failure rate. Rate-limited endpoints are retried without risk of auto-disable. |
| 4xx (except 429) | Marked FAILED immediately. No retry. |
| 5xx | Retried on the backoff schedule |
| Network error / timeout | Retried on the backoff schedule |
Return a 2xx quickly (within 30 seconds). Process the payload asynchronously if your handler needs more time. The worker enforces a 30-second timeout per delivery attempt.
Managing Endpoints
Disable and Re-enable
Temporarily stop deliveries without deleting the endpoint:status back to ACTIVE to resume. Events that occurred while disabled are not retroactively delivered.
Rotate Secret
If a secret is compromised, rotate it immediately:Delete
Deleting an endpoint archives it. It stops receiving deliveries and is removed from list results:Endpoint Health
Scrip monitors delivery success rates per endpoint. Endpoints with sustained delivery failures are automatically set toDISABLED. Pending deliveries for a disabled endpoint are marked FAILED.
To re-enable an endpoint after resolving the underlying issue:
429 responses do not count toward the failure rate. Rate-limited endpoints are retried on the normal backoff schedule without risk of auto-disable.
Debugging Deliveries
List Deliveries for an Endpoint
Inspect a Delivery
The detail endpoint includeslast_response_status, last_response_body (truncated to 4 KB), and last_error:
Retry a Failed Delivery
Reset aFAILED delivery back to PENDING for a fresh set of 8 attempts:
Best Practices
| Practice | Rationale |
|---|---|
| Verify signatures on every request | Prevents spoofed deliveries |
| Return 2xx quickly, process async | Avoids timeouts and unnecessary retries |
Use * sparingly | Subscribe only to events you need to reduce noise |
| Handle duplicates idempotently | Network retries can deliver the same event more than once. Use id to deduplicate. |
| Monitor failed deliveries | Check delivery status periodically or alert on consecutive failures |
Delivery Guarantees
Webhook events are created atomically with their domain operations using an outbox pattern. If the underlying transaction rolls back, no webhook is emitted. Each event is deduplicated by an internal idempotency key, so retried domain operations (like event reprocessing) don’t produce duplicate webhooks. 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’sid field.