Skip to main content

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.

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 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:
FieldTypeDescription
id, contact_idstringGoliath’s stable contact UUID. Both fields are the same value — id is provided for Zapier-style dedup keys.
contact_namestringTitle-cased full name. Empty string if not set.
primary_phonestring | nullTop-ranked phone number as a flat string.
primary_phone_typestring | nullMOBILE, RESIDENTIAL, or UNKNOWN.
primary_emailstring | nullTop-ranked email.
primary_addressstring | nullResolved street address for the first mapped property, or null if none.
phone_numbersarrayAll verified-or-unverified phones. See below.
emailsarrayAll verified-or-unverified emails. See below.
propertiesarrayProperties associated with this contact.
tagsarrayAll tags and custom-field values, fully resolved to human-readable labels.
contributorsarrayAssigned users on this contact.
seller_intent_scorenumber | nullGoliath’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_atstringISO-8601 timestamp when the contact was first created in Goliath.
updated_atstringISO-8601 timestamp of the most recent write to the contact record.
callsarrayUp to the 10 most recent calls, each with a condensed transcript.
textsarrayUp to the 20 most recent SMS messages, newest first.
notesarrayUp to the 20 most recent visible notes, newest first. Hidden and deleted notes are omitted.
dealsarrayAll non-archived deals this contact is on.
triggered_atstringISO-8601 timestamp when the dispatch was prepared.
trigger_source"WORKFLOW_NODE" | "AGENT_TOOL"What caused the webhook to fire.

phone_numbers[]

{
  "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[]

{
  "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[]

{
  "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).
[
  { "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[]

{ "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:
{
  "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:
{
  "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:
{
  "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:
{
  "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

{
  "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"
}