# Webhook payload reference

URL: https://useembers.com/help/integrations/webhook-payload-reference/
Category: Integrations
Plan: Solo and above
Updated: 2026-10-30
Last verified: 2026-10-30

> Every field in an Embers webhook, from the envelope and versions to the lead, engagement, change, contact, report, and test objects, and the key to join on.

Every Embers webhook is a JSON `POST` with the same envelope around an event-specific `data` object. This page lists every field, which ones to build on, and which ones are capped or conditional.

> **Warning: Solo and above, administrators only**
>
> Webhooks are configured on the Webhooks page (**Integrations** in the sidebar, then the **Webhooks** row, `/integrations/webhooks`) or under Settings, then API and webhooks, then Webhooks (`/developer/webhooks`), on Solo or above, by a workspace admin. Both end with the same **Payload schema** card. [Create an endpoint](/help/integrations/webhooks/) first, and [verify the signature](/help/integrations/verify-webhook-signatures/) before you parse anything.

## The envelope

| Field | Type | What it is |
| --- | --- | --- |
| `id` | UUID string | The event id. Equals the `webhook-id` header and stays the same on retries and replays |
| `type` | string | One of the event types below |
| `api_version` | string | `2026-07-20`. Covers transport: headers, signatures, delivery, and the ids you join on |
| `schema_version` | string | `2026-10-28`. Changes when a payload key is added or removed. `2026-10-28` added the `lead.changed` event; `2026-10-16` added the lead's country and current company |
| `created_at` | ISO 8601 | When the event was created, not when this attempt was sent |
| `account` | object | `id` (the workspace UUID) and `name` |
| `data` | object | Shaped by `type` |

`account` lets an agency route events from several [client workspaces](/help/reports-and-workspaces/agency-workspaces-and-client-feeds/) through one receiver. The 2026-09-10 schema dropped `account.type`, so route on `account.id`. Ignore keys you do not recognise.

## Event types

| Type | `data` carries |
| --- | --- |
| `lead.created` | `lead` and `engagement` (recent form) |
| `lead.engagement.added` | `lead` and `engagement` (batched form) |
| `lead.status_changed` | `lead`, `engagement` (recent form), `previous_status`, `status`, `changed_at` |
| `lead.changed` | `lead`, `engagement` (recent form), and `change` |
| `report.generated` | `report`, `reason`, `generated_at` |
| `webhook.test` | `is_test`, `sample_event_type`, and the sample shape of that event |

