Skip to main content

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.

By now you can receive deliveries, verify them, and survive retries. This page is the operational layer — how you actually create the webhook records, list them, take one offline, or rotate a leaked secret. There are three HTTP endpoints, all under https://spectrum.photon.codes/projects/{projectId}/webhooks/. Every example below uses curl. The same operations are also available through the Webhook tab of the Spectrum dashboard, the API reference, and any HTTP client (Postman, Insomnia, a language SDK, etc.) — see Three ways to manage webhooks on the overview for details.

Authentication

Every request uses HTTP Basic auth where the username is your projectId and the password is your projectSecret. The projectId also appears in the URL path — both are required.
curl -u "$PROJECT_ID:$PROJECT_SECRET" \
  "https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/"
Project credentials are scoped to a single project. They never expire — rotate them via photon projects regenerate-secret <id> (see the CLI projects docs) if they leak.

Register a webhook

curl -X POST "https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/" \
  -u "$PROJECT_ID:$PROJECT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"webhookUrl":"https://your-app.com/spectrum-webhook"}'
Response (200 OK):
{
  "succeed": true,
  "data": {
    "id": "6a4d2e8c-7b1f-4d3a-9a8e-2c5d6f7e8a9b",
    "webhookUrl": "https://your-app.com/spectrum-webhook",
    "signingSecret": "a3f8e29b...5c7e9b2d",
    "createdAt": "2026-05-14T19:00:00Z",
    "updatedAt": "2026-05-14T19:00:00Z"
  }
}
The signingSecret is only returned in this response. There is no GET endpoint that returns it, by design — once it’s gone, it’s gone. Save it to your secrets manager before doing anything else.

Errors

StatusWhen it happensWhat to do
422webhookUrl fails schema validation (empty, malformed, etc.)Send a syntactically valid URL string
409The same URL is already registered for this projectList existing webhooks, or delete the old one and re-register
401Bad project credentialsRotate via the CLI and try again

URL requirements

  • Should be https://. We accept http:// URLs today and don’t reject them at registration, but delivery is then sent in plaintext — anyone on the network path can read the payload and forge requests. Treat HTTPS as a hard requirement for any non-toy webhook.
  • Must be reachable from the public internet — if Spectrum can’t reach it, every delivery fails after the retry budget exhausts.
  • Path component is yours to choose; we POST to it as-is.

List registered webhooks

curl -u "$PROJECT_ID:$PROJECT_SECRET" \
  "https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/"
Response:
{
  "succeed": true,
  "data": [
    {
      "id": "6a4d2e8c-7b1f-4d3a-9a8e-2c5d6f7e8a9b",
      "webhookUrl": "https://your-app.com/spectrum-webhook",
      "createdAt": "2026-05-14T19:00:00Z",
      "updatedAt": "2026-05-14T19:00:00Z"
    },
    {
      "id": "9f1e3d4b-2c8a-4f5d-b7e6-1a2b3c4d5e6f",
      "webhookUrl": "https://staging.your-app.com/spectrum-webhook",
      "createdAt": "2026-05-15T09:30:00Z",
      "updatedAt": "2026-05-15T09:30:00Z"
    }
  ]
}
Webhooks are returned in creation order, oldest first. Soft-deleted webhooks are not included.
The list response does not include signingSecret. It’s only ever returned at creation time. Treat the secret like a password — if you need it back, rotate by deleting and re-registering.

Delete a webhook

curl -X DELETE \
  -u "$PROJECT_ID:$PROJECT_SECRET" \
  "https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/6a4d2e8c-7b1f-4d3a-9a8e-2c5d6f7e8a9b/"
Response:
{ "succeed": true, "data": { "id": "6a4d2e8c-7b1f-4d3a-9a8e-2c5d6f7e8a9b" } }
After this returns 200, no further events are delivered to that URL. A delivery already in flight at the moment of deletion may still complete (the worker won’t abort an in-progress POST). The delete is logical — the row is soft-deleted with a deletedAt timestamp on our side. The id and signingSecret are gone forever; re-registering the same URL produces a brand-new webhook with a new id and a new secret.

Errors

StatusWhen it happens
404The id doesn’t exist or has already been deleted

