> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.dealsignal.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.dealsignal.com/_mcp/server.

# Contact Profile Refresh Request completed (webhook)

POST 

When a refresh request **completes**, Dealsignal `POST`s the results to
the `webhook` URL you supplied at creation as JSON
(`Content-Type: application/json`, 60-second timeout). The payload is a
**bare JSON array** of profile objects, each
`{ "custom_columns": {...}, "contact_data": {...} }` — exactly the
structure returned by `request_status`.

A create response that reads `status: completed` is the final result for
every profile. POSTs for such a request can still arrive, even before the
create response (the request completed during the create call); treat
them as duplicates.

Up to two POSTs are sent, one after the other. Profiles that were already
`completed` in a synchronous create response, with or without a matched
contact, are normally not sent again: read them from the create response.

1. **Refreshed profiles** — profiles that completed with a matched
   contact. Skipped when there is nothing to report.
2. **Not-found profiles** — profiles that ended `not_found` without a
   matched contact (`contact_data` carries only the status fields).
   Always sent; with nothing to report the body is **empty** (not `[]`).

The webhook does not carry every profile of every request;
`request_status` always returns all of them.

### Correlation

The payload has **no envelope**: it carries neither the request's `id`
nor its `unique_identifier`. Correlate each profile through
`custom_columns`, which echoes the profile object you submitted — put a
key of your own in `metadata` if you need one.

### Delivery

Build an idempotent receiver:

* Deliveries can repeat. If a delivery fails (non-2xx, timeout,
  connection error), it is sent again the next time you call
  `request_status` — if your endpoint was down, call it to receive the
  results. You may also receive additional deliveries with an empty
  body. Deduplicate on your own key in `custom_columns.metadata`.
* Accept an empty body. Acknowledge each POST with a 2xx status right
  away, and process it after responding.
* Webhook requests are not signed. Use a hard-to-guess URL (for example
  with a secret token in it) and act only on profiles you can match to
  your own records.

Reference: https://docs.dealsignal.com/api-reference/contact-profile-refresh-requests/contact-profile-refresh-request-completed

## Request

### Payload

- `list of ContactProfileRefreshProfile`

## Types

### ContactProfileRefreshProfile

One profile of a request — the profile you submitted, echoed back, plus the enriched contact.

- `custom_columns` (map from string to any, optional) — The profile object you submitted, echoed back verbatim.
- `contact_data` (ContactProfileRefreshContactData, optional) — The enriched contact. Always carries the same **fixed set of 33 keys**, whatever you sent in `requested_attributes`. Contact details — `email`, `phone`, `mobile_number`, `direct_phone`, `corporate_phone` — are **never** included here, even when requested. Every contact-derived field is `null` until a contact is matched, except `current_company_and_title` (`{}`) and `previous_companies_and_titles` (`[]`). `search_status`, `match_status`, `remark`, `enrichment_status`, `enriched_by` and the two booleans are always present.

### ContactProfileRefreshContactData

The enriched contact. Always carries the same **fixed set of 33 keys**, whatever you sent in `requested_attributes`. Contact details — `email`, `phone`, `mobile_number`, `direct_phone`, `corporate_phone` — are **never** included here, even when requested. Every contact-derived field is `null` until a contact is matched, except `current_company_and_title` (`{}`) and `previous_companies_and_titles` (`[]`). `search_status`, `match_status`, `remark`, `enrichment_status`, `enriched_by` and the two booleans are always present.

- `id` (integer, required, nullable) — The matched Dealsignal contact ID; `null` until a contact is matched. A finished profile (`completed` or `not_found`) with `id` `null` has no matched contact — use this field, not `search_status`, to tell matched from unmatched profiles.
- `first_name` (string, required, nullable)
- `last_name` (string, required, nullable)
- `logo_url` (string, required, nullable)
- `company_name` (string, required, nullable)
- `title` (string, required, nullable)
- `changed_company` (boolean, required) — Always `true` or `false`, never `null`. Whether the contact's **primary employer** differs from your account's previous refresh of this contact. Identical to `ds_company_change` in the `changes` format. A profile without a verdict yet (refresh not concluded, no contact matched) reads `false`. The first refresh of a contact for your account has no previous refresh to compare with and reads `false` — it establishes the baseline for the next one. `false` therefore does not distinguish "no change" from "no verdict"; use `response_format=changes` and `ds_change_status` when that matters.
- `changed_title` (boolean, required) — Always `true` or `false`, never `null`. Whether the contact's **primary title** differs from your account's previous refresh of this contact — same rules as `changed_company`. Identical to `ds_title_change` in the `changes` format.
- `linkedin_url` (string, required, nullable)
- `company_website` (string, required, nullable)
- `seniorities` (string, required, nullable) — Seniority names, joined with ` || ` when there are several.
- `departments` (string, required, nullable) — Department names, joined with ` || ` when there are several.
- `contact_industry` (string, required, nullable)
- `contact_location` (string, required, nullable)
- `contact_country` (string, required, nullable)
- `contact_state` (string, required, nullable)
- `contact_city` (string, required, nullable)
- `skills` (string, required, nullable)
- `profile_completeness` (enum, required, nullable) — How complete the contact's profile is; `null` until a contact is matched.
  - Allowed values: `complete`, `rich`, `basic`, `partial`, `poor`
