> ## Documentation Index
> Fetch the complete documentation index at: https://developer.fitprotracker.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing your endpoint

> Recipes for end-to-end testing without bothering real data.

A few well-trodden patterns for testing a webhook handler before pointing it at production traffic.

## Recipe 0: Send a test event from the FPT admin

The fastest end-to-end test, requiring no setup beyond an existing subscription. From the FPT admin's webhook-endpoints page, fire a single synthetic event to your URL on demand — signed with your real signing secret, so your verification code path is exercised exactly as production traffic would exercise it.

<Steps>
  <Step title="Open Settings → Webhook Endpoints">
    Find your subscription's row in the grid. The first action button (paper-plane icon) is **Send test event**.
  </Step>

  <Step title="Pick an event type">
    A dialog opens with radio buttons for each event type **your subscription is subscribed to** (we filter the choices so you can't test something you wouldn't receive in production).
  </Step>

  <Step title="Inspect the payload preview">
    Below the event selector, FPT shows the **exact JSON body that will be POSTed** to your endpoint — refreshed live every time you change the event type. Use the copy button to grab the payload for your own unit-test fixtures without ever firing a real send. `eventId` and `eventTimestamp` get regenerated on actual send; everything else matches.
  </Step>

  <Step title="Click Send">
    FPT builds the same payload again with a fresh `eventId`/`eventTimestamp`, signs it with your subscription's secret, POSTs to your URL with a 10-second timeout, and records the attempt.
  </Step>

  <Step title="Read the result">
    The dialog shows the HTTP status pill, request duration, and the `eventId` we generated. On non-2xx, you also get the error message. Click **Send again** to re-fire the same event type.
  </Step>
</Steps>

### How test events look on the wire

Test events are intentionally distinguishable from production traffic, so your handler can branch (or just dedupe) on them:

| Marker             | Where         | Value                                                                                                         |
| ------------------ | ------------- | ------------------------------------------------------------------------------------------------------------- |
| `_test: true` flag | inside `data` | Boolean. Present only on synthetic events. The simplest way to filter or dedupe test traffic in your handler. |
| `X-FPT-Test-Event` | header        | `true`                                                                                                        |
| `User-Agent`       | header        | suffix ` (test)` (full: `FitProTracker-Webhook/1.0 (test)`)                                                   |
| `contactId`        | inside `data` | `-1` (synthetic, never collides with a real contact)                                                          |

### Recommended handler pattern

```javascript theme={null}
app.post("/webhooks/fpt", (req, res) => {
    // ... verify signature, dedupe by eventId ...

    if (req.body.data?._test === true) {
        // Test event — log, ack 200, skip side effects
        console.log("Test event received:", req.body.eventId);
        return res.status(200).end();
    }

    // Real event — process normally
});
```

<Note>
  Test deliveries are recorded in the FPT admin's deliveries history with `attemptNumber: 0`, so you can distinguish them from production traffic (which always starts at `attemptNumber: 1`).
</Note>

### Inspect and replay any delivery

From the Webhook Endpoints page, the clock-icon button on any row opens the **deliveries drill-down** — a paged history of every event FPT has fired to that endpoint with:

* **Health strip** — last 24h count, success rate, status, last successful delivery
* **Time-range filter** — last 1h / 24h / 7d / 30d / all
* **Status / event type / eventId filters** — slice to just failures, or just a specific event type, or find one event by `eventId` prefix
* **Click-to-expand row** — see the exact signed JSON body and `X-FPT-Signature` header that we POSTed
* **Copy as curl** — one-click reproduction of the request as a curl command, with the original signature header preserved so your verifier passes on replay

This makes debugging a partner's failed delivery a 30-second exercise instead of a 30-minute back-and-forth.

## Recipe 1: webhook.site (zero-code receiver)

The fastest possible test. Use webhook.site to capture raw POSTs and inspect them without writing any code.

