> ## 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.

# Event envelope

> The shape of every webhook payload Fit Pro Tracker sends.

Every webhook event — regardless of type — arrives in the same outer envelope. Only the `eventType` and `data` fields vary. Lock onto this envelope and your handler will work for `contact.*` today and every event family we ship in the future.

## Anatomy

```json theme={null}
{
  "eventId":         "9f1c7e2a8c4d4b1b9e3f5a6d7c8b9a0e",
  "eventType":       "contact.sub_group_changed",
  "eventTimestamp":  "2026-06-10T23:45:00.123Z",
  "locationId":      1234,
  "organizationId":  5678,
  "apiVersion":      "2026-05-29",
  "data": {
    "contactId":            9876,
    "previousSubGroupId":   3,
    "newSubGroupId":        7
  }
}
```

## Field reference

| Field            | Type                | Description                                                                                                                                                                                                                         |
| ---------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventId`        | `string`            | A globally unique identifier for this delivery. **Use this as your idempotency key** — see [delivery semantics](/webhooks/delivery). Hex string, no dashes, 32 characters.                                                          |
| `eventType`      | `string`            | Dot-namespaced event identifier. Format: `<resource>.<verb>` (e.g. `contact.created`, `payment.succeeded`). See [event catalog](/events/overview) for the full list.                                                                |
| `eventTimestamp` | `string (ISO 8601)` | When the underlying change happened in FPT, in UTC. Use this — not your receipt time — to order events.                                                                                                                             |
| `locationId`     | `number`            | The FPT location the event belongs to. For multi-location organizations, the same event from two locations is two separate deliveries with different `locationId`s.                                                                 |
| `organizationId` | `number`            | The FPT organization (the franchise / parent entity) the location belongs to. `0` if the location has no parent organization.                                                                                                       |
| `apiVersion`     | `string`            | Date-stamped envelope schema version (Stripe pattern). Currently `"2026-05-29"`. Backward-compatible additions (new optional fields) keep the same version; breaking changes ship a new dated version and consumers can pin to one. |
| `data`           | `object`            | Event-specific payload. Shape varies by `eventType` — see the individual event pages under [event catalog](/events/overview).                                                                                                       |

## What's in `data`?

The `data` object is the part that actually tells you what happened. Its shape depends on the event type. As of **v1.1.1**, the contact family carries an **identity baseline** (contactId, firstName, lastName, fullName, email, phone, isActive, contactGroupId, subGroupId, subGroupName) on every non-delete event — so partners can route, sync, and act on a single delivery without a follow-up state lookup. Additional fields gate behind PII subscription opt-ins.

<CodeGroup>
  ```json contact.created theme={null}
  {
    "contactId":      9876,
    "firstName":      "Jane",
    "lastName":       "Doe",
    "fullName":       "Jane Doe",
    "email":          "jane@example.com",
    "phone":          "+15551234567",
    "isActive":       true,
    "contactGroupId": 1,
    "subGroupId":     null,
    "subGroupName":   null,
    "inquiryDate":    "2026-06-10T23:45:00.123Z",
    "operation":      "create"
  }
  ```

  ```json contact.deleted theme={null}
  {
    "contactId": 9876,
    "operation": "delete"
  }
  ```

  ```json contact.status_changed theme={null}
  {
    "contactId":          9876,
    "firstName":          "Jane",
    "lastName":           "Doe",
    "fullName":           "Jane Doe",
    "email":              "jane@example.com",
    "phone":              "+15551234567",
    "isActive":           true,
    "contactGroupId":     3,
    "subGroupId":         null,
    "subGroupName":       null,
    "previousStatusId":   1,
    "newStatusId":        3,
    "previousStatusName": "Leads",
    "newStatusName":      "Members"
  }
  ```

  ```json contact.sub_group_changed theme={null}
  {
    "contactId":            9876,
    "firstName":            "Jane",
    "lastName":             "Doe",
    "fullName":             "Jane Doe",
    "email":                "jane@example.com",
    "phone":                "+15551234567",
    "isActive":             true,
    "contactGroupId":       3,
    "subGroupId":           7,
    "subGroupName":         "Gold Tier",
    "previousSubGroupId":   3,
    "newSubGroupId":        7,
    "previousSubGroupName": "Bronze Tier",
    "newSubGroupName":      "Gold Tier"
  }
  ```
</CodeGroup>

<Note>
  **Two patterns at play:**

  * **Identity baseline + diff** — non-delete events in the contact family carry the contact's identity (name, email, phone) so you can act on a single event without a state lookup. "Something changed" events (`status_changed`, `sub_group_changed`) add the before/after IDs and human-readable names.
  * **Signal-only** — `contact.deleted` ships only `{ contactId, operation: "delete" }`. The resource is gone; there's nothing to baseline.

  You can still call back to FPT's REST API (coming in v1.2) for state we don't carry inline, but the v1.1.1 baseline covers the common case where partners just want to sync the contact and route an automation.
</Note>

## Request headers

Beyond the JSON body, every delivery includes:

```http theme={null}
POST /your/path HTTP/1.1
Content-Type:    application/json
X-FPT-Signature: t=1717023600,v1=a3f8c2e7d1...
X-FPT-Event-Id:  9f1c7e2a8c4d4b1b9e3f5a6d7c8b9a0e
User-Agent:      FitProTracker-Webhook/1.0
```

| Header             | Meaning                                                                                                                                                                                             |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-FPT-Signature`  | HMAC-SHA256 signature of `${timestamp}.${rawBody}` — see [Signature verification](/webhooks/signing)                                                                                                |
| `X-FPT-Event-Id`   | Convenience copy of the envelope's `eventId`. Lets you dedupe at the HTTP layer (before parsing JSON) if your stack benefits from that. Same value as `body.eventId`.                               |
| `X-FPT-Test-Event` | Set to `true` **only on test events** fired from the admin's Send Test Event button (see [Testing](/webhooks/testing#recipe-0-send-a-test-event-from-the-fpt-admin)). Absent on production traffic. |
| `Content-Type`     | Always `application/json; charset=utf-8`                                                                                                                                                            |
| `User-Agent`       | `FitProTracker-Webhook/1.0` for production; `FitProTracker-Webhook/1.0 (test)` for test events                                                                                                      |

## Conventions

<AccordionGroup>
  <Accordion title="camelCase property names">
    All JSON properties use camelCase (`contactId`, not `ContactId` or `contact_id`). The wire format is stable; the C# / SQL backend uses PascalCase internally but serialization converts.
  </Accordion>

  <Accordion title="Null fields are omitted">
    If a field's value is `null`, it's left out of the JSON entirely rather than emitted as `"field": null`. Treat absence and explicit null as equivalent.
  </Accordion>

  <Accordion title="Timestamps are always UTC, always ISO 8601">
    Every `*At` / `*Date` / `*Timestamp` field is ISO 8601 in UTC with millisecond precision. No timezone offsets, no DATE-only formats unless explicitly noted on the event page.
  </Accordion>

  <Accordion title="IDs are integers, not strings">
    `contactId`, `locationId`, `organizationId`, `subscriptionId`, etc. are JSON numbers. Treat them as 32-bit ints on receipt; we'd give plenty of warning before we ever needed to widen to 64-bit.
  </Accordion>
</AccordionGroup>