Rotating the signing secret

There is no dedicated rotation endpoint. To rotate, delete and re-register:
# 1. Capture the old id
OLD_ID=6a4d2e8c-7b1f-4d3a-9a8e-2c5d6f7e8a9b

# 2. Register the same URL — get a new secret
curl -X POST "https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/" \
  -u "$PROJECT_ID:$PROJECT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"webhookUrl":"https://your-app.com/spectrum-webhook"}'
# Save the new signingSecret immediately

# 3. Update your server's SPECTRUM_SIGNING_SECRET env var and redeploy

# 4. Delete the old webhook
curl -X DELETE -u "$PROJECT_ID:$PROJECT_SECRET" \
  "https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/$OLD_ID/"
For a graceful zero-downtime rotation, run your verifier against both the old and new secret for a brief overlap window between steps 2 and 4. Treat a request as valid if either secret verifies it.
const verifyEither = (rawBody, timestamp, signature) =>
  verify(rawBody, timestamp, signature, OLD_SECRET) ||
  verify(rawBody, timestamp, signature, NEW_SECRET);
Step 4 (deleting the old webhook) is what actually cuts off the old secret — until that point we’ll keep signing with whichever record still exists.

Multiple webhooks per project

You can register as many URLs as you need. Each one:
  • Has its own id.
  • Has its own signingSecret.
  • Receives every event for the project (no per-URL filtering yet).
  • Is delivered in parallel with the others — one failing URL doesn’t delay the others.
Common patterns:
PatternSetup
Prod + staging mirrorRegister both URLs. Staging receives a copy of every prod event for testing.
Multi-service fan-outRegister one URL per consumer service. Each gets its own copy.
Backup endpointRegister a logging service in addition to your main handler. If your main handler is down, the logger still gets it for replay later.
There is no built-in fan-out filter. If you only want some events on a particular URL, branch in your handler and 2xx the rest.

Working with the CLI

A first-class photon webhooks CLI is on the roadmap. Until then, wrap the curl commands above in a shell function or use photon projects show --json to find your credentials and pipe them in.
# A small helper to list webhooks for the active project
list_webhooks() {
  local row creds id
  row=$(photon projects show --json)
  id=$(echo "$row" | jq -r '.id')
  creds=$(echo "$row" | jq -r '"\(.id):\(.secret)"')
  curl -s -u "$creds" "https://spectrum.photon.codes/projects/$id/webhooks/"
}

Working with the dashboard

The same operations are available as a UI in the Spectrum dashboard under the Webhook tab of a workspace (alongside Platforms, Users, Lines, and Settings). The tab contains:
  • A list of every registered webhook — endpoint URL and registration date.
  • An + Add webhook button. The signing secret is shown once in a modal after registration; it cannot be retrieved later.
  • A Remove button on each row. Same effect as the DELETE endpoint: delivery stops immediately and the signing secret is invalidated.

Common workflows

”I lost my secret”

  1. List webhooks, find the one you’ve lost the secret for.
  2. Delete it.
  3. Re-register the same URL — store the new secret immediately.
  4. Update your server.

”I’m changing my URL”

  1. Register the new URL (you’ll get a new secret).
  2. Update your DNS / ingress so the new URL points at your server.
  3. Once you’ve confirmed events are arriving on the new URL, delete the old one.
This avoids any window where events go nowhere because the old URL has been deleted but the new one isn’t live yet.

”I’m rotating credentials after a leak”

  1. Rotate the project secret first via photon projects regenerate-secret <id>. This invalidates Basic auth on management endpoints, so an attacker can’t make further changes.
  2. Rotate every webhook signing secret using the delete-and-recreate flow above.
The signing secret being leaked doesn’t grant the attacker the ability to send messages on your behalf — only to forge inbound deliveries to your webhook URL. But in either case, rotating quickly is the right move.

Where to next

You can register, list, delete, and rotate. If you’re hitting a snag along the way — a verify that won’t pass, a webhook that registers but never delivers, a duplicate that won’t go away — the next chapter is the triage guide.

Troubleshooting

Common signature errors, missed deliveries, duplicates, ngrok issues, and how to debug them.

API reference

Schema and live request playground for every endpoint on this page.