Webhooks

Incoming events

A private URL that another tool posts to. When the post names a Contact who is waiting on that event in a Motion, the Motion moves on.

Outgoing webhooks tell your tool what happened in Curviate. An incoming event is the way back: your email tool, CRM or workflow posts JSON to a URL Curviate gives you, and an until event.<name> Step in a Motion (a booked meeting, a scored lead, an email reply) proceeds. Incoming events belong to the Automate Tier, like Motions.

Treat this URL like a password

Anyone who has the URL can post to it. It is shown in full once, when you create or rotate it, and is masked everywhere else. Only the person who may edit it can copy it from the dashboard. If it leaks, rotate it.

Create one

curviate incoming create --event-name meeting_booked --match-email lead_email

or over REST:

curl -X POST https://api.curviate.com/v1/incoming-events \
  -H "Authorization: Bearer $CURVIATE_API_KEY" -H "Content-Type: application/json" \
  -d '{"event_name": "meeting_booked", "match": {"email": "lead_email"}}'

The CLI's incoming group has the same verbs: create, list, get, update, delete, rotate, capture and arrivals. create takes --event-name or --event-name-field, --motion, --match-enrollment-id, --match-linkedin-url or --match-email (each a path in the post) and --required-header NAME[:VALUE].

The answer holds the URL, https://api.curviate.com/in/inc_.../<secret>, in full once. It starts switched off, waiting for a sample:

  1. Post a real example from your tool to the URL. The first post is the sample. It moves nothing.
  2. Read the incoming event: sample_state is captured and shape lists the paths and their types (never a value).
  3. Switch it on with PATCH /v1/incoming-events/{id} and {"enabled": true}. Switching on needs a sample and at least one match path.

To replace the sample later, POST /v1/incoming-events/{id}/capture (curviate incoming capture <id>): the next post becomes the new sample and, if the event is on, still moves Motions.

Name the event

Either event_name (fixed, for example meeting_booked) or event_name_field (a path to the name in each post, for example event_type). A name is lowercase letters, digits, _ and ., starting with a letter, up to 64 characters. The Motion waits for it with until: "event.<name>".

A path reads the posted JSON with dots and items[0] for array elements, or ["a.b"] for a key that holds a dot. A path that finds nothing is unset, never an error. Optionally set motion_id so that only that Motion's Enrollments move.

Find the Contact

match has up to three paths. The Contact is the first of these that resolves:

  1. enrollment_id: the Enrollment of that id (in your Team), and through it its Contact.
  2. linkedin_url: the Contact whose LinkedIn address equals the value.
  3. email: the one Contact in your Team whose stored email equals the value, ignoring case. Two or more Contacts with that email match nothing (ambiguous_email).

Email tools post the sender under different names, so look for lead_email, to_email or leadEmail. Then Curviate moves that Contact's active or paused Enrollments whose Sequence has an until event.<name> Step. A paused Enrollment records the event and moves when it resumes. An event that arrives before its Step is remembered and satisfies the Step when it is reached. A post fires no webhook.

Save values from the post

An until event.<name> Step can keep parts of the post:

{ "id": "booked", "until": "event.meeting_booked", "timeout": "14d",
  "save": { "meeting_type": "meeting.type", "score": "lead.score" },
  "save_to_contact": { "crm_stage": "deal.stage", "email": "lead_email" },
  "then": "is_hot", "else": "end" }
  • save writes Enrollment values for this Motion only. Read them as {{enrollment.meeting_type}} in messages and in webhook body templates, and test them with if enrollment.score. They end with the Enrollment.
  • save_to_contact writes the Contact's custom fields, which stay. email fills only an empty email; an email the Contact already has is kept.

Each entry says where in the post to read, as a path. A value that is an object or an array is not saved. A text value is cut to 2,000 characters and an Enrollment holds up to 50 values. Every rule is on Sequence rules.

What Curviate keeps

Curviate keeps no part of a post. It reads the paths in memory, acts, and discards it. What is stored: the sample's shape (paths and types, key names cut to 64 characters, an email-shaped key masked), and for each of the last 20 posts the outcome and counts. Never a value. The event name read from event_name_field is the one exception, kept only when it is a valid name.

Responses

What the URL answers

A post gets 200 {"received": true} whatever the outcome; the outcome is recorded on the arrival. Only a post that cannot be handled gets another code, checked in this order:

ResponseWhen
404The id or the secret is wrong.
410The URL was rotated out (after its 24-hour overlap) or the event was deleted.
401A required_header is set and the post lacks it or sends another value.
413The body is over 256 KB.
400The body is not JSON.
429More than 600 posts a minute for your Team. Retry-After says when.
200Everything else, with the outcome below.

Two identical posts are two arrivals; the second finds the Enrollment already moved.

Arrivals

GET /v1/incoming-events/{id}/arrivals (curviate incoming arrivals <id>) returns the last 20 posts, kept 30 days, metadata only: when, the event name, the outcome, how the Contact was matched and how many Enrollments moved or recorded it. Each saved value shows its name and saved, path_not_found, not_scalar, email_kept or limit_reached, never the value.

OutcomeMeaning
movedAn Enrollment waiting on the event moved on.
recordedThe event was recorded for an Enrollment that has not reached its Step, or is paused.
no_enrollment_waitingThe Contact has no Enrollment with an until Step for this event.
no_contactA match path resolved, but no Contact matches.
no_match_fieldNo match path found a value in the post.
ambiguous_emailTwo or more Contacts hold that email; nothing moved.
invalid_event_nameThe name read from the post is not a valid event name.
sample_capturedThe post became the sample; nothing moved.
incoming_offThe event is switched off.
tier_requiredYour Team's Tier does not include Motions.
bad_payloadThe body was not JSON.

Rotate, switch off, delete

  • POST /v1/incoming-events/{id}/rotate (curviate incoming rotate <id>) issues a new URL, in full in the answer only. The old URL keeps working for 24 hours (previous_url_valid_until), then answers 410.
  • Anyone in the Team can create one. Only its creator edits, rotates and captures. Owners and Managers can switch any one off or on and delete it. When the creator leaves, the incoming events move with their Motions to the billing Owner.
  • Below the Automate Tier you can still read, delete and switch off, but not create, edit or switch on.

Next steps