Webhooks
Receive real-time notifications for order status changes
Webhooks
Webhooks deliver real-time HTTP POST notifications to your server when events occur in the Starship platform. Instead of polling the API for updates, your application receives callbacks immediately when an order is fulfilled or its status changes.
Why Use Webhooks?
| Benefit | Description |
|---|---|
| Real-time | Instant notifications -- no polling required |
| Efficient | Reduces API calls and server load |
| Reliable | Automatic retries with exponential backoff |
| Secure | HMAC-SHA256 signed payloads with timestamp binding |
Base URL
All API examples below use {{host}} as a placeholder. Replace it with your environment's base URL:
| Environment | Base URL |
|---|---|
| Sandbox | https://api-playground.starshiprewards.com |
| Production | https://api.starshiprewards.com |
Quick Start
1. Create Your Webhook Endpoint
Set up an HTTPS endpoint on your server that can receive POST requests:
# Verify your endpoint is reachable
curl -X POST https://your-endpoint.example.com/webhooks \
-H "Content-Type: application/json" \
-d '{"test": true}'2. Register Your Webhook
Use the API to register your endpoint and subscribe to the events you need:
curl -X POST "{{host}}/api/v1/webhooks" \
-H "X-API-Key: sk_live_abc123def456" \
-H "X-API-Secret: your-secret-here" \
-H "Content-Type: application/json" \
-d '{
"name": "Order Updates",
"url": "https://your-endpoint.example.com/webhooks",
"events": ["order.created", "order.pending", "order.delivered", "order.partially_delivered", "order.cancelled"],
"credential_id": 1
}'Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | A friendly name for this webhook |
url | string | Yes | Your HTTPS endpoint URL |
events | string[] | Yes | Event types to subscribe to |
credential_id | integer | No | Links this webhook to an API credential. When set, webhook deliveries are signed with the credential's signing secret -- the same key used for HMAC request signing. |
Response:
{
"id": 1,
"name": "Order Updates",
"url": "https://your-endpoint.example.com/webhooks",
"token": "whsec_a1b2c3d4e5f6g7h8i9j0...",
"events": ["order.created", "order.pending", "order.delivered", "order.partially_delivered", "order.cancelled"],
"is_active": true,
"created_at": "2026-01-19T10:00:00Z"
}Unified signing secret: When you provide credential_id, the token in the response is the credential's signing secret. This is the same secret used for HMAC request signing on that credential. One secret, two purposes -- see Authentication Overview.
Save the token value immediately. It is only returned once at creation time and is required for verifying webhook signatures. If lost, you can rotate the signing secret via the Client Portal.
3. Send a Test Event
Trigger a test delivery to confirm your endpoint is working:
curl -X POST "{{host}}/api/v1/webhooks/1/test" \
-H "X-API-Key: sk_live_abc123def456" \
-H "X-API-Secret: your-secret-here"Available Events
Order Events
| Event | Trigger | Description |
|---|---|---|
order.created | Order submitted | A new order has been created and payment deducted from the wallet |
order.pending | Awaiting fulfillment | The order is queued for processing and voucher allocation |
order.delivered | Vouchers delivered | All vouchers are ready for delivery to end-users |
order.partially_delivered | Partial fulfillment | Some items delivered, others still pending |
order.cancelled | Order cancelled | The order has been cancelled and a refund issued to the wallet |
Payload Structure
Event Envelope
Every webhook delivery wraps the event-specific data in a standard envelope:
{
"event_id": "evt_2kT7xR9vBqMf4Np1",
"event_type": "order.delivered",
"created_at": "2026-01-19T10:30:00Z",
"data": {
// Event-specific payload (see below)
}
}Order Created Example
{
"event_id": "evt_8mN3pQ7wKxYb2Rt5",
"event_type": "order.created",
"created_at": "2026-01-19T10:29:55Z",
"data": {
"id": 456789,
"reference_code": "STR-2026-456789",
"status": "PENDING",
"sub_status": "",
"product_name": "Gift Card",
"denomination": "100.00",
"quantity": 5,
"amount": "485.00",
"delivered_quantity": 0,
"delivered_on": null,
"created_at": "2026-01-19T10:29:55Z"
}
}Order Delivered Example
{
"event_id": "evt_2kT7xR9vBqMf4Np1",
"event_type": "order.delivered",
"created_at": "2026-01-19T10:30:05Z",
"data": {
"id": 456789,
"reference_code": "STR-2026-456789",
"status": "DELIVERED",
"sub_status": "",
"product_name": "Gift Card",
"denomination": "100.00",
"quantity": 5,
"amount": "485.00",
"delivered_quantity": 5,
"delivered_on": "2026-01-19T10:30:05Z",
"created_at": "2026-01-19T10:30:00Z"
}
}Order Cancelled Example
{
"event_id": "evt_5jL9rW4tHzCe1Qm8",
"event_type": "order.cancelled",
"created_at": "2026-01-19T11:15:00Z",
"data": {
"id": 456789,
"reference_code": "STR-2026-456789",
"status": "CANCELLED",
"sub_status": "",
"product_name": "Gift Card",
"denomination": "100.00",
"quantity": 5,
"amount": "485.00",
"delivered_quantity": 0,
"delivered_on": null,
"created_at": "2026-01-19T10:29:55Z"
}
}HTTP Headers
Every webhook request includes the following headers:
| Header | Description | Example |
|---|---|---|
Content-Type | Always application/json | application/json |
User-Agent | Identifies the sender | starshiprewards.com/webhook-agent |
X-Webhook-ID | Your webhook configuration ID | 42 |
X-Event-ID | Unique identifier for this event | evt_2kT7xR9vBqMf4Np1 |
X-Event-Type | The event type | order.delivered |
X-Timestamp | Unix timestamp (seconds) | 1708800000 |
X-Signature | HMAC-SHA256 hex digest | a3f2b8c9d1e4... |
X-Request-ID | Unique ID for this delivery attempt (useful for debugging with support) | whr_2kT7xR9vBqMf4Np1 |
X-STARSHIP-WEBHOOK-TOKEN | Your webhook signing secret (for quick token validation) | whsec_a1b2c3... |
Signature Verification
Always verify webhook signatures before processing any payload. Signature verification protects against forged requests and replay attacks.
How It Works
The signature binds the request timestamp to the payload body:
signed_content = timestamp + "." + raw_request_body
signature = HMAC-SHA256(signed_content, webhook_token)The timestamp value comes from the X-Timestamp header (Unix seconds). Reject any request where the timestamp is more than 5 minutes old to prevent replay attacks.
# Manually verify a signature (useful for debugging)
PAYLOAD='{"event_id":"evt_2kT7xR9vBqMf4Np1","event_type":"order.delivered"}'
SECRET="whsec_a1b2c3d4e5f6g7h8i9j0"
TIMESTAMP="1708800000"
# Compute expected signature: HMAC-SHA256(timestamp + "." + body, secret)
EXPECTED=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | \
openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
echo "Expected: $EXPECTED"
# Compare with the X-Signature header value from the request
RECEIVED="a3f2b8c9d1e4..."
if [ "$EXPECTED" = "$RECEIVED" ]; then
echo "Signature valid"
else
echo "Signature INVALID -- reject this request"
fiRetry Policy
Failed deliveries are retried automatically with exponential backoff:
| Attempt | Delay | Total Elapsed |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 min |
| 3 | 2 minutes | 3 min |
| 4 | 4 minutes | 7 min |
| 5 | 8 minutes | 15 min |
After 5 failed attempts, the delivery is marked as permanently failed. You can view delivery history and redeliver failed events through the API.
What Counts as Success
A delivery is considered successful when your endpoint returns:
- An HTTP status code in the 2xx range (200-299)
- A response within 30 seconds
Any other status code, a connection error, or a timeout triggers a retry.
Managing Webhooks
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/webhooks | List all webhooks |
POST | /api/v1/webhooks | Create a webhook |
GET | /api/v1/webhooks/:id | Get webhook details |
PUT | /api/v1/webhooks/:id | Update a webhook |
DELETE | /api/v1/webhooks/:id | Delete a webhook |
POST | /api/v1/webhooks/:id/test | Send a test event |
Update a Webhook
curl -X PUT "{{host}}/api/v1/webhooks/1" \
-H "X-API-Key: sk_live_abc123def456" \
-H "X-API-Secret: your-secret-here" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Name",
"url": "https://your-endpoint.example.com/webhooks/v2",
"events": ["order.created", "order.pending", "order.delivered"],
"is_active": true
}'Delete a Webhook
curl -X DELETE "{{host}}/api/v1/webhooks/1" \
-H "X-API-Key: sk_live_abc123def456" \
-H "X-API-Secret: your-secret-here"Delivery Management
Monitor webhook delivery status, view delivery details, and retry failed deliveries.
Delivery Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/webhooks/deliveries | List all deliveries (paginated) |
GET | /api/v1/webhooks/deliveries/stats | Delivery statistics |
GET | /api/v1/webhooks/deliveries/:id | Get delivery details |
POST | /api/v1/webhooks/deliveries/:id/retry | Retry a failed delivery |
List Deliveries
Retrieve a paginated list of webhook deliveries. Results are filtered to your account automatically.
curl -X GET "{{host}}/api/v1/webhooks/deliveries?page=1&limit=20&status=failed" \
-H "X-API-Key: sk_live_abc123def456" \
-H "X-API-Secret: your-secret-here"Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Items per page (max 100) |
status | string | — | Filter by status: pending, delivered, failed |
event_type | string | — | Filter by event type, e.g. order.delivered |
order_id | integer | — | Filter by order ID |
webhook_url_id | integer | — | Filter by webhook configuration ID |
Response:
{
"success": true,
"data": {
"deliveries": [
{
"id": 42,
"webhook_url_id": 1,
"order_id": 456789,
"event_type": "order.delivered",
"event_id": "evt_2kT7xR9vBqMf4Np1",
"payload": { "event_type": "order.delivered", "data": { "..." } },
"status": "delivered",
"attempt_count": 1,
"max_attempts": 5,
"response_status": 200,
"response_time_ms": 142,
"delivered_at": "2026-01-19T10:30:06Z",
"created_at": "2026-01-19T10:30:05Z"
}
]
}
}Pagination headers are included in the response: X-Page, X-Total-Count, X-Total-Pages, X-Page-Size.
Get Delivery Details
Retrieve full details for a specific delivery, including the complete payload and response body.
curl -X GET "{{host}}/api/v1/webhooks/deliveries/42" \
-H "X-API-Key: sk_live_abc123def456" \
-H "X-API-Secret: your-secret-here"Retry a Failed Delivery
Re-queue a failed delivery for another attempt. Only deliveries with status: "failed" can be retried. The existing cron job picks up retried deliveries within approximately 1 minute.
curl -X POST "{{host}}/api/v1/webhooks/deliveries/42/retry" \
-H "X-API-Key: sk_live_abc123def456" \
-H "X-API-Secret: your-secret-here"Response:
{
"success": true,
"data": {
"message": "Delivery queued for retry",
"delivery_id": 42
}
}Retries are subject to the same maximum attempt limit (5 attempts). A delivery that has already exhausted all attempts cannot be retried further.
Delivery Statistics
Get aggregate statistics for your webhook deliveries.
curl -X GET "{{host}}/api/v1/webhooks/deliveries/stats" \
-H "X-API-Key: sk_live_abc123def456" \
-H "X-API-Secret: your-secret-here"Response:
{
"success": true,
"data": {
"total_count": 150,
"pending_count": 2,
"delivered_count": 143,
"failed_count": 5,
"last_24h_total": 12,
"last_24h_delivered": 11,
"last_24h_failed": 1
}
}Production Handler Example
A complete, production-ready handler that includes signature verification, idempotency checks, and async processing:
# Simulate a signed webhook delivery for local testing
TIMESTAMP=$(date +%s)
PAYLOAD='{"event_type":"order.delivered","data":{"id":123}}'
SECRET='whsec_a1b2c3d4e5f6g7h8i9j0'
curl -X POST https://your-endpoint.example.com/webhooks \
-H "Content-Type: application/json" \
-H "X-Event-Type: order.delivered" \
-H "X-Event-ID: evt_test_2kT7xR9vBqMf" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: $(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)" \
-d "$PAYLOAD"Best Practices
| Practice | Details |
|---|---|
| Always verify signatures | Never process a webhook without validating the HMAC-SHA256 signature |
| Respond within 30 seconds | Return HTTP 200 immediately, then process the event asynchronously |
| Implement idempotency | Store processed event_id values and skip duplicates -- retries may deliver the same event more than once |
| Use HTTPS | Webhook URLs must use HTTPS. Plain HTTP endpoints are rejected |
| Check timestamps | Reject requests with X-Timestamp older than 5 minutes to prevent replay attacks |
| Handle all subscribed events | Return 200 even for event types you do not act on, to avoid unnecessary retries |
Troubleshooting
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Signature mismatch | Incorrect signing secret, or payload was modified before verification | Verify you are signing timestamp + "." + raw_body (not parsed JSON). Use the token returned at webhook creation. |
| Timestamp rejected | Clock drift between your server and ours | Ensure your server uses NTP. The tolerance window is 5 minutes. |
| Repeated retries | Your endpoint returns a non-2xx status or times out | Respond with HTTP 200 before doing any heavy processing |
| Duplicate events | Normal behavior during retries | Implement idempotency by tracking event_id |
| Missing events | Webhook not subscribed to the event type | Check your webhook configuration and update the events list |
| SSL/TLS errors | Invalid or expired certificate | Use a valid certificate from a trusted CA |
Signing Secret & API Credential Link
When creating a webhook with credential_id, the webhook's signing token is the credential's signing secret -- the same key used for HMAC request signing. This unified design means:
- One secret to manage: Rotate the signing secret via
/client/api/api-keys/:id/rotate-signing-secretand both your API request signatures and webhook verification update at once - Consistent verification: The same HMAC-SHA256 approach secures both inbound requests (your calls to us) and outbound deliveries (our webhooks to you)
- Gradual adoption: Start with basic webhook token verification today. When you enable HMAC request signing later, you're already using the same secret
If you create a webhook without credential_id, a standalone webhook token is generated. This still works for signature verification but is not linked to any API credential's signing secret.
See the full migration path: Authentication Overview -- Migration Path
Related Guides
- Authentication Overview -- API key setup and unified signing secret
- HMAC Request Signing -- Full signing specification
- Security Considerations -- Key rotation and credential management
- Create Order -- Placing orders via the API
- Idempotency -- Safely handling retries and duplicate requests
- Get Order Details -- Polling as an alternative to webhooks