Skip to content

Register a webhook

This guide sets up an HTTP endpoint that Photon calls on every status change of your transactions, and shows how to authenticate and verify each delivery.

Prerequisites

  • An API key or a wallet-login JWT — Get API access
  • A publicly reachable HTTP(S) endpoint that accepts JSON POST requests

1. Register your endpoints

POST https://photon.example.com/api/user/register-webhooks authenticates with either an api-key header or Authorization: Bearer <JWT>. The body is a top-level JSON array:

Field Type Required Notes
url string yes Must start with http:// or https://.
authType string yes One of none, bearer, basic, api-key.
secret string when authTypenone Credential sent on each delivery; also the HMAC signing key.
apiKeyVar string no For api-key: name of the header or query parameter. Default X-API-Key.
apiKeyPlacement string no For api-key: query to send the secret as a query parameter instead of a header.
enabled boolean no Default true. Disabled webhooks receive nothing.
curl -X POST https://photon.example.com/api/user/register-webhooks \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "url": "https://example.com/hooks/photon",
      "authType": "bearer",
      "secret": "whsec_a1b2c3d4"
    }
  ]'
const res = await fetch("https://photon.example.com/api/user/register-webhooks", {
  method: "POST",
  headers: { "api-key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify([
    {
      url: "https://example.com/hooks/photon",
      authType: "bearer",
      secret: "whsec_a1b2c3d4",
    },
  ]),
});
console.log(await res.json());

Registration replaces your entire webhook set — include every webhook you want to keep on each call. [] removes them all.

2. Read the response

Warning

This endpoint returns a bare object, not the standard Photon envelope. Secrets are never echoed back.

{
  "userId": "68c275846a6ba1c9a2198a8c",
  "address": "0xA7A833e6641D7901F30EaD6f27d4Ee2C9bb670a7",
  "webhooks": [
    { "url": "https://example.com/hooks/photon", "authType": "bearer", "enabled": true }
  ]
}

3. Handle deliveries

Each delivery is a POST with Content-Type: application/json. The body is the bare notification JSON — the same payload the WebSocket streams, with no event wrapper:

{
  "txId": "68fa3450539a3c9d28bbca33",
  "chainId": 84532,
  "status": "EXECUTED",
  "costUSD": 0.0021,
  "totalNativeTokenUsed": "630000000000",
  "gasPrice": "30000000000",
  "txHash": "0x9f2c41d1a3f0e8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3",
  "submittedAt": "2026-07-29T10:15:00.000Z",
  "updatedAt": "2026-07-29T10:15:04.500Z",
  "retries": 0
}

costUSD, totalNativeTokenUsed, gasPrice, and txHash are omitted when not yet known. Authentication depends on authType:

authType What Photon sends
none No auth header.
bearer Authorization: Bearer <secret>
basic Authorization: Basic <secret> — supply the pre-encoded credentials as the secret.
api-key The secret in a header named apiKeyVar, or as a query parameter when apiKeyPlacement is query.

4. Verify the signature

Whenever a secret is set, every delivery also carries X-Photon-Signature: sha256=<hex> — the HMAC-SHA256 of the raw request body, keyed with your secret. Setting authType: "none" with a secret gives you signature verification without an auth header.

import { createHmac, timingSafeEqual } from "node:crypto";

function verifySignature(rawBody: string, header: string, secret: string): boolean {
  const expected =
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  return (
    header.length === expected.length &&
    timingSafeEqual(Buffer.from(header), Buffer.from(expected))
  );
}

5. Respond quickly

Reply with any 2xx status to acknowledge a delivery. Requests time out after 10 seconds; a timeout or non-2xx response is retried with exponential backoff (2 s, 4 s, 8 s, 16 s) for up to 5 total attempts.