[Send lead events to your own endpoint](/help/integrations/webhooks/#which-leads-trigger-an-event) covers when each one fires.

## The lead object

`data.lead` is the same on every lead event.

| Field | Type | Notes |
| --- | --- | --- |
| `id` | UUID string | The Embers member id. See [join on linkedin_urn](#join-on-linkedin_urn) |
| `name` | string | Full name. There is no first or last name field |
| `headline` | string | The LinkedIn headline |
| `title` | string | Current title, falling back to the current role, then the headline |
| `company` | string | Current company, falling back to the current role |
| `industry`, `location` | string | As stored on the profile |
| `country`, `country_code` | string | Read from their LinkedIn location: `United States`, `US` (ISO 3166). Empty when the location names no country |
| `company_size` | string | Their employer's LinkedIn headcount band: `1-10`, `11-50`, `51-200`, `201-500`, `501-1,000`, `1,001-5,000`, `5,001-10,000`, `10,001+`. Empty until the company is known |
| `company_employee_count` | integer or null | The employee count on their employer's LinkedIn page, once Embers has read it |
| `company_industry` | string | Their employer's industry, one of 20, such as `Financial Services`. `industry` stays the free text from their profile |
| `company_linkedin_url` | string | Their employer's LinkedIn page, from the current role on their profile. Empty for freelance or stealth roles |
| `company_website` | string | Their employer's website as a full `https://` address, from its LinkedIn page. Empty until Embers has read the page |
| `company_domain` | string | The same website as a bare domain, such as `acme.com`. Empty when `company_website` is |
| `linkedin_url` | string | Profile URL. Can change |
| `linkedin_urn` | string | LinkedIn's immutable member id. Can be blank on older records |
| `score` | integer | Lead score, 0 to 100, as shown in the app. See [how lead scoring works](/help/leads/how-scoring-works/) |
| `icp_match` | boolean or null | `null` means not yet classified |
| `icp_confidence` | integer | 0 to 100 |
| `icp_reason` | string | One sentence on why the lead matched or did not |
| `trend` | string | `rising`, `stable`, or `cooling` |
| `top_signals` | array of strings | The signals behind the score |
| `sources` | array of strings | Agents that found them: `post_engagement`, `comment_engagement`, `competitor_engagement`, `keyword_tracking`, `qualify_post` |
| `status` | string | Pipeline status after the event |
| `follower_count` | integer or null | Divisor for the engagement rate |
| `first_seen_at` | ISO 8601 or null | When Embers first saw this person |
| `work_history` | array | Up to 10 roles: `company`, `title`, `start_date`, `end_date`, `location`, `industry`. No job descriptions |
| `certifications` | array | Up to 10: `name`, `authority`, `date` |
| `contact` | object | Paid plans only. See [contact fields](#contact-fields) |

Status values are `active` (shown as Pending in the app), `contacted`, `replied`, `call_booked`, `not_interested`, and `dismissed`. The [leads list](/help/leads/leads-list/) describes each one.

Country and company values fill in as Embers reads profiles and company pages, so a new lead can arrive with some of them empty. Embers never guesses one.

Two numbers the app shows are derived rather than sent. Engagement rate is `engagement_count` divided by `follower_count`. Grade comes from `score`: A at 80 or more, B at 60, C at 40, D at 20, F below that.

## Join on linkedin_urn

Join people on `lead.linkedin_urn`. Do not key on `linkedin_url`: people change their vanity slug, and some are only ever seen in URN form, so one person can appear under two URLs. `lead.id` is stable per workspace and person, but when two member records are merged the losing id stops appearing. When `linkedin_urn` is blank on an older record, fall back to `lead.id`.

## Contact fields

`lead.contact` appears only when the workspace is on a paid Solo plan or above. On Free, and during the [free trial](/help/account-and-billing/free-trial/), the key is omitted entirely.

| Field | Notes |
| --- | --- |
| `email`, `phone` | Stored values only, otherwise `null` |
| `included` | `true` only when a stored email or phone exists |
| `enriched_at`, `last_verified_at` | When the contact was enriched, or `null` |
| `source`, `confidence` | Where it came from (`harvest` or `apify`) and how sure (`safe`, `verified`, or `risky`), or `null` |
| `status` | Always `ready` |

A webhook never triggers a fresh lookup and never invents an address. [Reveal a lead's email and phone](/help/outreach/lead-enrichment-and-contact-info/) explains how contacts get stored. Comment text can still contain details a person published, but that is public post content, not the `contact` object.

## The engagement object

On `lead.created`, `lead.status_changed`, `lead.changed`, and test samples of them, `data.engagement` is the recent form:

- `recent`: the lead's 10 most recent engagements, newest first.
- `by_type`: counts by `engagement_type` across those 10.
- `first_engaged_at`, `last_engaged_at`: the range they cover.

On `lead.engagement.added` it is the batched form, one event per lead per scan:

- `engagement_count`: how many engagements Embers first saw between `window_start` and `window_end`.
- `engagements`: the lead's most recent engagements, newest first, up to 50. It can include some that an earlier event already carried.
- `truncated`: `true` when `engagement_count` is larger than the array.

Embers sends at most 500 `lead.engagement.added` events per workspace per scoring pass.

**Fields on each engagement item**

| Field | Notes |
| --- | --- |
| `engagement_type` | LinkedIn's own value, such as `comment`, `comment_reply`, `repost`, or `praise`. Three mean the lead wrote the post: `keyword_authored` (it matched a tracked keyword), `commented_post_authored` (you commented on it, and it showed they need what you sell), and `watched_commented_post_authored` (a watched profile commented on it, with `source` `competitor`) |
| `engagement_category` | Branch on this: `comment`, `reply`, `reaction`, `repost`, or `authored_post` |
| `engaged_at`, `engaged_at_precision` | See below |
| `source` | `post` (your posts), `comment` (your comments elsewhere), `competitor`, or `keyword` |
| `comment_text` | The comment or reply, up to 500 characters, ending in `...` when cut |
| `post_url`, `post_snippet` | The post, with up to 280 characters of its text |
| `post_author_name`, `post_author_type` | `post_author_type` is `you`, `competitor`, `third_party`, or `lead` (the lead wrote the post) |
| `competitor_name` | Set on competitor engagement, otherwise empty |
| `intent_category` | What the words show, one set for comments and posts: `seeking_solution`, `switching_provider`, `seeking_recommendation`, `hiring_outsourcing`, `pain_point`, `growth_change`, `question`, `advocacy`, `thought_leadership`, `promotional`, `casual`, or `unknown`. Earlier payloads sent `purchase_intent` and `pain_alignment`; those are now `seeking_solution` and `pain_point` |
| `matched_keywords` | Keyword items only: the tracked phrases the post matched |

`engaged_at_precision` is `exact` for comments and replies, which carry a LinkedIn timestamp. Everything else, including reactions, reposts, and keyword posts, is `approximate`: Embers substitutes the time it first saw the engagement or the post's own date. Order a follow-up sequence on `exact` rows only.

## Status, report, and test fields

**`lead.status_changed`** adds `previous_status`, `status`, and `changed_at` beside `lead`. Blocking a company sends one with `status` set to `dismissed`.

**`lead.changed`** adds `change`, one [company change](/help/leads/company-changes/) Embers confirmed at the lead or at the company they work at. Each lead gets it once per change; a company's change reaches its 25 best-scored leads in the workspace at most.

| Field | Type | What it is |
| --- | --- | --- |
| `id` | integer | The change's id. The same change sent to several leads carries the same id |
| `kind` | string | `new_role`, `title_change`, `headcount`, `hiring`, `team_hiring`, `funding`, `new_leader`, or `acquisition` |
| `label` | string | The kind in words: `New role`, `Title change`, `Headcount growth`, `Hiring`, `Funding`, `New leader` |
| `about` | string | `person` for a new role or title change, `company` for the rest |
| `summary` | string | One sentence, such as `Started as VP Sales at Acme in Oct 2026.` |
| `effective_date` | date or null | `YYYY-MM-DD`, when the change happened, as the source says. A LinkedIn role date is the first of its month |
| `confirmed_at` | ISO 8601 or null | When Embers confirmed it: the "checked" date in the app |
| `source`, `source_label` | string | `linkedin_profile` (LinkedIn profile), `linkedin_jobs` (LinkedIn jobs), `account_insights` (Account Insights), `company_page` (Company page), or `sec_form_d` (SEC Form D) |
| `source_url` | string | Where it came from, such as the person's profile or the filing, when it has a web address. Can be empty |

```json
"change": {
  "id": 4821,
  "kind": "funding",
  "label": "Funding",
  "about": "company",
  "summary": "Acme filed a Form D: $8.7M raised, first sale Oct 22, 2026.",
  "effective_date": "2026-10-22",
  "confirmed_at": "2026-10-24T09:00:00+00:00",
  "source": "sec_form_d",
  "source_label": "SEC Form D",
  "source_url": "https://www.sec.gov/"
}
```

**`report.generated`** carries `data.report` with `id`, `title`, `status`, `date_range`, `date_start`, `date_end`, `share_url` (the public link), and `view_count`, plus `reason` (`created` or `regenerated`) and `generated_at`. It leaves out the report snapshot. See [client reports](/help/reports-and-workspaces/client-reports/).

**`webhook.test`** carries `is_test: true`, `sample_event_type`, and a sample of that event with placeholder data. A sample includes `contact` when your plan would.

**Example lead.status_changed payload**

```json
{
  "id": "e1b4f6e8-4c4a-4f50-b3c7-2ef2f97d5012",
  "type": "lead.status_changed",
  "api_version": "2026-07-20",
  "schema_version": "2026-10-28",
  "created_at": "2026-07-20T10:00:00+00:00",
  "account": {
    "id": "4d2f0eb9-81e5-4f20-8a60-25cbb9fe9ea5",
    "name": "Acme LinkedIn"
  },
  "data": {
    "lead": {
      "id": "967caf1c-c0a6-4b98-9f3b-1e0811fb53b4",
      "name": "Alex Smith",
      "headline": "VP Sales at Acme",
      "title": "VP Sales",
      "company": "Acme",
      "industry": "Insurance",
      "location": "Chicago, Illinois, United States",
      "country": "United States",
      "country_code": "US",
      "company_size": "51-200",
      "company_employee_count": 140,
      "company_industry": "Financial Services",
      "company_linkedin_url": "https://www.linkedin.com/company/acme/",
      "company_website": "https://acme.com",
      "company_domain": "acme.com",
      "linkedin_url": "https://www.linkedin.com/in/alex-smith/",
      "linkedin_urn": "ACoAAABBBCCCDDDEEEFFFGGG",
      "score": 82,
      "icp_match": true,
      "icp_confidence": 90,
      "icp_reason": "Insurance principal with clear sales authority.",
      "trend": "rising",
      "top_signals": ["Commented on multiple posts"],
      "sources": ["post_engagement"],
      "status": "contacted",
      "follower_count": 4200,
      "first_seen_at": "2026-06-02T09:15:00+00:00",
      "work_history": [
        {
          "company": "Acme",
          "title": "VP Sales",
          "start_date": "2021-04",
          "end_date": "",
          "location": "Chicago",
          "industry": "Insurance"
        }
      ],
      "certifications": [],
      "contact": {
        "email": "a***@acme.example",
        "phone": null,
        "enriched_at": "2026-07-01T00:00:00+00:00",
        "last_verified_at": "2026-07-01T00:00:00+00:00",
        "included": true,
        "source": "harvest",
        "confidence": "safe",
        "status": "ready"
      }
    },
    "engagement": {
      "recent": [
        {
          "engagement_type": "comment",
          "engagement_category": "comment",
          "engaged_at": "2026-07-19T14:02:00+00:00",
          "engaged_at_precision": "exact",
          "source": "post",
          "comment_text": "This matches what we see at renewal.",
          "post_url": "https://www.linkedin.com/feed/update/7481311828074078208/",
          "post_author_name": "Your Name",
          "post_author_type": "you",
          "post_snippet": "A post about renewal cycles in commercial insurance.",
          "competitor_name": "",
          "intent_category": "pain_point"
        }
      ],
      "by_type": { "comment": 1 },
      "first_engaged_at": "2026-07-19T14:02:00+00:00",
      "last_engaged_at": "2026-07-19T14:02:00+00:00"
    },
    "previous_status": "active",
    "status": "contacted",
    "changed_at": "2026-07-20T10:00:00+00:00"
  }
}
```

## Related

- [Verify webhook signatures](/help/integrations/verify-webhook-signatures/): Check the HMAC before you trust a request.

- [Send lead events to your own endpoint](/help/integrations/webhooks/): Create an endpoint and choose its events.

- [Send Embers leads to Zapier](/help/integrations/zapier/): Map these fields into a Zap.

## Frequently asked questions

**Why is there no first name or last name in the payload?**

The payload carries one name field. HubSpot contacts and a CSV export with split names give you first and last names; the webhook does not.

**Will a new field break my receiver?**

Not if you ignore keys you do not recognise. Almost every change is additive. The one removal so far, account.type on 2026-09-10, bumped schema_version.

**Why is lead.contact missing from my payloads?**

The workspace is on Free or still in its free trial. Contact details reach the webhook on a paid Solo plan or above.