<Steps>
  <Step title="Open webhook.site">
    Open [webhook.site](https://webhook.site) — you'll get a unique URL like `https://webhook.site/8a3f-7c2e-...`
  </Step>

  <Step title="Subscribe in FPT">
    Settings → Webhook Endpoints → Add Endpoint. Paste your webhook.site URL. Check the events you want — for the fastest verification, pick `contact.status_changed` (you can trigger it on demand by moving a test contact between lifecycle groups). Save.
  </Step>

  <Step title="Trigger an event">
    Easiest path: click the **Send test event** paper-plane icon on your endpoint's row. A dialog opens with a live payload preview and fires a real signed POST to your URL. To verify with a real (non-test) event, move a test contact between lifecycle groups (Lead → Member) — within \~10 seconds, the request lands on webhook.site with full headers (including `X-FPT-Signature`) and body.
  </Step>

  <Step title="Inspect">
    Use webhook.site's UI to expand headers, view the JSON body, and replay the request to a different URL if you want to forward to your local dev server.
  </Step>
</Steps>

<Note>
  webhook.site is great for inspection but **don't leave production subscriptions pointed at it** — payloads contain customer data, and webhook.site is a public service.
</Note>

## Recipe 2: ngrok + your local server

For active development on your real handler, expose your localhost to the internet via [ngrok](https://ngrok.com/):

<Steps>
  <Step title="Install ngrok and start a tunnel to your local server">
    ```bash theme={null}
    ngrok http 3000
    ```

    ngrok prints a public HTTPS URL like `https://abc123.ngrok-free.app`.
  </Step>

  <Step title="Subscribe in FPT to the ngrok URL">
    Settings → Webhook Endpoints → URL: `https://abc123.ngrok-free.app/webhooks/fpt`
  </Step>

  <Step title="Iterate">
    Hit save in your code, restart your server, trigger an event in FPT. Watch the request hit your local handler in real time. Set breakpoints, log payloads, refactor.
  </Step>
</Steps>

<Tip>
  ngrok URLs change on every restart in the free tier. Update the subscription URL in FPT each time, or get an ngrok paid plan with a reserved domain.
</Tip>

## Recipe 3: Capture-and-replay

Once you have a few real events captured (from webhook.site or your own logs), you can replay them locally without touching FPT at all. This is the fastest dev loop for handler logic — no waiting on events to fire.

<CodeGroup>
  ```bash curl theme={null}
  # Replay a captured event against your local server
  curl -X POST http://localhost:3000/webhooks/fpt \
    -H "Content-Type: application/json" \
    -H "X-FPT-Signature: t=1717023600,v1=a3f8c2e7d1..." \
    --data-binary @captured-event.json
  ```

  ```javascript Node script theme={null}
  import fs from "node:fs";

  const event = JSON.parse(fs.readFileSync("captured-event.json", "utf8"));
  const signature = "t=1717023600,v1=a3f8c2e7d1...";  // copy from captured headers

  const response = await fetch("http://localhost:3000/webhooks/fpt", {
      method: "POST",
      headers: {
          "Content-Type": "application/json",
          "X-FPT-Signature": signature,
      },
      body: JSON.stringify(event),
  });
  console.log(response.status);
  ```
</CodeGroup>

<Warning>
  The captured signature is only valid for \~5 minutes after we sent it (our default timestamp tolerance) and for the exact body we signed. If you reformat the JSON or wait too long, signature verification will fail. For local replay testing where you want signature verification to keep working indefinitely, **disable the timestamp tolerance check in your dev environment** — but never in production.
</Warning>

## Recipe 4: Generate test payloads in code

For unit tests of your handler logic, build synthetic events directly:

<CodeGroup>
  ```javascript Node.js (Jest) theme={null}
  import crypto from "node:crypto";

  function buildSignedRequest(event, secret) {
      const rawBody = JSON.stringify(event);
      const t = Math.floor(Date.now() / 1000);
      const v1 = crypto.createHmac("sha256", secret)
          .update(`${t}.${rawBody}`)
          .digest("hex");
      return {
          body: rawBody,
          headers: { "x-fpt-signature": `t=${t},v1=${v1}` },
      };
  }

  test("handles contact.sub_group_changed", async () => {
      const event = {
          eventId: "test-" + crypto.randomBytes(16).toString("hex"),
          eventType: "contact.sub_group_changed",
          eventTimestamp: new Date().toISOString(),
          locationId: 1234,
          organizationId: 5678,
          apiVersion: "2026-05-29",
          data: { contactId: 9876, previousSubGroupId: 3, newSubGroupId: 7 },
      };
      const req = buildSignedRequest(event, process.env.FPT_WEBHOOK_SECRET);
      const res = await myHandler(req);
      expect(res.status).toBe(200);
      // ... assert your side effects
  });
  ```

  ```python Python (pytest) theme={null}
  import hmac
  import hashlib
  import json
  import time
  import secrets

  def build_signed_request(event, secret):
      raw_body = json.dumps(event).encode()
      t = int(time.time())
      v1 = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return {
          "body": raw_body,
          "headers": {"X-FPT-Signature": f"t={t},v1={v1}"},
      }

  def test_handles_contact_sub_group_changed():
      event = {
          "eventId": "test-" + secrets.token_hex(16),
          "eventType": "contact.sub_group_changed",
          "eventTimestamp": "2026-06-10T23:45:00Z",
          "locationId": 1234,
          "organizationId": 5678,
          "apiVersion": "2026-05-29",
          "data": {"contactId": 9876, "previousSubGroupId": 3, "newSubGroupId": 7},
      }
      req = build_signed_request(event, os.environ["FPT_WEBHOOK_SECRET"])
      res = my_handler(req)
      assert res.status == 200
      # ... assert your side effects
  ```
</CodeGroup>

## Common gotchas

<AccordionGroup>
  <Accordion title="Signature mismatch because your framework re-encoded the body">
    Express, Flask, ASP.NET — most web frameworks parse JSON into an object before your handler runs. If you sign `JSON.stringify(req.body)` instead of the raw bytes, you'll fail verification. See [Signature verification](/webhooks/signing) for the right pattern.
  </Accordion>

  <Accordion title="ngrok URL keeps changing">
    Free-tier ngrok rotates the subdomain on every restart. Either update the FPT subscription each time, or use a paid plan with a reserved domain.
  </Accordion>

  <Accordion title="webhook.site stops capturing">
    Free webhook.site sessions expire after a few days of inactivity. Your unique URL is durable but the captured request log may rotate.
  </Accordion>

  <Accordion title="Timestamps in test fixtures get stale">
    If you store a captured signature in a fixture file and try to replay it weeks later, your verifier's timestamp tolerance check will reject it. For dev/test environments, consider disabling the tolerance check or regenerating fixtures dynamically.
  </Accordion>
</AccordionGroup>
