Skip to main content
A webhook front door parses inbound JSON, dedupes on email or domain, and upserts a lead without creating a duplicate. A webhook is just a URL that other systems can POST data to. This is the intake that every revenue operations (RevOps) team eventually wants. Form fills, product sign-ups, and partner feeds all POST to one endpoint. The Webhook received trigger then sends each one down a clean, repeatable path. That path has six steps. First you parse the JSON field by field. Then you filter out anything missing a usable identity. Next you look the contact up with Find records on a dedup key. A dedup key is the one stable field, like email, that you match on. It is what stops you from creating two copies of the same person. Finally you write with Create or update record, tag the source with Add record to list, and confirm the intake in a Slack channel.

Prerequisites

  • An Attio plan on the Plus tier or higher. The new Workflows engine and the Webhook received trigger are available there.
  • A connected Slack integration with a channel the team watches, such as #new-leads.
  • A Person object with an email_addresses attribute, and a Company object with a domains attribute. These two attributes are your dedup keys.
  • A list to tag intake against. This recipe uses one called Inbound leads with a single-select source attribute.
  • Permission to read and write records in the workspace. The Attio side needs no API key or external scope. The source system needs only the webhook URL.

How it works

The webhook accepts one JSON payload, you check it before any write, and the upsert matches on a stable key. An upsert means update the record if it already exists, or create it if it does not. Because you match on a stable key, a repeat submission updates the existing record instead of cloning it. The free logic blocks do all the qualifying. That way credits are spent only on the writes at the end. The run flows through these blocks in order:
  1. Webhook received accepts the inbound POST.
  2. Parse JSON extracts email, name, company_domain, and source field by field.
  3. Filter requires an email or a domain. It stops the run otherwise.
  4. Find records looks up an existing Person by the dedup key.
  5. Create or update record performs the safe upsert on email_addresses.
  6. Add record to list tags the lead with its source.
  7. Slack confirms the intake to the team.

Build steps

1

Add the webhook trigger

Create a new workflow and add the Webhook received trigger. Copy the generated endpoint URL into your source system. Set the source to send Content-Type: application/json, because the trigger parses the body only when the content type is JSON.
2

Parse the payload field by field

Add the Parse JSON block and point it at the webhook body. Define each field you depend on. Mark only the fields you truly need as required, so a thin payload fails loudly at this step instead of writing garbage downstream.
3

Filter out unusable payloads

Add the Filter block. Require a usable identity, meaning at minimum an email or a domain. That way spam and empty pings stop at the Filter block before any credit is spent. A condition that stops a run is free.
4

Find the existing record

Add the Find records block to look up a Person on the dedup key. This lookup is what makes the upsert safe, because you match on the same stable attribute every time.
5

Upsert the lead

Add the Create or update record block. Set the matching attribute to the same key you searched on, so a repeat submission updates the existing Person instead of cloning it.
The match attribute on Create or update record must be the same email_addresses key you searched on in Find records. A different match attribute, or none at all, makes every resubmission write a fresh Person and silently fills the workspace with duplicates.
6

Tag the source list

Add the Add record to list block so each lead carries where it came from. Map the parsed source into the list’s source attribute.
7

Confirm in Slack

Add the Slack block to post a one-line confirmation to the channel the team watches.

Variable mapping

Credit cost worked example

The trigger, Parse JSON, Filter, and Find records are free logic and lookup blocks. The two writes cost 1 credit each: Create or update record is 1 credit, and Add record to list is 1 credit. The Slack confirmation is an integration block at 1 credit. So a fully successful run that reaches Slack costs 3 credits. A run that upserts and tags but skips Slack costs 2 credits. At a realistic 800 valid intakes per month, that comes to about 800 × 3 = 2,400 credits. Any payloads the Filter rejects add 0 credits. Compare that figure against your plan’s monthly allowance on Credits and pricing before you turn the workflow on. This recipe contains no AI blocks, so it carries no token-based cost to estimate.

Test plan

  1. POST a clean payload to the endpoint with Content-Type: application/json:
  2. Open the run in run history. Parse JSON outputs the four fields, Filter passes, Find records returns empty on the first submission, and Create or update record reports a created Person.
  3. POST the exact same payload again. This is the duplicate test. Find records now returns the existing Person, and Create or update record reports an update rather than a second record. Confirm in the Person list that there is still only one jane@acme.com.
  4. Check #new-leads for two confirmation messages, one per run.
  5. POST a deliberately failing payload with no identity:
    Filter stops the run, and run history shows zero writes and zero credits spent.

Failure modes

Variations

Tagging each source to its own list

Route each source to its own list instead of one shared Inbound leads list. Add a Switch on the source field after the upsert, with one branch per source (pricing_page, partner_feed, webinar). Give each branch its own Add record to list block. The Switch block is free, so this variation adds no credit cost beyond the writes you would already make.

Rejecting spam early

Tighten the Filter to drop obvious junk before any write. Add conditions that stop the run when email ends in a disposable domain, or when company_domain is on a blocklist. The Filter block is free and stops the run, so rejected spam costs zero credits and never reaches the upsert.

See also


Source: Webhooks in Attio (Attio Help Center). Last reviewed against the Attio Help Center, June 2026.