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.
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_emailor 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:
- Post a real example from your tool to the URL. The first post is the sample. It moves nothing.
- Read the incoming event:
sample_stateiscapturedandshapelists the paths and their types (never a value). - Switch it on with
PATCH /v1/incoming-events/{id}and{"enabled": true}. Switching on needs a sample and at least onematchpath.
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:
enrollment_id: the Enrollment of that id (in your Team), and through it its Contact.linkedin_url: the Contact whose LinkedIn address equals the value.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" }savewrites Enrollment values for this Motion only. Read them as{{enrollment.meeting_type}}in messages and in webhook body templates, and test them withif enrollment.score. They end with the Enrollment.save_to_contactwrites the Contact's custom fields, which stay.emailfills 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:
| Response | When |
|---|---|
404 | The id or the secret is wrong. |
410 | The URL was rotated out (after its 24-hour overlap) or the event was deleted. |
401 | A required_header is set and the post lacks it or sends another value. |
413 | The body is over 256 KB. |
400 | The body is not JSON. |
429 | More than 600 posts a minute for your Team. Retry-After says when. |
200 | Everything 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.
| Outcome | Meaning |
|---|---|
moved | An Enrollment waiting on the event moved on. |
recorded | The event was recorded for an Enrollment that has not reached its Step, or is paused. |
no_enrollment_waiting | The Contact has no Enrollment with an until Step for this event. |
no_contact | A match path resolved, but no Contact matches. |
no_match_field | No match path found a value in the post. |
ambiguous_email | Two or more Contacts hold that email; nothing moved. |
invalid_event_name | The name read from the post is not a valid event name. |
sample_captured | The post became the sample; nothing moved. |
incoming_off | The event is switched off. |
tier_required | Your Team's Tier does not include Motions. |
bad_payload | The 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 answers410.- 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
- Webhook recipes: Instantly, Smartlead, HubSpot and n8n, each with its incoming event.
- Body templates: send Enrollment values on to your tool.
- Sequence rules:
save,save_to_contactandenrollment.*checks.