- `enriched_by` (string, required) — Always `dealsignal`.
- `contact_updated_at` (datetime, required, nullable)
- `primary_position` (ContactProfileRefreshContactDataPrimaryPosition, required, nullable) — The contact's primary position; `null` until a contact is matched. Selected independently of `ds_primary_company` / `ds_primary_title` in the `changes` format and can differ from it on some contacts — the change verdict is always about the `ds_primary_*` position.
- `contact_headline` (string, required, nullable)
- `contact_about_me` (string, required, nullable)
- `contact_education` (list of ContactProfileRefreshEducation, required, nullable) — The contact's education, one object per entry; keys without a value are omitted. `[]` when the contact has none, `null` until a contact is matched.
- `current_company_and_title` (ContactProfileRefreshCompanyAndTitle, required) — A snapshot of the contact's current position that may predate the refresh. Read `primary_position` / `current_position_details` for the refreshed state. `{}` when there was no current position or no contact is matched; never `null`.
- `previous_companies_and_titles` (list of ContactProfileRefreshCompanyAndTitle, required) — The contact's ended positions, in no guaranteed order. A snapshot taken together with `current_company_and_title`. `[]` when there were none or no contact is matched; never `null`.
- `search_status` (string, required, nullable) — Processing state of this profile. `completed` and `not_found` are final; `refreshing` means the profile is being refreshed. Any other value means the profile is still in progress — rely on the request's `status` rather than on specific in-progress values. `null` for a profile that was closed without a final status when the request completed. Whether a contact was matched is told by `contact_data.id` (see that field), not by `search_status`.
- `match_status` (string, required) — `found_and_verified` (matched and `search_status: completed`), `found_and_unverified` (matched, not verified by this refresh), `invalid_li_profile` (matched, but the LinkedIn profile is flagged invalid), `not_found`, or a verification note (`Verified`, `Contact Not Found`, `Invalid Source Data - Contact`, `Invalid Source Data - Linkedin Url`, `Invalid Source Data - Email`). Don't use `match_status` to decide whether a profile is finished — use `search_status` and the request's `status`.
- `remark` (string, required, nullable)
- `enrichment_status` (string, required, nullable) — Display label of `search_status`: `Finding A Match`, `Found And Refreshing`, `Found And Enriching`, `Completed`, `Not Found`; `null` for states without a label. A profile that finished (`completed` or `not_found`) without a match reports `Not Found`.
- `current_position_details` (list of ContactProfileRefreshPositionDetail, required, nullable) — The contact's current positions; `null` until a contact is matched.
- `old_position_details` (list of ContactProfileRefreshPositionDetail, required, nullable) — The contact's ended positions; `null` until a contact is matched.

### ContactProfileRefreshContactDataPrimaryPosition

The contact's primary position; `null` until a contact is matched. Selected independently of `ds_primary_company` / `ds_primary_title` in the `changes` format and can differ from it on some contacts — the change verdict is always about the `ds_primary_*` position.

- `company` (string, optional, nullable)
- `title` (string, optional, nullable)
- `company_linkedin_url` (string, optional, nullable)
- `company_domain` (string, optional, nullable)

### ContactProfileRefreshEducation

One education entry. Keys without a value are omitted.

- `university` (string, optional)
- `major` (string, optional)
- `degree` (string, optional)
- `start_date` (date, optional)
- `end_date` (date, optional)
- `logo_url` (string, optional)

### ContactProfileRefreshCompanyAndTitle

- `company` (string, optional, nullable)
- `title` (string, optional, nullable)
- `company_linkedin_url` (string, optional, nullable)
- `company_domain` (string, optional, nullable)

### ContactProfileRefreshPositionDetail

- `title` (string, optional, nullable)
- `company_name` (string, optional, nullable)
- `company_linkedin_url` (string, optional, nullable)
- `start_date` (date, optional, nullable)
- `end_date` (date, optional, nullable)
- `country` (string, optional, nullable)
- `company_logo_url` (string, optional, nullable)