If you’ve followed the rest of this guide and something still isn’t working, this is the page. It’s organized by symptom — find what you’re seeing in the headings below, follow the fix. Each section is self-contained, so you can land here from a search result and still get what you need. If your symptom isn’t listed, jump to Still stuck? at the bottom for what to send us so we can trace it on our side.Documentation Index
Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
Use this file to discover all available pages before exploring further.
”Every request fails signature verification”
By far the most common issue, and almost always the same root cause: the body you hash isn’t the body we hashed.Diagnosis
Add a temporary log on your server:| Observation | Likely cause |
|---|---|
rawBodyLength === 0 | You called req.json() first, which consumed the stream. Read the raw body first. |
rawBodyFirst80 starts with { "event": (with extra spaces) | Your framework parsed and re-serialized. Capture raw bytes, not a parsed object. |
signatureLen !== 67 | Header truncated or a proxy is mangling it. Should be v0= + 64 hex chars. |
timestamp === undefined | Reverse proxy is stripping X-Spectrum-* headers. Add them to your allow-list. |
Fix by framework
| Framework | Trick |
|---|---|
| Express | Use express.raw({ type: 'application/json' }) instead of express.json() |
| FastAPI | await request.body() and don’t type the parameter as a Pydantic model |
| Hono | await c.req.text() before c.req.json() |
| Next.js Route Handler | await req.text() and parse with JSON.parse(text) |
| Cloudflare Workers | await request.text() |
”Most requests verify but some sporadically fail”
Two likely culprits:- Server clock skew. If your server’s clock drifts more than ~5 minutes from real UTC, every delivery looks “stale” and your timestamp check rejects it. Run
ntpd/chronydand confirm withdate -u. - You’re loading the wrong secret in some environments. Log
SECRET.length === 64on startup — if it’s ever false, you’ve loaded an env var from the wrong source.
”I never receive anything”
Walk through this checklist in order:Confirm the webhook is registered
Confirm the URL is reachable from the public internet
Confirm the platform is enabled and connected
Confirm the message is actually inbound to your project
Send the test message from a phone number that isn’t your own to the line attached to the project.
”I receive duplicates”
This is expected behavior under at-least-once delivery. The two scenarios that cause it:- Your handler succeeded but timed out before responding. We retried, you processed twice.
- Your handler returned
5xxafter partially processing. We retried, you re-ran the partial work.
Fix
Dedupe at the top of your handler usingX-Spectrum-Webhook-Id plus payload.message.id as a composite key:
”Deliveries time out”
If you’re seeing your endpoint logged as “took >10s,” it triggers a retry on our side and a likely duplicate processing on yours.Diagnosis
Look at what your handler is doing synchronously:Fix
Acknowledge first, process asynchronously:”ngrok URL keeps changing”
Free ngrok tunnels get a new URL every restart. That URL won’t be registered with us, so deliveries 404 immediately.Fix
-
For local development, kill the old webhook and re-register every time you restart ngrok:
- Better: use a reserved subdomain (paid plan) so the URL is stable across restarts.
- Best for long-lived dev: deploy a tiny forwarder (Cloudflare Worker, Vercel function) that POSTs to your local machine over a stable tunnel.
”The signing secret leaked”
Treat it like any other credential leak.- Rotate the secret using the delete-and-recreate flow in Managing webhooks.
- If you suspect the project credentials also leaked, rotate them too:
photon projects regenerate-secret <id>. - Audit recent inbound events for anomalies (events for spaces you don’t recognize, suspicious sender ids, payloads with unusual content).
”I want to test verification without spamming real messages”
Build a test request locally that mimics a real delivery:test-verify.ts
bun test-verify.ts), copy the printed curl, and paste it to test your verifier offline.
”How do I see what we sent you?”
We don’t currently expose a delivery log to customers. If you need to debug a specific event, the fastest path is:- Reach out to support with the webhook id, the approximate UTC timestamp, and what you observed on your side.
- We can confirm whether the delivery left our worker, what status code came back, and how many retries it took.
”Multiple webhook URLs receive the event in different orders”
That’s expected. Deliveries to multiple URLs run in parallel — there’s no ordering guarantee across URLs. If your downstream consumers need to coordinate, designate one URL as the primary and have it fan out internally rather than registering several with us.Still stuck?
Send us:- Your project id (the one in
photon projects show). - The webhook id or the URL.
- A timestamp range (UTC).
- One example body and signature you couldn’t verify (with the secret redacted).