Starship Rewards API
Webhooks

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?

BenefitDescription
Real-timeInstant notifications -- no polling required
EfficientReduces API calls and server load
ReliableAutomatic retries with exponential backoff
SecureHMAC-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:

EnvironmentBase URL
Sandboxhttps://api-playground.starshiprewards.com
Productionhttps://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

FieldTypeRequiredDescription
namestringYesA friendly name for this webhook
urlstringYesYour HTTPS endpoint URL
eventsstring[]YesEvent types to subscribe to
credential_idintegerNoThe 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.
tokenstringNoOptional 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

EventTriggerDescription
order.createdOrder submittedA new order has been created and payment deducted from the wallet
order.pendingAwaiting fulfillmentThe order is queued for processing and voucher allocation
order.deliveredVouchers deliveredAll vouchers are ready for delivery to end-users
order.partially_deliveredPartial fulfillmentSome items delivered, others still pending
order.cancelledOrder cancelledThe 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:

HeaderDescriptionExample
Content-TypeAlways application/jsonapplication/json
User-AgentIdentifies the senderstarshiprewards.com/webhook-agent
X-Webhook-IDYour webhook configuration ID42
X-Event-IDUnique identifier for this eventevt_2kT7xR9vBqMf4Np1
X-Event-TypeThe event typeorder.delivered
X-TimestampUnix timestamp (seconds)1708800000
X-SignatureHMAC-SHA256 hex digesta3f2b8c9d1e4...
X-Request-IDUnique ID for this delivery attempt (useful for debugging with support)whr_2kT7xR9vBqMf4Np1
X-STARSHIP-WEBHOOK-TOKENLegacy 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"
fi

Retry Policy

Failed deliveries are retried automatically with exponential backoff:

AttemptDelayTotal Elapsed
1Immediate0
21 minute1 min
32 minutes3 min
44 minutes7 min
58 minutes15 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

MethodEndpointDescription
GET/api/v1/webhooksList all webhooks
POST/api/v1/webhooksCreate a webhook
GET/api/v1/webhooks/:idGet webhook details
PUT/api/v1/webhooks/:idUpdate a webhook
DELETE/api/v1/webhooks/:idDelete a webhook
POST/api/v1/webhooks/:id/testSend 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

MethodEndpointDescription
GET/api/v1/webhooks/deliveriesList all deliveries (paginated)
GET/api/v1/webhooks/deliveries/statsDelivery statistics
GET/api/v1/webhooks/deliveries/:idGet delivery details
POST/api/v1/webhooks/deliveries/:id/retryRetry 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:

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page (max 100)
statusstring—Filter by status: pending, delivered, failed
event_typestring—Filter by event type, e.g. order.delivered
order_idinteger—Filter by order ID
webhook_url_idinteger—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

PracticeDetails
Always verify signaturesNever process a webhook without validating the HMAC-SHA256 signature
Respond within 30 secondsReturn HTTP 200 immediately, then process the event asynchronously
Implement idempotencyStore processed event_id values and skip duplicates -- retries may deliver the same event more than once
Use HTTPSWebhook URLs must use HTTPS. Plain HTTP endpoints are rejected
Check timestampsReject requests with X-Timestamp older than 5 minutes to prevent replay attacks
Handle all subscribed eventsReturn 200 even for event types you do not act on, to avoid unnecessary retries

Troubleshooting

SymptomLikely CauseResolution
Signature mismatchWrong key, or payload was modified before verificationVerify 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 missingThe webhook has no token, or the token equals your signing secret, so it is not sentExpected. Verify X-Signature with your signing secret instead of checking the header.
No deliveries arrivingThe linked API credential is inactiveDeliveries are never signed with the token instead. Reactivate the credential, or link the webhook to an active one with credential_id.
Timestamp rejectedClock drift between your server and oursEnsure your server uses NTP. The tolerance window is 5 minutes.
Repeated retriesYour endpoint returns a non-2xx status or times outRespond with HTTP 200 before doing any heavy processing
Duplicate eventsNormal behavior during retriesImplement idempotency by tracking event_id
Missing eventsWebhook not subscribed to the event typeCheck your webhook configuration and update the events list
SSL/TLS errorsInvalid or expired certificateUse a valid certificate from a trusted CA

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-secret and 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-TOKEN is a legacy header, is not sent when it would equal your signing secret, and must never be used to compute or verify X-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