Outbound Webhooks
Receive HTTP POSTs from Storra when events happen so your systems stay in sync.
Outbound webhooks let Storra push events to your systems in real-time. Use them to:
- Sync orders into your accounting / CRM (QuickBooks, HubSpot)
- Post purchase notifications to a Discord channel or Slack workspace
- Trigger Zapier / Make / n8n automations
- Update internal databases when refunds happen
- Track player attribution / conversion events
Enable Developer mode
Webhooks live behind the Storra Developer app:
- Go to Apps.
- Find Storra Developer and click Install (or Enable if previously installed).
- A new Developer nav group appears in the sidebar with Webhooks, API Keys, and Logs.
Create a webhook
- Go to Webhooks and click New webhook.
- Endpoint URL: a publicly-accessible HTTPS URL on your server (e.g.
https://api.your-server.com/storra-webhook). HTTP is rejected — must be HTTPS. - Description: a short label so you remember what this is for.
- Events: tick the event types you want delivered. Subscribe only to what you actually need — every event you don't subscribe to is one fewer request hitting your server.
- Click Create. Storra generates a signing secret and shows it once. Copy it into your server's environment variables (
STORRA_WEBHOOK_SECRET).
Event types
| Event | When it fires |
|---|---|
customer.created | First time a unique email buys from your store |
purchase.completed | Order paid + recorded; fires after payment confirmation but before deliveries |
purchase.refunded | Refund processed (check data.partial for partial refunds) |
delivery.success | A deliverable (game command / Discord role / file) completed |
delivery.failed | A deliverable dead-lettered after retry exhaustion |
basket.abandoned | Basket sat untouched for 24h with at least one item |
subscription.created | Future Recurring subscription started |
subscription.canceled | Future Subscription canceled |
chargeback.created | Stripe / PayPal raised a dispute |
Request format
Every webhook is a POST with JSON body and these headers:
POST /your-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Storra-Webhooks/1.0
X-Storra-Event: purchase.completed
X-Storra-Event-Id: evt_01HXYZ...
X-Storra-Signature: 2f7a... (64 hex chars)
X-Storra-Timestamp: 1745678900
Body envelope:
{
"id": "evt_01HXYZ...",
"type": "purchase.completed",
"created_at": "2026-04-28T10:30:00Z",
"project_id": "proj_abc",
"data": {
"order_id": "order_xyz",
"customer_email": "buyer@example.com",
"amount_cents": 999,
"currency": "USD",
"items": [...]
}
}
Verifying signatures (REQUIRED)
Always verify the X-Storra-Signature header before trusting a payload. Skipping verification means anyone who guesses your endpoint URL can forge fake events into your system.
Node.js
import crypto from "node:crypto";
import express from "express";
const app = express();
// Important: capture the raw body BEFORE JSON parsing
app.post("/storra-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-storra-signature"];
const timestamp = req.headers["x-storra-timestamp"];
// Reject events older than 5 minutes (replay protection)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).send("timestamp out of range");
}
const expected = crypto
.createHmac("sha256", process.env.STORRA_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest("hex");
if (!crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex"),
)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body);
handleEvent(event);
return res.status(200).send("ok");
}
);
Python
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["STORRA_WEBHOOK_SECRET"].encode()
@app.post("/storra-webhook")
def webhook():
sig = request.headers.get("X-Storra-Signature", "")
ts = request.headers.get("X-Storra-Timestamp", "0")
if abs(time.time() - int(ts)) > 300:
abort(400)
expected = hmac.new(
SECRET,
f"{ts}.{request.data.decode()}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
handle_event(request.json)
return "ok"
crypto.timingSafeEqual / hmac.compare_digest, never ===. Naive string comparison leaks the secret one byte at a time via timing side-channel.Retries
If your endpoint returns non-2xx (or doesn't respond within 15 seconds), Storra retries with exponential backoff:
| Attempt | Delay after previous |
|---|---|
| 1 (initial) | — |
| 2 | 30 seconds |
| 3 | 1 minute |
| 4 | 2 minutes |
| 5 | 5 minutes |
| 6 | 10 minutes |
| 7 | 30 minutes |
| 8 (final) | 1 hour |
After 8 attempts (~ 1h 50m total), the event is dead-lettered and visible in the dashboard's webhook logs with a "Failed" badge. You can manually replay dead-lettered events from the log.
Idempotency
Make your handler idempotent — the same event might be delivered more than once (network glitches, retry races, or you manually replay from logs). Use X-Storra-Event-Id as a deduplication key:
const event = JSON.parse(req.body);
const seen = await db.eventLog.findUnique({ where: { id: event.id } });
if (seen) return res.status(200).send("already processed");
await db.eventLog.create({ data: { id: event.id } });
await processEvent(event);
res.status(200).send("ok");
Delivery logs
Every attempt is logged in Webhooks → Logs:
- HTTP method + URL
- Request body (full payload)
- HTTP status returned by your endpoint
- Response time
- Response body (first 1 KB, for debugging)
- Attempt number + next-retry timestamp
Logs retain for 30 days. Filter by webhook, event type, or status to investigate delivery issues.
Local development
Storra can't deliver to localhost. For local dev, tunnel your local server with one of:
- ngrok —
ngrok http 3000gives you an HTTPS URL pointing atlocalhost:3000 - Cloudflare Tunnel —
cloudflared tunnel --url http://localhost:3000 - Tailscale Funnel — for tailnet-internal exposure
Use the tunnel URL as your webhook endpoint while developing.
FAQ
Can I subscribe one webhook to all events?
Yes — tick all event types. Most teams do this for one webhook (sync everything to internal data warehouse) and create separate, narrow webhooks for specific automations (only purchase.completed for the "post to Discord" webhook).
What if my endpoint is slow?
15-second timeout. If your handler does heavy work, ack immediately (return 200) and process asynchronously via a background queue.
Can I see the signing secret again?
No — Storra only stores the hash. Lost it? Rotate via Webhooks → Settings → Rotate signing secret; the old secret is invalidated immediately.
Does the order of events matter?
Storra delivers events in the order they were generated, but your retries might land out of order if some events fail and retry while later ones succeed. If ordering matters for your handler, use the created_at timestamp + idempotency check to handle out-of-order deliveries gracefully.
Updated recently