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

# Contact Webhook Payload

> Reference for the JSON body POSTed to URLs configured on the contact Send Webhook workflow action and the Forward Contact agent tool

## About

Goliath fires an outbound HTTP `POST` to a URL you configure whenever either
of the following runs against a contact:

* A **Send Webhook** step in a Contact workflow (see the workflows editor).
* The **Forward Contact** tool, invoked by a text agent when the contact is
  ready to be handed off downstream (the tool's target URLs live on the
  Communication Agent's config — the agent cannot choose them).

The body of the request is the same shape in both cases — only
`trigger_source` differs. Every configured URL receives the same JSON
payload; there is no per-URL transformation.

<Aside type="note">
  URLs must start with `http://` or `https://`. Other schemes (`ftp:`, `file:`,
  `data:`, etc.) are rejected at save time.
</Aside>

## Triggers

The webhook fires from exactly one of two places in Goliath. The payload is
identical; `trigger_source` is how your receiver can tell them apart.

### Workflow node (`WORKFLOW_NODE`)

A **Send Webhook** step running inside a Contact workflow. URLs are configured
on the node itself in the workflow editor — one step can hit multiple URLs.
The node fires the instant the workflow executor reaches it; there is no
debounce, no batching across contacts, and no queue between the workflow and
the dispatcher.

Dry-run previews of a workflow skip the HTTP dispatch entirely but still
record an audit entry — safe to use when validating a workflow graph without
hitting your real endpoints.

### Agent tool (`AGENT_TOOL`)

The **Forward Contact** tool, called by a Communication Agent during a live
conversation when the agent's LLM decides the contact is ready to be handed
off. The URLs live on the agent's configuration — the model chooses *whether*
to call the tool, but not *where* the payload goes.

If the bound agent has no forwarding URLs configured, the tool returns a
no-op `{ status: 'failed', urlsFired: 0 }` without dispatching and without
throwing. The agent sees the failure in the tool response and can explain
it to the user.

## Delivery semantics

### Headers

Goliath sends only one header:

```
Content-Type: application/json; charset=utf-8
```

There is **no** `User-Agent`, no request signing, no HMAC, and no API-key
header. See [Authentication](#authentication) below for how to secure your
receiver.

### URL sequencing

When a step has multiple URLs configured, they are hit **sequentially in
configured order**, not in parallel. Each URL has its own 10-second timeout.

The dispatcher is **fail-fast**: the first URL to return a non-2xx response
(or time out, or fail at the socket level) aborts the entire dispatch, and
any remaining URLs in the list are **not** attempted. Order your list so the
most critical receiver comes first.

### Error handling

* Any non-2xx response is treated as a failure. 4xx and 5xx are **not**
  distinguished — both fail the step identically.
* The response body is read and included in the recorded error message
  (truncated to the first 300 characters) to help you debug from the
  workflow audit UI.
* For `WORKFLOW_NODE`: the node is marked failed and any failure branch you
  have wired in the workflow takes over. The error message is visible in
  the workflow's run history.
* For `AGENT_TOOL`: the failure surfaces as a failed tool invocation in the
  agent's conversation transcript. The agent's LLM sees the error and
  decides how to recover (retry the tool, apologize to the user, etc.).

### No automatic retries

A failed request is **not** retried by Goliath. If you need at-least-once
semantics, wrap your receiver in your own queue (accept the request, 202
back, process asynchronously on your side) and make the internal processing
idempotent on `contact_id` + `triggered_at`.

## Receiver guidance

### Success criteria

Any `2xx` status code is a success. The response body is **not parsed** —
there is no way to return structured data that affects the workflow (no
"skip next step", no "override a field"). Dispatch is one-way, fire-and-forget.

### Authentication

Because Goliath does not sign requests, you are responsible for any
receiver-side authentication. Common patterns:

* Embed a long random secret in the URL as a query string
  (`https://your.service/hook?key=...`) and validate it server-side. The URL
  is stored encrypted at rest in Goliath and only transmitted over TLS.
* IP-allowlist Goliath's egress range at your load balancer.
* Terminate the webhook at a lightweight proxy (API Gateway, Cloudflare
  Worker) that checks the shared secret and re-signs the request for your
  internal services.

### Idempotency

The pair `contact_id` + `triggered_at` uniquely identifies a dispatch. Use
it as your dedup key. Note that a single contact can fire multiple webhooks
per day — from different workflows, or from multiple agent handoffs — so
`contact_id` alone is not sufficient if you care about per-event semantics.

## Payload shape

Top-level fields:

| Field                 | Type                              | Description                                                                                                                                                                                                                        |
| --------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`, `contact_id`    | `string`                          | Goliath's stable contact UUID. Both fields are the same value — `id` is provided for Zapier-style dedup keys.                                                                                                                      |
| `contact_name`        | `string`                          | Title-cased full name. Empty string if not set.                                                                                                                                                                                    |
| `primary_phone`       | `string \| null`                  | Top-ranked phone number as a flat string.                                                                                                                                                                                          |
| `primary_phone_type`  | `string \| null`                  | `MOBILE`, `RESIDENTIAL`, or `UNKNOWN`.                                                                                                                                                                                             |
| `primary_email`       | `string \| null`                  | Top-ranked email.                                                                                                                                                                                                                  |
| `primary_address`     | `string \| null`                  | Resolved street address for the first mapped property, or `null` if none.                                                                                                                                                          |
| `phone_numbers`       | `array`                           | All verified-or-unverified phones. See below.                                                                                                                                                                                      |
| `emails`              | `array`                           | All verified-or-unverified emails. See below.                                                                                                                                                                                      |
| `properties`          | `array`                           | Properties associated with this contact.                                                                                                                                                                                           |
| `tags`                | `array`                           | All tags and custom-field values, fully resolved to human-readable labels.                                                                                                                                                         |
| `contributors`        | `array`                           | Assigned users on this contact.                                                                                                                                                                                                    |
| `seller_intent_score` | `number \| null`                  | Goliath's internal 0–1 seller-intent score. Derived as the highest lead score across any properties currently mapped to this contact; `null` if no properties are mapped to the contact or if the score has not been computed yet. |
| `created_at`          | `string`                          | ISO-8601 timestamp when the contact was first created in Goliath.                                                                                                                                                                  |
| `updated_at`          | `string`                          | ISO-8601 timestamp of the most recent write to the contact record.                                                                                                                                                                 |
| `calls`               | `array`                           | Up to the 10 most recent calls, each with a condensed transcript.                                                                                                                                                                  |
| `texts`               | `array`                           | Up to the 20 most recent SMS messages, newest first.                                                                                                                                                                               |
| `notes`               | `array`                           | Up to the 20 most recent visible notes, newest first. Hidden and deleted notes are omitted.                                                                                                                                        |
| `deals`               | `array`                           | All non-archived deals this contact is on.                                                                                                                                                                                         |
| `triggered_at`        | `string`                          | ISO-8601 timestamp when the dispatch was prepared.                                                                                                                                                                                 |
| `trigger_source`      | `"WORKFLOW_NODE" \| "AGENT_TOOL"` | What caused the webhook to fire.                                                                                                                                                                                                   |

### `phone_numbers[]`

```json theme={null}
{
  "phone_number": "5716998065",
  "phone_type": "MOBILE",
  "verification_status": "VERIFIED",
  "ranking": 1
}
```

* `phone_type` is one of `MOBILE`, `RESIDENTIAL`, `UNKNOWN`.
* `verification_status` is one of:
  * `VERIFIED` — a user has explicitly confirmed this number is correct.
  * `UNVERIFIED` — the default; added from skip-trace, manual entry, or an
    upstream integration and never confirmed.
  * `WRONG` — a user has explicitly marked this number as incorrect. It's
    kept on the contact so future skip-trace runs don't re-add it.
* `ranking` is the display order on the contact record (1 = primary). May
  be `null` when no ranking has been assigned.
* The array is sorted by `ranking` ascending, with `null` rankings last.
  `primary_phone` and `primary_phone_type` mirror the first ranked entry.

### `emails[]`

```json theme={null}
{
  "email": "joao.cruz@example.com",
  "verification_status": "VERIFIED",
  "ranking": 1
}
```

`verification_status` uses the same `VERIFIED` / `UNVERIFIED` / `WRONG`
semantics as phones. The array is sorted by `ranking` ascending and
`primary_email` mirrors the first ranked entry.

### `properties[]`

```json theme={null}
{
  "address": "123 Main St, Dallas, TX 75201",
  "verification_status": "UNVERIFIED"
}
```

### `tags[]`

Every tag — including list memberships, free-form markers, and custom
fields — is flattened into a single array of `{tag_name, tag_type, value}`
tuples. For dropdown custom fields, `value` is the resolved option label
(not the option ID).

```json theme={null}
[
  { "tag_name": "Lead Temperature", "tag_type": "CUSTOM_FIELD_DROPDOWN", "value": "Warm" },
  { "tag_name": "Source", "tag_type": "CUSTOM_FIELD_DROPDOWN", "value": "Cal.com" },
  { "tag_name": "Meeting Booked", "tag_type": "FREE_FORM", "value": "true" },
  { "tag_name": "Hot Leads - March", "tag_type": "LIST", "value": "member" }
]
```

`tag_type` is one of: `LIST`, `FREE_FORM`, `CUSTOM_FIELD_DROPDOWN`,
`CUSTOM_FIELD_TEXT`, `CUSTOM_FIELD_NUMBER`, `CUSTOM_FIELD_DATE`,
`CUSTOM_FIELD_DOLLAR`.

### `contributors[]`

```json theme={null}
{ "user_name": "Zach Fitch", "role": "POINT_PERSON" }
```

* `role` is `POINT_PERSON` (the contact's owner) or `PARTICIPANT`.
* Internally Goliath tracks a distinct `OWNER` role that is serialized to
  `POINT_PERSON` on the wire — receivers never need to handle it separately.
* Revoked contributors (users previously assigned and then removed) are
  filtered out; only active contributors are included.

### `calls[]`

Up to 10 most recent calls, newest first. Each entry:

```json theme={null}
{
  "id": "b4cf3471-2072-48e0-9db9-8180714bad5c",
  "direction": "INBOUND",
  "status": "COMPLETED",
  "started_at": "2026-04-21T18:10:04.332Z",
  "duration_seconds": 456,
  "summary": "A realtor and agent discuss the benefits of using the Mojo platform...",
  "recording_url": "https://recordings.example.com/a.mp3",
  "transcript": [
    { "speaker": "Joao Da Cruz", "text": "Hello?" },
    { "speaker": "Max Yuan", "text": "Hey. Nice to meet you. This is Max." }
  ]
}
```

* `direction`: `INBOUND` or `OUTBOUND`.
* `status`: `RINGING`, `COMPLETED`, or `NO_ANSWER`.
* `recording_url`: signed URL to the source recording, or `null` if no
  recording was captured. Internal `gs://` URIs are never exposed.
* `transcript`: dialogue as an ordered array of `{speaker, text}` pairs.
  Per-utterance timing is intentionally omitted to keep bodies small —
  fetch the Call via the GraphQL API if you need start/end offsets.
* `transcript` is `null` when no transcription has been produced yet.

### `texts[]`

Up to 20 most recent SMS messages exchanged with this contact, newest first.
Each entry:

```json theme={null}
{
  "id": "6c7d8e9f-0a1b-2c3d-4e5f-67890abcdef0",
  "direction": "OUTBOUND",
  "status": "DELIVERED",
  "body": "Thanks! I'll send over the docs tonight.",
  "delivered_at": "2026-04-21T18:12:00.000Z",
  "created_at": "2026-04-21T18:11:59.000Z"
}
```

* `direction`: `INBOUND` or `OUTBOUND`.
* `status`:
  * `PENDING` — queued inside Goliath, not yet handed to Twilio.
  * `SENT` — Twilio accepted the send.
  * `DELIVERED` — delivery confirmed by Twilio (or, for inbound messages,
    the state in which Goliath records them).
  * `FAILED` — Twilio returned an error or the carrier rejected the message.
  * `SKIPPED` — Goliath refused to send because the recipient has explicitly
    opted out of texts (e.g., replied `STOP`). No message was transmitted.
    Only applies to outbound messages.
* `delivered_at`: ISO-8601 timestamp when Twilio confirmed delivery. `null`
  until then — including while the message is still pending, has failed, or
  was skipped.
* `created_at`: ISO-8601 timestamp when the message was recorded by Goliath.

### `notes[]`

Up to 20 most recent visible notes, newest first. Hidden (internal-only) and
deleted notes are excluded. Each entry:

```json theme={null}
{
  "id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
  "body": "Owner mentioned they want to close before tax season.",
  "is_pinned": true,
  "created_at": "2026-04-20T15:00:00.000Z",
  "updated_at": "2026-04-20T15:05:00.000Z"
}
```

* `is_pinned`: whether the note is pinned to the top of the contact in Goliath.
* Note replies are not included — fetch the Note via the GraphQL API if you
  need the full thread.

### `deals[]`

All non-archived deals this contact is on, most recent close date first:

```json theme={null}
{
  "id": "cee87bfc-aff4-4680-bebd-f5e341e11f88",
  "title": "Goliath Demo",
  "stage": "Follow Up Required",
  "price_cents": "12500000",
  "close_date": "2026-07-01T00:00:00.000Z",
  "description": null
}
```

* `stage`: the human-readable pipeline stage name (not the UUID).
* `price_cents`: price in USD cents, serialized as a **string** so large
  values survive JSON parsing without loss of precision. `null` when no
  price is set.
* `close_date`: ISO-8601 timestamp, or `null`.

## Full example

```json theme={null}
{
  "id": "acacc4f1-2169-4447-8c38-d1afde9fc258",
  "contact_id": "acacc4f1-2169-4447-8c38-d1afde9fc258",
  "contact_name": "Joao Da Cruz",
  "primary_phone": "5716998065",
  "primary_phone_type": "MOBILE",
  "primary_email": "joao.cruz@example.com",
  "primary_address": null,
  "phone_numbers": [
    { "phone_number": "5716998065", "phone_type": "MOBILE", "verification_status": "VERIFIED", "ranking": 1 }
  ],
  "emails": [
    { "email": "joao.cruz@example.com", "verification_status": "VERIFIED", "ranking": 1 }
  ],
  "properties": [],
  "tags": [
    { "tag_name": "Lead Temperature", "tag_type": "CUSTOM_FIELD_DROPDOWN", "value": "Warm" },
    { "tag_name": "Source", "tag_type": "CUSTOM_FIELD_DROPDOWN", "value": "Cal.com" }
  ],
  "contributors": [
    { "user_name": "Zach Fitch", "role": "PARTICIPANT" }
  ],
  "seller_intent_score": null,
  "created_at": "2026-04-01T09:00:00.000Z",
  "updated_at": "2026-04-21T09:30:00.000Z",
  "calls": [
    {
      "id": "b4cf3471-2072-48e0-9db9-8180714bad5c",
      "direction": "INBOUND",
      "status": "COMPLETED",
      "started_at": "2026-04-21T18:10:04.332Z",
      "duration_seconds": 456,
      "summary": "A realtor and agent discuss the platform.",
      "recording_url": null,
      "transcript": [
        { "speaker": "Joao Da Cruz", "text": "Hello?" },
        { "speaker": "Max Yuan", "text": "Hey. Nice to meet you." }
      ]
    }
  ],
  "texts": [
    {
      "id": "6c7d8e9f-0a1b-2c3d-4e5f-67890abcdef0",
      "direction": "OUTBOUND",
      "status": "DELIVERED",
      "body": "Thanks! I'll send over the docs tonight.",
      "delivered_at": "2026-04-21T18:12:00.000Z",
      "created_at": "2026-04-21T18:11:59.000Z"
    }
  ],
  "notes": [
    {
      "id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
      "body": "Owner mentioned they want to close before tax season.",
      "is_pinned": true,
      "created_at": "2026-04-20T15:00:00.000Z",
      "updated_at": "2026-04-20T15:05:00.000Z"
    }
  ],
  "deals": [
    {
      "id": "cee87bfc-aff4-4680-bebd-f5e341e11f88",
      "title": "Goliath Demo",
      "stage": "Follow Up Required",
      "price_cents": null,
      "close_date": null,
      "description": null
    }
  ],
  "triggered_at": "2026-04-22T01:02:36.642Z",
  "trigger_source": "WORKFLOW_NODE"
}
```
