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_7Fq2XmR9vTb4NcZ1yWd6Ke" \
-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 | The API credential whose signing secret signs deliveries -- the same key used for HMAC request signing. If omitted, the webhook is linked to the API key you call with. The credential must be active. |
token | string | No | Optional legacy token, sent back in the X-STARSHIP-WEBHOOK-TOKEN header. It is not the signing key -- never use it to compute or verify X-Signature. |
Response:
{
"id": 1,
"name": "Order Updates",
"url": "https://your-endpoint.example.com/webhooks",
"token": "",
"is_active": true,
"events": ["order.created", "order.pending", "order.delivered", "order.partially_delivered", "order.cancelled"],
"credential_id": 1,
"created_at": "2026-01-19T10:00:00Z",
"message": "Webhook created successfully. X-Signature is signed with the linked API key's signing secret (reveal or rotate it under API keys)."
}Unified signing secret: Every webhook is linked to an API credential (credential_id in the response), and X-Signature is signed with that credential's signing secret -- the same secret used for HMAC request signing. One secret, two purposes -- see Authentication Overview. The token field only echoes the optional token you sent; it is not the signing secret.
The signing secret is not returned by this call. In the Client Portal, open API keys to reveal it (requires your password) or rotate it. If the API key you call with has no signing secret yet, one is generated when the webhook is created. A credential_id you pass explicitly must already have one.
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_7Fq2XmR9vTb4NcZ1yWd6Ke" \
-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 | Legacy webhook token, kept for backward compatibility. Not sent when the webhook has no token, or when the token would equal your signing secret. Never use this value as an HMAC key. | 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, signing_secret)signing_secret is the signing secret of the API credential the webhook is linked to. Verify X-Signature with that secret only -- never with the X-STARSHIP-WEBHOOK-TOKEN header value, which travels in clear and is not the signing key.
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="your-credential-signing-secret"
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_7Fq2XmR9vTb4NcZ1yWd6Ke" \
-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_7Fq2XmR9vTb4NcZ1yWd6Ke" \
-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_7Fq2XmR9vTb4NcZ1yWd6Ke" \
-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_7Fq2XmR9vTb4NcZ1yWd6Ke" \
-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_7Fq2XmR9vTb4NcZ1yWd6Ke" \
-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_7Fq2XmR9vTb4NcZ1yWd6Ke" \
-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='your-credential-signing-secret'
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 | Wrong key, or payload was modified before verification | Verify you are signing timestamp + "." + raw_body (not parsed JSON). Use the signing secret of the webhook's linked API credential (Client Portal > API keys), not the X-STARSHIP-WEBHOOK-TOKEN value. If the secret was rotated, use the new one. |
X-STARSHIP-WEBHOOK-TOKEN missing | The webhook has no token, or the token equals your signing secret, so it is not sent | Expected. Verify X-Signature with your signing secret instead of checking the header. |
| No deliveries arriving | The linked API credential is inactive | Deliveries are never signed with the token instead. Reactivate the credential, or link the webhook to an active one with credential_id. |
| 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
Every webhook is signed with the signing secret of the API credential it is linked to -- the same key used for HMAC request signing. Pass credential_id to choose the credential; if you omit it, the webhook is linked to the API key you call with. 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)
- The token is not a key:
X-STARSHIP-WEBHOOK-TOKENis a legacy header, is not sent when it would equal your signing secret, and must never be used to compute or verifyX-Signature
A webhook linked to an inactive credential is not delivered -- deliveries are never signed with the token instead.
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