Implement a webhook receiver
Osigu delivers async outcomes — sponsor decisions, dispensation completions, prescription email deliveries — as webhook events to an HTTPS endpoint your team owns. This guide walks the receiver end-to-end: signature verification, dedupe, and returning quickly.
Concept refresher: Webhooks. Full event catalogue: Webhook events.
Prerequisites
- An HTTPS-reachable endpoint — Osigu will not deliver to plain HTTP.
- A webhook signing secret from Osigu (send
webhooks@osigu.comthe URL you want events delivered to and we'll issue you one). - A request-body persistence you can query on retries (postgres, redis, whatever).
1. Register the endpoint
Once onboarding sends you the signing secret, register your endpoint URL:
curl -X POST "https://sandbox.osigu.com/webhooks/v1/subscriptions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.myprovider.com/osigu/webhooks",
"event_types": ["authorization.*", "dispensing.*"]
}'
event_types accepts glob-style patterns. Use ["*"] to subscribe to everything.
2. Verify the signature
Every delivery carries:
X-Osigu-Signature: t=1751385600,v1=8b7c…hex_hmac_sha256
Rules:
- Compute
HMAC-SHA256(secret, "${t}.${raw_body}")wheretis the value from the header andraw_bodyis the exact bytes of the request body. - Compare hex-encoded HMAC to the
v1=value with a constant-time compare. - Reject if the
ttimestamp is more than 5 minutes off from now.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.OSIGU_WEBHOOK_SECRET;
app.post(
'/osigu/webhooks',
express.raw({ type: 'application/json' }),
(req, res) => {
const header = req.get('X-Osigu-Signature') ?? '';
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=')),
);
const t = Number(parts.t);
const v1 = parts.v1;
if (!t || Math.abs(Date.now() / 1000 - t) > 300) {
return res.status(401).send('timestamp');
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${t}.${req.body.toString()}`)
.digest('hex');
if (
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1 ?? ''))
) {
return res.status(401).send('signature');
}
const event = JSON.parse(req.body.toString());
// ... dedupe on event.event_id, enqueue work, respond ...
return res.status(200).send('ok');
},
);
Python (FastAPI)
import hmac, hashlib, os, time
from fastapi import FastAPI, Request, HTTPException
SECRET = os.environ["OSIGU_WEBHOOK_SECRET"].encode()
app = FastAPI()
@app.post("/osigu/webhooks")
async def webhook(req: Request):
raw = await req.body()
header = req.headers.get("X-Osigu-Signature", "")
parts = dict(kv.split("=") for kv in header.split(","))
t = int(parts.get("t", 0))
v1 = parts.get("v1", "")
if abs(time.time() - t) > 300:
raise HTTPException(401, "timestamp")
expected = hmac.new(SECRET, f"{t}.{raw.decode()}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
raise HTTPException(401, "signature")
# ... dedupe, enqueue, return ...
return {"ok": True}
Go
func webhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
parts := map[string]string{}
for _, kv := range strings.Split(r.Header.Get("X-Osigu-Signature"), ",") {
p := strings.SplitN(kv, "=", 2)
if len(p) == 2 { parts[p[0]] = p[1] }
}
t, _ := strconv.ParseInt(parts["t"], 10, 64)
if math.Abs(float64(time.Now().Unix()-t)) > 300 {
http.Error(w, "timestamp", 401); return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("OSIGU_WEBHOOK_SECRET")))
fmt.Fprintf(mac, "%d.%s", t, body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(parts["v1"])) {
http.Error(w, "signature", 401); return
}
// dedupe, enqueue, return
w.WriteHeader(200)
}
3. Dedupe on event_id
The same event can arrive more than once. Persist processed event_ids (with a TTL of ~7 days) and short-circuit on duplicates:
-- On receive:
INSERT INTO webhook_events (event_id, received_at) VALUES ($1, now())
ON CONFLICT (event_id) DO NOTHING RETURNING event_id;
-- If no row returned, it's a duplicate — return 200 immediately, don't reprocess.
4. Respond fast, work async
Return 200 in under 10 seconds — ideally under 1 second. Do the heavy work off the request thread (background job, message queue). If Osigu times out, we'll retry, which means duplicates for you to dedupe.
A minimal pattern:
POST → verify → dedupe → enqueue job → return 200
5. Retry behaviour
Osigu retries on any non-2xx for 72 hours with exponential backoff:
- 1st retry ~1 min later
- 2nd retry ~5 min later
- Then progressively longer intervals up to ~4h
- After 72h of failures, the event is abandoned and an alert fires on Osigu's side. You'll be contacted.
6. Testing locally
Use ngrok (or Cloudflare Tunnel / tailscale funnel) to expose your dev machine on HTTPS:
ngrok http 3000
Register the ngrok URL as your sandbox webhook subscription. Trigger a sandbox event (a completed dispensation, an OTP-validated auth) and confirm you get the callback.
Related
- Concept: Webhooks.
- Webhook events — full catalogue of
event_typevalues and payload shapes.