Download OpenAPI specification:
Public REST API for the oToK multichannel marketing communication platform.
The oToK REST API (/v1) gives programmatic access to your workspace: contacts (including
per-channel marketing consent), tags and groups, WhatsApp campaigns and templates, deals
and pipelines, the product catalog, payments, payment requests (pay-links), orders,
transactional email, broadcast email campaigns, newsletters, email suppressions,
read-only audience and sender-profile discovery, outbound webhooks, and bookings.
Send a workspace API key as a bearer token on every request:
Authorization: Bearer otok_live_...
Keys are created in Settings → Developers in the oToK app and are shown exactly once.
A revoked key is indistinguishable from an unknown key — both return 401 "Invalid API key".
The REST API requires a plan with API access (Growth or higher); other plans receive 403.
Endpoint groups that mirror a plan-gated product area additionally require that feature on
the workspace's plan — deals/pipelines (deals), payments and contact documents
(payments), payment requests (workspace_payments), orders (orders), campaigns
(campaigns), email campaigns (email_marketing), newsletters (newsletters),
bookings/meeting-types (booking). Every route in a gated group answers
403 with error_code: "FEATURE_NOT_INCLUDED_IN_PLAN" (a body without a statusCode
field) when the plan lacks the feature. Note the two distinct payment gates:
/v1/payments* and GET /v1/contacts/{id}/documents require the payments feature
(the payments ledger), while /v1/payment-requests* requires workspace_payments
(collecting money through the workspace's own connected payment provider) — a workspace
can hold either feature without the other, and the 403 message embeds whichever feature
id is missing. The suppression list (/v1/suppressions*) and the sender-profile
listing (GET /v1/sender-profiles) require the email_marketing feature.
Contacts (including consent), notes, tags, contact groups, templates, products,
audiences, emails, and webhook endpoints require only plan-wide API access.
The API uses two error body shapes:
POST /v1/campaigns/{id}/execute failures) use a
structured envelope: {"error": {"code", "message"}} — key on error.code.{"statusCode", "message", "error"}, where message is a string or an array of
per-field validation messages. Some 4xx responses extend this shape with a
machine-readable error_code field (deals, payments, orders, bookings, contacts).Request bodies are strictly validated: unknown body properties are rejected with 400.
Path :id parameters must be UUIDs; a non-UUID id returns 400 (except
DELETE /v1/webhook-endpoints/{id}, which returns 404).
The API never deletes customer data — contacts, deals, products, payments, orders,
campaigns, tags, and contact groups have no DELETE routes (deactivate a product with
is_active: false instead). The only DELETE endpoints are for resources the API
owns: DELETE /v1/notes/{id}, DELETE /v1/webhook-endpoints/{id},
DELETE /v1/suppressions/{id} (which lifts a send-time block — it deletes no
contact data and resubscribes no one), and DELETE /v1/newsletter-issues/{id}
(drafts and scheduled issues only — published issues are never deletable).
| Scope | Limit |
|---|---|
| Per API key (default, all endpoints) | 100 requests / minute |
POST /v1/emails |
300 requests / minute per key |
| Per source IP, across all API-key traffic | 300 requests / minute |
Exceeding a limit returns HTTP 429 with a Retry-After header. The per-key bucket is
shared across all source IPs using that key.
Most list endpoints return { "data", "total", "limit", "offset" } with limit
default 50 (hard cap 500). GET /v1/deals, GET /v1/payments,
GET /v1/payment-requests, GET /v1/orders, and the
email-campaign/newsletter/audience/sender-profile lists use dedicated query
parameters with limit default 25 (cap 100); absent or
empty limit/offset values default. On deals, payments, payment requests, and
the email-campaign/newsletter/audience/sender-profile lists, malformed values
return 400
("Invalid limit: must be a non-negative integer"); on orders, paging never
returns 400 — malformed or zero values silently default and out-of-range values are
clamped. Unrecognized enum filter values on deals/payments/orders are silently
ignored; GET /v1/payment-requests, GET /v1/email-campaigns, and
GET /v1/newsletters/{id}/issues instead reject an unknown status with 400
(as does GET /v1/audiences for an unknown kind).
phone/email.external_reference; products upsert by
external_id.external_reference — a replay
updates content/fields while the record is still editable and never touches
status, scheduled_at, or an issue's number.duplicate: true).external_refund_id — only when you supply one.idempotency_key.POST /v1/payment-requests is not
idempotent: a repeat POST mints a second, independently payable link. Cancel extras
with POST /v1/payment-requests/{id}/cancel.Idempotent create responses carry a top-level boolean duplicate field — false on a
fresh create, true when the call matched an existing record — with one exception:
POST /v1/orders returns the same full order body for both outcomes and no
duplicate marker (compare created_at, or pre-check with
GET /v1/orders?external_reference=...); the order refund endpoint does return
{ "duplicate", "order" }. Status codes are 201 for both outcomes everywhere except
POST /v1/emails, which answers 201 fresh / 200 replay.
/v1/email-campaigns*) — author, estimate,
launch, and schedule broadcast email campaigns: idempotent create via
external_reference, PATCH while draft/scheduled, GET …/estimate through the
send path's audience resolver, and send/schedule/unschedule riding the exact
in-app launch gates. Requires the email_marketing plan feature. A/B testing is
deliberately not exposed — create A/B campaigns in-app./v1/newsletters* + /v1/newsletter-issues*) —
list/create newsletters (rows carry active subscriber counts) and author their
issues: idempotent issue create via external_reference,
publish/schedule/unschedule with publish-time issue numbering, and deletion for
never-published issues. Requires the newsletters plan feature.content authoring contract on both new surfaces: exactly one of
markdown (CommonMark subset + ::button[Label](https://url) /
::snippet[name-or-uuid] directives + [[…]] personalization tokens), typed
blocks, or a raw design_json document — compiled at write time, with the
result reported in the response's compile: {ok, errors, warnings} envelope.GET /v1/audiences
lists the saved audiences campaigns accept as audience_id (rows carry the
summary columns and advisory size cache — never the stored definition), and
GET /v1/sender-profiles lists the email from-identities email campaigns accept
as sender_profile_id, each with a verified send-readiness signal (requires
the email_marketing plan feature; audiences need only plan-wide API access).
Neither resource is writable through the public API.GET /v1/meeting-types/{id}/embed — website-embed material for a meeting type:
the hosted booking page URL, the workspace's publishable embed key (bk_…, safe in
page HTML — not the secret API key), and a ready-to-paste snippet. Requires the
booking plan feature.GET /v1/contacts/{id}/consent reads both
channels' stored consent decision + provider-owned deliverability (email adds the
composed send-time suppressed verdict), and PUT /v1/contacts/{id}/consent/{channel}
records a subscribed/unsubscribed decision with its provenance (evidence trail
included; a channel with a spam complaint on record answers 409
consent_sticky_complained)./v1/suppressions*) — list, idempotently add,
and remove workspace email-suppression rows. Requires the email_marketing plan
feature. Suppression and consent deliberately compose at send time: adding a
suppression does not change consent, and removing one resubscribes no one./v1/products*) — the workspace product catalog
shared by deals and payments: list/get/create/update with an idempotent POST
upsert via external_id and no DELETE (deactivate with is_active: false).contact.created, contact.updated, contact.deleted, contact.consent_changed),
inbound messages (message.received), deal lifecycle (deal.created,
deal.stage_changed, deal.won, deal.lost), booking lifecycle (booking.created,
booking.rescheduled, booking.cancelled, booking.reassigned), event attendance
(event.attendance.changed), and form submissions (form.submitted). All are
opt-in by listing — the default subscription is unchanged, and a pre-existing
endpoint never starts receiving a new family unasked./v1/payment-requests*) — hosted pay-links
collected through the workspace's own connected payment provider (Cardcom / Sumit) —
and the four payment_request.* webhook event types (opt-in by listing, like the
order events). Gated by the workspace_payments plan feature (distinct from the
payments ledger gate).GET /v1/contacts/{id}/documents — read-only listing of a contact's financial
documents (invoices, receipts, credit documents), with an opt-in ?live=true
provider lookup. Gated by the payments feature.POST /v1/payments and PATCH /v1/payments/{id} accept a vat_mode +
vat_rate pair (recurring plans only, always together) and a free-form
metadata object (≤ 2048 bytes serialized; replace-on-write, null clears on
PATCH). Payment and payment-entry response fields are now fully documented,
including the recurring auto-charging/dunning fields, the stored VAT pair, and
entry-level refund/credit-document fields./v1/orders*) and the five order.* webhook
event types.Deal.amount, Payment.total_amount, and payment entry amount/recognized_amount —
is now typed as a JSON number. Spec versions up to 1.1.0 incorrectly documented
these response fields as decimal strings (e.g. "3600.00"); the API has always
returned JSON numbers (e.g. 3600), so nothing changed on the wire — but clients
generated from the earlier schema will see these declared types change from
string to number on regeneration.Contact upsert & update, plus the merge-conflict contract. There is no DELETE endpoint for contacts.
Standard list conventions apply. search matches name, first_name, last_name,
email, phone, company_name (case-insensitive substring), plus an exact match on
the E.164-normalized form of the term and the contact's historical phone/email
identifiers.
Contacts support extra filter operators: $jsonb_contains for tags/groups (by
UUID), $event_attendance ({"event_id": "...", "status": "attended"}), and the
virtual consent fields whatsapp_subscribed, email_subscribed (boolean),
whatsapp_deliverability, email_deliverability.
| filter | string Default: "{}" Example: filter={"lifecycle_stage":"lead"} String-encoded JSON object. Invalid JSON → 400 Grammar: exact match ( Filter values are type-checked against the target field before the query
runs — a mistyped value returns 400 |
| sort | string Default: "-created_at" Field name; |
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
| search | string Free-text search (fields vary per resource). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "phone": "string",
- "name": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "string",
- "avatar_url": "string",
- "notes": "string",
- "lifecycle_stage": "lead",
- "source": "manual",
- "block_state": "none",
- "company_name": "string",
- "vat_number": "string",
- "job_title": "string",
- "industry": "string",
- "company_website": "string",
- "annual_revenue": 0,
- "employee_count": 0,
- "currency_preference": "string",
- "address_line1": "string",
- "address_line2": "string",
- "city": "string",
- "state": "string",
- "country": "string",
- "postal_code": "string",
- "gender": "male",
- "date_of_birth": "2019-08-24",
- "language": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "fbclid": "string",
- "lead_score": 0,
- "linkedin_url": "string",
- "facebook_url": "string",
- "instagram_handle": "string",
- "twitter_handle": "string",
- "custom_fields": { },
- "tags": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "groups": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "event_attendances": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "status": "string",
- "registered_at": "2019-08-24T14:15:22Z",
- "attended_at": "2019-08-24T14:15:22Z",
- "unregistered_at": "2019-08-24T14:15:22Z"
}
], - "whatsapp_subscribed": true,
- "email_subscribed": true,
- "whatsapp_deliverability": "string",
- "email_deliverability": "string",
- "score_band": "cold",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Creates a contact, or updates the existing contact that the given phone/email
resolves to. Resolution is history-aware and phone wins over email. On a match,
scalar fields overwrite, custom_fields shallow-merge, and tags/groups are
added to the existing set (never removed by this route).
The response is 201 in both cases (create and update) and carries a top-level
boolean duplicate field — false when this request created the contact, true
when it matched and updated an existing one.
If the phone resolves to one existing contact and the email to a different one, the
API does not write: it opens a merge request and returns
409 CONTACT_MERGE_REQUIRED. The non-identity fields of the request are parked
with the merge request and applied when it is resolved.
| phone | string <= 32 characters Normalized to E.164 (local numbers use the workspace default country). |
| name | string <= 200 characters |
| first_name | string <= 100 characters |
| last_name | string <= 100 characters |
string <email> <= 255 characters | |
| avatar_url | string <= 500 characters |
| notes | string <= 5000 characters |
| lifecycle_stage | string Enum: "lead" "prospect" "customer" "inactive" "archived" |
| source | string Enum: "manual" "import" "widget" "campaign" "api" "form" |
| block_state | string Enum: "none" "workspace" "global" Changes route through the consent/blocking subsystem. Global blocks cannot be
lifted via the API. A non- |
| company_name | string <= 200 characters |
| vat_number | string <= 40 characters |
| job_title | string <= 120 characters |
| industry | string <= 80 characters |
| company_website | string <= 500 characters |
| annual_revenue | number |
| employee_count | integer [ 0 .. 2147483647 ] |
| currency_preference | string <= 8 characters |
| address_line1 | string <= 200 characters |
| address_line2 | string <= 200 characters |
| city | string <= 100 characters |
| state | string <= 100 characters |
| country | string <= 100 characters |
| postal_code | string <= 20 characters |
| gender | string Enum: "male" "female" "other" "prefer_not_to_say" |
| date_of_birth | string <date> ISO 8601 date. |
| language | string <= 12 characters |
| utm_source | string <= 200 characters |
| utm_medium | string <= 200 characters |
| utm_campaign | string <= 200 characters |
| utm_term | string <= 200 characters |
| utm_content | string <= 200 characters |
| gclid | string <= 200 characters |
| fbclid | string <= 200 characters |
| lead_score | number Engine-owned: silently ignored while workspace lead scoring is enabled — the response echoes the computed score. Writable only when scoring is disabled. |
| linkedin_url | string <= 500 characters |
| facebook_url | string <= 500 characters |
| instagram_handle | string <= 100 characters |
| twitter_handle | string <= 100 characters |
object Arbitrary keys; shallow-merged into the existing object on update. Set a
key to | |
| tags | Array of strings[ items [ 1 .. 100 ] characters ] Tag names. Missing tags are auto-created. |
| groups | Array of strings[ items [ 1 .. 100 ] characters ] Group names. Missing groups are auto-created. |
{- "phone": "string",
- "name": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "user@example.com",
- "avatar_url": "string",
- "notes": "string",
- "lifecycle_stage": "lead",
- "source": "manual",
- "block_state": "none",
- "company_name": "string",
- "vat_number": "string",
- "job_title": "string",
- "industry": "string",
- "company_website": "string",
- "annual_revenue": 0,
- "employee_count": 2147483647,
- "currency_preference": "string",
- "address_line1": "string",
- "address_line2": "string",
- "city": "string",
- "state": "string",
- "country": "string",
- "postal_code": "string",
- "gender": "male",
- "date_of_birth": "2019-08-24",
- "language": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "fbclid": "string",
- "lead_score": 0,
- "linkedin_url": "string",
- "facebook_url": "string",
- "instagram_handle": "string",
- "twitter_handle": "string",
- "custom_fields": { },
- "tags": [
- "string"
], - "groups": [
- "string"
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "phone": "string",
- "name": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "string",
- "avatar_url": "string",
- "notes": "string",
- "lifecycle_stage": "lead",
- "source": "manual",
- "block_state": "none",
- "company_name": "string",
- "vat_number": "string",
- "job_title": "string",
- "industry": "string",
- "company_website": "string",
- "annual_revenue": 0,
- "employee_count": 0,
- "currency_preference": "string",
- "address_line1": "string",
- "address_line2": "string",
- "city": "string",
- "state": "string",
- "country": "string",
- "postal_code": "string",
- "gender": "male",
- "date_of_birth": "2019-08-24",
- "language": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "fbclid": "string",
- "lead_score": 0,
- "linkedin_url": "string",
- "facebook_url": "string",
- "instagram_handle": "string",
- "twitter_handle": "string",
- "custom_fields": { },
- "tags": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "groups": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "event_attendances": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "status": "string",
- "registered_at": "2019-08-24T14:15:22Z",
- "attended_at": "2019-08-24T14:15:22Z",
- "unregistered_at": "2019-08-24T14:15:22Z"
}
], - "whatsapp_subscribed": true,
- "email_subscribed": true,
- "whatsapp_deliverability": "string",
- "email_deliverability": "string",
- "score_band": "cold",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "duplicate": true
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "phone": "string",
- "name": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "string",
- "avatar_url": "string",
- "notes": "string",
- "lifecycle_stage": "lead",
- "source": "manual",
- "block_state": "none",
- "company_name": "string",
- "vat_number": "string",
- "job_title": "string",
- "industry": "string",
- "company_website": "string",
- "annual_revenue": 0,
- "employee_count": 0,
- "currency_preference": "string",
- "address_line1": "string",
- "address_line2": "string",
- "city": "string",
- "state": "string",
- "country": "string",
- "postal_code": "string",
- "gender": "male",
- "date_of_birth": "2019-08-24",
- "language": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "fbclid": "string",
- "lead_score": 0,
- "linkedin_url": "string",
- "facebook_url": "string",
- "instagram_handle": "string",
- "twitter_handle": "string",
- "custom_fields": { },
- "tags": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "groups": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "event_attendances": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "status": "string",
- "registered_at": "2019-08-24T14:15:22Z",
- "attended_at": "2019-08-24T14:15:22Z",
- "unregistered_at": "2019-08-24T14:15:22Z"
}
], - "whatsapp_subscribed": true,
- "email_subscribed": true,
- "whatsapp_deliverability": "string",
- "email_deliverability": "string",
- "score_band": "cold",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Same field set and validation as the POST upsert (all optional), with different semantics:
tags/groups replace the full set (empty array clears; names auto-create).custom_fields still shallow-merge — set a key to null to remove it.name and first_name/last_name stay in sync.block_state changes route through the consent/blocking subsystem; global blocks
cannot be lifted via the API.Setting phone/email to an identifier that belongs to a different contact does
not apply the write: like the POST upsert's conflict path, a merge request is opened
and the response is 409 CONTACT_MERGE_REQUIRED. Non-identity fields sent in the
same PATCH are held on the merge request and applied when it is resolved. The
conflict check is history-aware: an identifier no contact currently holds but
that another contact previously held also parks a merge request and returns the 409.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| phone | string <= 32 characters Normalized to E.164 (local numbers use the workspace default country). |
| name | string <= 200 characters |
| first_name | string <= 100 characters |
| last_name | string <= 100 characters |
string <email> <= 255 characters | |
| avatar_url | string <= 500 characters |
| notes | string <= 5000 characters |
| lifecycle_stage | string Enum: "lead" "prospect" "customer" "inactive" "archived" |
| source | string Enum: "manual" "import" "widget" "campaign" "api" "form" |
| block_state | string Enum: "none" "workspace" "global" Changes route through the consent/blocking subsystem. Global blocks cannot be
lifted via the API. A non- |
| company_name | string <= 200 characters |
| vat_number | string <= 40 characters |
| job_title | string <= 120 characters |
| industry | string <= 80 characters |
| company_website | string <= 500 characters |
| annual_revenue | number |
| employee_count | integer [ 0 .. 2147483647 ] |
| currency_preference | string <= 8 characters |
| address_line1 | string <= 200 characters |
| address_line2 | string <= 200 characters |
| city | string <= 100 characters |
| state | string <= 100 characters |
| country | string <= 100 characters |
| postal_code | string <= 20 characters |
| gender | string Enum: "male" "female" "other" "prefer_not_to_say" |
| date_of_birth | string <date> ISO 8601 date. |
| language | string <= 12 characters |
| utm_source | string <= 200 characters |
| utm_medium | string <= 200 characters |
| utm_campaign | string <= 200 characters |
| utm_term | string <= 200 characters |
| utm_content | string <= 200 characters |
| gclid | string <= 200 characters |
| fbclid | string <= 200 characters |
| lead_score | number Engine-owned: silently ignored while workspace lead scoring is enabled — the response echoes the computed score. Writable only when scoring is disabled. |
| linkedin_url | string <= 500 characters |
| facebook_url | string <= 500 characters |
| instagram_handle | string <= 100 characters |
| twitter_handle | string <= 100 characters |
object Arbitrary keys; shallow-merged into the existing object on update. Set a
key to | |
| tags | Array of strings[ items [ 1 .. 100 ] characters ] Tag names. Missing tags are auto-created. |
| groups | Array of strings[ items [ 1 .. 100 ] characters ] Group names. Missing groups are auto-created. |
{- "phone": "string",
- "name": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "user@example.com",
- "avatar_url": "string",
- "notes": "string",
- "lifecycle_stage": "lead",
- "source": "manual",
- "block_state": "none",
- "company_name": "string",
- "vat_number": "string",
- "job_title": "string",
- "industry": "string",
- "company_website": "string",
- "annual_revenue": 0,
- "employee_count": 2147483647,
- "currency_preference": "string",
- "address_line1": "string",
- "address_line2": "string",
- "city": "string",
- "state": "string",
- "country": "string",
- "postal_code": "string",
- "gender": "male",
- "date_of_birth": "2019-08-24",
- "language": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "fbclid": "string",
- "lead_score": 0,
- "linkedin_url": "string",
- "facebook_url": "string",
- "instagram_handle": "string",
- "twitter_handle": "string",
- "custom_fields": { },
- "tags": [
- "string"
], - "groups": [
- "string"
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "phone": "string",
- "name": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "string",
- "avatar_url": "string",
- "notes": "string",
- "lifecycle_stage": "lead",
- "source": "manual",
- "block_state": "none",
- "company_name": "string",
- "vat_number": "string",
- "job_title": "string",
- "industry": "string",
- "company_website": "string",
- "annual_revenue": 0,
- "employee_count": 0,
- "currency_preference": "string",
- "address_line1": "string",
- "address_line2": "string",
- "city": "string",
- "state": "string",
- "country": "string",
- "postal_code": "string",
- "gender": "male",
- "date_of_birth": "2019-08-24",
- "language": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "fbclid": "string",
- "lead_score": 0,
- "linkedin_url": "string",
- "facebook_url": "string",
- "instagram_handle": "string",
- "twitter_handle": "string",
- "custom_fields": { },
- "tags": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "groups": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "event_attendances": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "status": "string",
- "registered_at": "2019-08-24T14:15:22Z",
- "attended_at": "2019-08-24T14:15:22Z",
- "unregistered_at": "2019-08-24T14:15:22Z"
}
], - "whatsapp_subscribed": true,
- "email_subscribed": true,
- "whatsapp_deliverability": "string",
- "email_deliverability": "string",
- "score_band": "cold",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns both channels (whatsapp + email) at once. A channel without a stored
decision reads consent_state/deliverability "unknown" — treat unknown as not
sendable. The email channel additionally carries the composed send-time
suppressed verdict (suppression list + blacklist + deliverability), which is
independent of the consent decision.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "block_state": "none",
- "channels": {
- "whatsapp": {
- "consent_state": "subscribed",
- "consent_basis": "express_opt_in",
- "consent_source": "string",
- "consent_at": "2019-08-24T14:15:22Z",
- "consent_expires_at": "2019-08-24T14:15:22Z",
- "deliverability": "unknown",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "complained_at": "2019-08-24T14:15:22Z",
- "last_bounce_at": "2019-08-24T14:15:22Z",
- "suppressed": true,
- "suppression_reason": "string"
}, - "email": {
- "consent_state": "subscribed",
- "consent_basis": "express_opt_in",
- "consent_source": "string",
- "consent_at": "2019-08-24T14:15:22Z",
- "consent_expires_at": "2019-08-24T14:15:22Z",
- "deliverability": "unknown",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "complained_at": "2019-08-24T14:15:22Z",
- "last_bounce_at": "2019-08-24T14:15:22Z",
- "suppressed": true,
- "suppression_reason": "string"
}
}
}Records a subscribed/unsubscribed decision with its provenance — the same path
every in-app consent change takes, so the consent-evidence ledger and activity
timeline apply identically. "unknown" is a system state and cannot be set, and
deliverability is provider-owned (bounce/complaint feedback) and is not
writable. Subscribing a channel whose deliverability is complained is refused
with 409 consent_sticky_complained — a spam complaint can never be lifted
through the API.
Returns the resulting channel object in the same shape GET uses, so the caller
sees the applied state, stamped provenance, and deliverability (the email
channel includes the suppressed verdict).
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| channel required | string Enum: "whatsapp" "email"
|
| state required | string Enum: "subscribed" "unsubscribed" The consent decision to record. |
| basis | string Enum: "express_opt_in" "double_opt_in" "soft_opt_in" "implied" "imported" Legal basis of the decision. Defaults to |
| source | string <= 100 characters Where the decision came from in your system; recorded as |
| expires_at | string <date-time> When this consent expires (ISO 8601). |
| ip | string <= 64 characters End-user IP captured with the decision (evidence trail). |
| user_agent | string <= 512 characters End-user user agent captured with the decision (evidence trail). |
{- "state": "subscribed",
- "basis": "express_opt_in",
- "source": "string",
- "expires_at": "2019-08-24T14:15:22Z",
- "ip": "string",
- "user_agent": "string"
}{- "consent_state": "subscribed",
- "consent_basis": "express_opt_in",
- "consent_source": "string",
- "consent_at": "2019-08-24T14:15:22Z",
- "consent_expires_at": "2019-08-24T14:15:22Z",
- "deliverability": "unknown",
- "unsubscribed_at": "2019-08-24T14:15:22Z",
- "complained_at": "2019-08-24T14:15:22Z",
- "last_bounce_at": "2019-08-24T14:15:22Z",
- "suppressed": true,
- "suppression_reason": "string"
}Returns a JSON array of all the contact's notes — not paginated, no query
parameters. Pinned notes come first (most recently pinned on top), then the rest
newest-first. author_name is included on list responses only.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
[- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "author_user_id": "13209de9-6c50-42f7-a1fd-5bf87d7b9225",
- "author_name": "string",
- "source": "string",
- "body": "string",
- "body_json": { },
- "mentioned_user_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "pinned_at": "2019-08-24T14:15:22Z",
- "conversation_id": "cc71b11a-25cd-4c2d-9950-df2cc38e3407",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]API note payloads are text only (rich text and mentions are in-app features; sending
them returns 400). Notes created via the API have no author user and are attributed
to source api.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| body required | string <= 5000 characters Trimmed — empty after trim → 400 "Note body cannot be empty". |
| pinned | boolean
|
{- "body": "string",
- "pinned": true
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "author_user_id": "13209de9-6c50-42f7-a1fd-5bf87d7b9225",
- "author_name": "string",
- "source": "string",
- "body": "string",
- "body_json": { },
- "mentioned_user_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "pinned_at": "2019-08-24T14:15:22Z",
- "conversation_id": "cc71b11a-25cd-4c2d-9950-df2cc38e3407",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Both fields optional; sending neither returns the current note unchanged. A body
change bumps updated_at; a pin toggle alone does not. If the note is the contact's
profile note, a body edit also updates the contact's notes field (and fires
field-change automations).
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| body | string <= 5000 characters Empty → 400. |
| pinned | boolean Pin/unpin. |
{- "body": "string",
- "pinned": true
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "author_user_id": "13209de9-6c50-42f7-a1fd-5bf87d7b9225",
- "author_name": "string",
- "source": "string",
- "body": "string",
- "body_json": { },
- "mentioned_user_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "pinned_at": "2019-08-24T14:15:22Z",
- "conversation_id": "cc71b11a-25cd-4c2d-9950-df2cc38e3407",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Deleting the contact's profile note also clears the contact's notes field. A
deletion breadcrumb is kept in the contact's activity timeline.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "success": true
}Standard list conventions; search matches the name field.
| filter | string Default: "{}" Example: filter={"lifecycle_stage":"lead"} String-encoded JSON object. Invalid JSON → 400 Grammar: exact match ( Filter values are type-checked against the target field before the query
runs — a mistyped value returns 400 |
| sort | string Default: "-created_at" Field name; |
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
| search | string Free-text search (fields vary per resource). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "color": "string",
- "type": "contact",
- "usage_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Tag names are unique per workspace (case-insensitive). A name that already exists
returns 409 Conflict — look up the existing record (list with search=<name>)
and reuse it.
| name | string [ 1 .. 100 ] characters |
| color | string <= 20 characters |
| type | string Default: "both" Enum: "contact" "conversation" "both" |
{- "name": "string",
- "color": "string",
- "type": "contact"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "color": "string",
- "type": "contact",
- "usage_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "color": "string",
- "type": "contact",
- "usage_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Same fields as create, all optional.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| name | string [ 1 .. 100 ] characters |
| color | string <= 20 characters |
| type | string Default: "both" Enum: "contact" "conversation" "both" |
{- "name": "string",
- "color": "string",
- "type": "contact"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "color": "string",
- "type": "contact",
- "usage_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Standard list conventions; search matches the name field.
| filter | string Default: "{}" Example: filter={"lifecycle_stage":"lead"} String-encoded JSON object. Invalid JSON → 400 Grammar: exact match ( Filter values are type-checked against the target field before the query
runs — a mistyped value returns 400 |
| sort | string Default: "-created_at" Field name; |
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
| search | string Free-text search (fields vary per resource). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "color": "string",
- "is_system": true,
- "contact_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Group names are unique per workspace (case-insensitive). A name that already exists returns 409 Conflict.
| name | string [ 1 .. 100 ] characters |
| description | string <= 500 characters |
| color | string <= 20 characters |
{- "name": "string",
- "description": "string",
- "color": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "color": "string",
- "is_system": true,
- "contact_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "color": "string",
- "is_system": true,
- "contact_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Same fields as create, all optional.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| name | string [ 1 .. 100 ] characters |
| description | string <= 500 characters |
| color | string <= 20 characters |
{- "name": "string",
- "description": "string",
- "color": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "color": "string",
- "is_system": true,
- "contact_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Standard list conventions. List rows omit variables and audience_filters —
fetch a single campaign to read them.
| filter | string Default: "{}" Example: filter={"lifecycle_stage":"lead"} String-encoded JSON object. Invalid JSON → 400 Grammar: exact match ( Filter values are type-checked against the target field before the query
runs — a mistyped value returns 400 |
| sort | string Default: "-created_at" Field name; |
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
| search | string Free-text search (fields vary per resource). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "status": "string",
- "type": "broadcast",
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "template_name": "string",
- "audience_baseline": "string",
- "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "custom_message": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "read_count": 0,
- "failed_count": 0,
- "reply_count": 0,
- "pending_retry_count": 0,
- "instance_id": "06587974-2dbe-4e10-8bf9-38cce0f5a366",
- "variables": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}If the campaign is saved with status: "scheduled" and a scheduled_at, it is
automatically queued to execute at scheduled_at — you do not need to call
/execute. Any change to the campaign re-syncs the schedule.
At send time, the audience can only narrow the built-in WhatsApp send-eligibility baseline (subscribed, not blocked, deliverable) — never widen it.
| name | string [ 1 .. 200 ] characters |
| description | string <= 2000 characters |
| status | string Enum: "draft" "scheduled"
|
| type | string Enum: "broadcast" "drip" "triggered" Omitted on create → |
| template_id | string <uuid> WhatsApp template to send. |
| template_name | string <= 512 characters |
| audience_id | string <uuid> A saved audience; takes precedence over |
| audience_filters | object A |
| custom_message | string |
| scheduled_at | string <date-time> ISO 8601. A date-only value ("2026-07-01") is accepted and interpreted as a
date; an unparseable value returns 400
|
| timezone | string <= 64 characters Omitted → |
| instance_id | string <uuid> WhatsApp instance (phone number connection) to send from. |
| variables | object Template variable mappings. |
{- "name": "string",
- "description": "string",
- "status": "draft",
- "type": "broadcast",
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "template_name": "string",
- "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "custom_message": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "instance_id": "06587974-2dbe-4e10-8bf9-38cce0f5a366",
- "variables": { }
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "status": "string",
- "type": "broadcast",
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "template_name": "string",
- "audience_baseline": "string",
- "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "custom_message": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "read_count": 0,
- "failed_count": 0,
- "reply_count": 0,
- "pending_retry_count": 0,
- "instance_id": "06587974-2dbe-4e10-8bf9-38cce0f5a366",
- "variables": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "status": "string",
- "type": "broadcast",
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "template_name": "string",
- "audience_baseline": "string",
- "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "custom_message": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "read_count": 0,
- "failed_count": 0,
- "reply_count": 0,
- "pending_retry_count": 0,
- "instance_id": "06587974-2dbe-4e10-8bf9-38cce0f5a366",
- "variables": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Same fields as create, all optional; same audience validation and scheduling
re-sync. The API can only set status to draft or scheduled. There is no
restriction based on the campaign's current status — patching a running/completed
campaign is possible and re-syncs scheduling, so take care not to accidentally
re-schedule a finished campaign.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| name | string [ 1 .. 200 ] characters |
| description | string <= 2000 characters |
| status | string Enum: "draft" "scheduled"
|
| type | string Enum: "broadcast" "drip" "triggered" Omitted on create → |
| template_id | string <uuid> WhatsApp template to send. |
| template_name | string <= 512 characters |
| audience_id | string <uuid> A saved audience; takes precedence over |
| audience_filters | object A |
| custom_message | string |
| scheduled_at | string <date-time> ISO 8601. A date-only value ("2026-07-01") is accepted and interpreted as a
date; an unparseable value returns 400
|
| timezone | string <= 64 characters Omitted → |
| instance_id | string <uuid> WhatsApp instance (phone number connection) to send from. |
| variables | object Template variable mappings. |
{- "name": "string",
- "description": "string",
- "status": "draft",
- "type": "broadcast",
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "template_name": "string",
- "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "custom_message": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "instance_id": "06587974-2dbe-4e10-8bf9-38cce0f5a366",
- "variables": { }
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "status": "string",
- "type": "broadcast",
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "template_name": "string",
- "audience_baseline": "string",
- "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "custom_message": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "read_count": 0,
- "failed_count": 0,
- "reply_count": 0,
- "pending_retry_count": 0,
- "instance_id": "06587974-2dbe-4e10-8bf9-38cce0f5a366",
- "variables": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}The campaign must have status: "scheduled" — a draft must first be PATCHed to
scheduled. No request body.
Success answers 200; failures use real error statuses with the structured
{"error": {"code", "message"}} envelope (404 campaign_not_found,
409 campaign_not_scheduled).
A campaign created via POST /v1/campaigns without an explicit status defaults
to draft, so a naive create → execute sequence returns the 409 — set
status: "scheduled" on create, or PATCH it, before executing.
Execution is queued with a per-campaign job id, so repeated execute calls while a
run is queued do not enqueue duplicates. Delivery progress is reflected in the
campaign's counters, which you can poll via GET /v1/campaigns/{id}.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "success": true,
- "message": "Campaign queued for execution",
- "jobId": "execute-1f2e3d4c-..."
}Standard list conventions. Only templates with an approved status can be delivered
by WhatsApp.
| filter | string Default: "{}" Example: filter={"lifecycle_stage":"lead"} String-encoded JSON object. Invalid JSON → 400 Grammar: exact match ( Filter values are type-checked against the target field before the query
runs — a mistyped value returns 400 |
| sort | string Default: "-created_at" Field name; |
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
| search | string Free-text search (fields vary per resource). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "display_name": "string",
- "category": "string",
- "language": "string",
- "status": "string",
- "header_type": "string",
- "header_text": "string",
- "body_text": "string",
- "body_variables_examples": [
- "string"
], - "footer_text": "string",
- "buttons": [
- { }
], - "meta_template_id": "string",
- "rejection_reason": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "display_name": "string",
- "category": "string",
- "language": "string",
- "status": "string",
- "header_type": "string",
- "header_text": "string",
- "body_text": "string",
- "body_variables_examples": [
- "string"
], - "footer_text": "string",
- "buttons": [
- { }
], - "meta_template_id": "string",
- "rejection_reason": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Sends the template to a phone number. The template must belong to your workspace (404 otherwise). The recipient contact is matched by normalized phone — created automatically if it doesn't exist — and the sent message is recorded in the Inbox conversation.
The nested shapes of body_variables / header_config / button_configs are
strictly validated on the request: a malformed entry (e.g.
body_variables: [null], or a button without parameters) returns 400 in the
standard validation shape, where message is an array of per-field strings (e.g.
["body_variables.0.text must be a string"]). Unknown properties inside these
nested objects are rejected too (property <name> should not exist). Content that
only the WhatsApp provider rejects still surfaces as a 502.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| to required | string Recipient phone, international format (e.g. |
Array of objects Values for the template's body placeholders. | |
object
| |
Array of objects Values for dynamic buttons (e.g. dynamic-URL suffixes). The set of valid
|
{- "to": "string",
- "body_variables": [
- {
- "type": "text",
- "text": "string",
- "param_name": "string"
}
], - "header_config": {
- "type": "text",
- "variables": [
- "string"
], - "media_type": "image",
- "media_link": "string"
}, - "button_configs": [
- {
- "type": "string",
- "index": 0,
- "parameters": [
- "string"
]
}
]
}{- "success": true,
- "wamid": "string",
- "message_id": "d7d9d9fd-478f-40e6-b651-49b7f19878a2",
- "conversation_id": "cc71b11a-25cd-4c2d-9950-df2cc38e3407"
}Returns a JSON array (no pagination envelope) of the workspace's pipelines,
ordered by position, each with its ordered stages. Exactly one pipeline per
workspace has is_default: true. Use this to map stage ids before creating or
moving deals.
[- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "description": "string",
- "is_default": true,
- "position": 0,
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "stages": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "name": "string",
- "color": "string",
- "position": 0,
- "win_probability": 100,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}
]Uses dedicated query parameters (not the generic filter) and its own pagination
defaults: limit default 25 (cap 100), offset min 0. Absent or empty
limit/offset values default; malformed values return 400
("Invalid limit: must be a non-negative integer"). The UUID query parameters are
validated — a malformed value returns 400 ("Invalid pipeline_id: must be a UUID"), while an empty value (?pipeline_id=) is treated as absent. Unrecognized
enum filter values are still silently ignored rather than returning 400. Results
are ordered newest-first. Rows include the joined
contact_name/contact_phone/contact_email.
| pipeline_id | string <uuid> Exact match — take the id from |
| stage_id | string <uuid> Exact match. Malformed → 400 |
| status | string Enum: "open" "won" "lost" Any other value is silently ignored (unfiltered result). |
| contact_id | string <uuid> Exact match. Malformed → 400 |
| owner_user_id | string <uuid> Exact match. Malformed → 400 |
| external_reference | string Exact match — look up a deal by your idempotency reference. |
| search | string Case-insensitive match over deal title + contact name/phone/email. |
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100. Absent or empty values default; a malformed value returns
400 |
| offset | integer >= 0 Default: 0 Default 0, min 0. Absent or empty values default; a malformed value returns 400
|
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "status": "open",
- "lost_reason": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "stage_entered_at": "2019-08-24T14:15:22Z",
- "position": 0,
- "note": "string",
- "external_reference": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Creates a deal — or, when external_reference matches an existing deal in the
workspace, updates it:
product_id (when a product
reference resolves), title (still ignored while a product is attached),
amount, currency, owner_user_id, expected_close_at, note.contact_id is always re-applied from the freshly resolved contact — a repeat
POST with different phone/email re-points the deal to that contact. Every repeat
POST must still carry contact_id or phone/email.stage_id, if present and different from the current stage, moves the deal
(same ledger/automation effects as POST /v1/deals/{id}/stage).status is never touched on a match; pipeline_id is ignored on a match.Contact resolution is shared with the contacts upsert: contact_id wins; otherwise
phone/email are upserted (phone match wins, email fallback). While a product is
attached, the deal's title derives from the product name and any client-sent
title is ignored; a missing amount defaults to the product's price.
The response is 201 in both cases and carries a top-level boolean duplicate
field — false when this request created the deal, true when the
external_reference matched an existing deal.
| contact_id | string <uuid> Existing contact (404 if not in this workspace). Wins over |
| phone | string <= 32 characters International format preferred. |
string <email> | |
| name | string <= 200 characters Used only when a new contact is created. |
| title | string <= 200 characters Required unless a product is attached (then derived from the product name). |
| product_id | string <uuid> Attach a product by id. |
| product_sku | string <= 120 characters Attach by SKU. |
| product_external_id | string <= 255 characters Attach by your external product id. |
| amount | number [ 0 .. 9999999999 ] Rounded to 2 decimals. Omitted → product price if a product is attached, else 0. |
| currency | string <= 8 characters Uppercased. Omitted → workspace default currency. |
| pipeline_id | string <uuid> Omitted → the workspace default pipeline. Ignored on an |
| stage_id | string <uuid> Omitted → the pipeline's first stage. On an |
| owner_user_id | string <uuid> Must be a workspace agent. Omitted → the deal is unowned. |
| expected_close_at | string <date-time> |
| note | string <= 4000 characters |
| external_reference | string <= 255 characters Idempotency key, unique per workspace. |
{- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "phone": "string",
- "email": "user@example.com",
- "name": "string",
- "title": "string",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "product_sku": "string",
- "product_external_id": "string",
- "amount": 9999999999,
- "currency": "string",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "external_reference": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "status": "open",
- "lost_reason": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "stage_entered_at": "2019-08-24T14:15:22Z",
- "position": 0,
- "note": "string",
- "external_reference": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "duplicate": true
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "status": "open",
- "lost_reason": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "stage_entered_at": "2019-08-24T14:15:22Z",
- "position": 0,
- "note": "string",
- "external_reference": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string"
}Field updates do not write stage-history entries and do not fire deal
automations — only the /stage and /status routes do.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| product_id | string or null <uuid> UUID attaches/replaces (must be active unless it's the already-attached
product); |
| title | string <= 200 characters Ignored while a product is (or ends up) attached. |
| amount | number [ 0 .. 9999999999 ] Rounded to 2 decimals. |
| currency | string <= 8 characters Uppercased. |
| contact_id | string <uuid> Re-points the deal; 404 "Contact not found" if not in this workspace. |
| owner_user_id | string or null <uuid>
|
| expected_close_at | string or null <date-time>
|
| note | string or null <= 4000 characters Trimmed; empty becomes |
{- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "title": "string",
- "amount": 9999999999,
- "currency": "string",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "note": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "status": "open",
- "lost_reason": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "stage_entered_at": "2019-08-24T14:15:22Z",
- "position": 0,
- "note": "string",
- "external_reference": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string"
}Move a deal to any stage in the workspace — cross-pipeline moves are supported; the
deal's pipeline_id follows the target stage. A move to the deal's current
stage is a pure reorder (no history entry, no automations). A genuine stage change
stamps stage_entered_at, records a stage-transition history entry and activity
row, and fires deal-stage-changed automations.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| stage_id required | string <uuid> Target stage. |
| index | integer [ 0 .. 100000 ] Position within the stage column, 0 = top. Omitted → top. |
{- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "index": 100000
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "status": "open",
- "lost_reason": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "stage_entered_at": "2019-08-24T14:15:22Z",
- "position": 0,
- "note": "string",
- "external_reference": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string"
}Closing (won/lost) stamps closed_at and keeps the deal on its last stage.
Reopening (open) clears closed_at and lost_reason. won/lost fire the
corresponding deal automations (reopen fires none). If the deal already has the
requested status, the current row is returned unchanged (no history entry, no
automations).
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| status required | string Enum: "open" "won" "lost"
|
| lost_reason | string <= 1000 characters Stored only when status is |
{- "status": "open",
- "lost_reason": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "status": "open",
- "lost_reason": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "stage_entered_at": "2019-08-24T14:15:22Z",
- "position": 0,
- "note": "string",
- "external_reference": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string"
}The workspace product catalog shared by deals and payments — list, get, create (idempotent upsert via external_id), update. No DELETE; deactivate with is_active: false.
Standard list envelope, newest first. Filters combine (AND). q is a literal
substring match on name or description (case-insensitive; %/_ are not
wildcards); sku and external_id are exact-match lookups.
| q | string Substring match on |
| sku | string Exact-match lookup by SKU. |
| external_id | string Exact-match lookup by external id. |
| is_active | boolean
|
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "is_active": true,
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}When external_id matches an existing product, that product's fields are
updated instead of a new one being created and the response carries
duplicate: true (201 for both outcomes). sku and external_id are each
per-workspace-unique — a clash with a different product answers 409
product_conflict. vat_mode + vat_rate travel as one both-or-neither pair
(400 when only one leg is sent).
| name required | string [ 1 .. 200 ] characters |
| sku | string or null <= 100 characters Per-workspace-unique product code (human-facing). |
| external_id | string or null <= 200 characters Per-workspace-unique id of this product in your system — the POST idempotency key (a repeat POST with the same value updates that product instead of creating a duplicate). |
| description | string or null <= 2000 characters |
| price | number or null >= 0 Default price in the workspace payment currency. |
| vat_mode | string or null Enum: "inclusive" "exclusive" null Per-product VAT override. Travels with |
| vat_rate | number or null [ 0 .. 100 ] VAT percent (0–100, max 2 decimals); paired with |
| is_active | boolean Inactive products stay on existing deals/payments but cannot be attached to new ones. |
{- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "vat_mode": "inclusive",
- "vat_rate": 100,
- "is_active": true
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "is_active": true,
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "duplicate": true
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "is_active": true,
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Partial update — only the fields present in the body change (a field you don't
send is never nulled). There is no DELETE: deactivate with
{"is_active": false} so records that reference the product stay intact.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| name | string [ 1 .. 200 ] characters |
| sku | string or null <= 100 characters Per-workspace-unique product code (human-facing). |
| external_id | string or null <= 200 characters Per-workspace-unique id of this product in your system — the POST idempotency key (a repeat POST with the same value updates that product instead of creating a duplicate). |
| description | string or null <= 2000 characters |
| price | number or null >= 0 Default price in the workspace payment currency. |
| vat_mode | string or null Enum: "inclusive" "exclusive" null Per-product VAT override. Travels with |
| vat_rate | number or null [ 0 .. 100 ] VAT percent (0–100, max 2 decimals); paired with |
| is_active | boolean Inactive products stay on existing deals/payments but cannot be attached to new ones. |
{- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "vat_mode": "inclusive",
- "vat_rate": 100,
- "is_active": true
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "is_active": true,
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Uses dedicated query parameters and its own pagination defaults (limit default 25,
cap 100). Absent or empty limit/offset values default; malformed values return
400 ("Invalid limit: must be a non-negative integer"). Unrecognized enum values
are still silently ignored. Ordered by purchase_date descending. Rows include
joined contact_name/contact_phone/contact_email. List rows do not include
entries.
| type | string Enum: "one_time" "recurring" "installments" Other values are silently ignored. |
| status | string Enum: "active" "completed" "cancelled" Arrangement status: other values silently ignored. |
| search | string Case-insensitive match over payment title + contact name/phone/email. |
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100. Absent or empty values default; a malformed value returns
400 |
| offset | integer >= 0 Default: 0 Default 0, min 0. Absent or empty values default; a malformed value returns 400
|
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "type": "one_time",
- "title": "string",
- "note": "string",
- "currency": "string",
- "total_amount": 0,
- "arrangement_status": "active",
- "method": "cash",
- "purchase_date": "2019-08-24T14:15:22Z",
- "recurring_interval": "weekly",
- "recurring_next_due_at": "2019-08-24T14:15:22Z",
- "recurring_auto_generate": true,
- "recurring_cancelled_at": "2019-08-24T14:15:22Z",
- "recurring_end_at": "2019-08-24T14:15:22Z",
- "recurring_max_occurrences": 0,
- "recurring_payment_method_id": "40505daa-dc68-4f54-9b57-2fe1c01c228d",
- "recurring_last_attempt_at": "2019-08-24T14:15:22Z",
- "recurring_failure_count": 0,
- "recurring_paused_at": "2019-08-24T14:15:22Z",
- "recurring_next_retry_at": "2019-08-24T14:15:22Z",
- "recurring_dunning_started_at": "2019-08-24T14:15:22Z",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "installment_count": 0,
- "external_reference": "string",
- "source": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Creates a payment — or, when external_reference matches an existing payment,
updates it. Only mutable fields are applied on a match:
product_id, title (subject to the product title lock),
note, method, metadata (the provided object replaces the stored one —
omit it to keep it).one_time: amount, status.recurring: auto_generate, recurring_end_at,
recurring_max_occurrences, and the vat_mode+vat_rate pair (a full pair
re-prices the plan — future cycles are charged and documented with the new
posture).type, interval, installment_count,
purchase_date — and, unlike deals, the contact is NOT re-pointed on a match.Entry creation per type: one_time — one entry with the given status
(the header's arrangement_status mirrors it); recurring — the first cycle is
recorded now when record_first_payment is true (default), further cycles record
automatically when auto_generate is on; installments — installment_count
monthly entries starting at purchase_date, recognized as revenue immediately.
Contact resolution is identical to deals (409 CONTACT_MERGE_REQUIRED on identity
conflict). Unlike deals, amount is always required — the product price does not
substitute for it. The response is 201 in both cases and carries a top-level
boolean duplicate field — false when this request created the payment, true
when the external_reference matched an existing payment.
| contact_id | string <uuid> Existing contact (404 if not in this workspace). Wins over |
| phone | string <= 32 characters |
string <email> | |
| name | string <= 200 characters Used only for a newly created contact. |
| type required | string Enum: "one_time" "recurring" "installments" |
| amount required | number [ 0 .. 9999999999 ] one_time: the charge amount; recurring: amount per cycle; installments: the total deal amount. Always required — the product price does not substitute for it. |
| product_id | string <uuid> |
| product_sku | string <= 120 characters |
| product_external_id | string <= 255 characters |
| title | string <= 200 characters Ignored while a product is attached (title derives from the product name). |
| note | string <= 1000 characters |
| method | string Enum: "cash" "card" "bank_transfer" "other" |
| currency | string <= 3 characters Uppercased. Omitted → workspace default currency. |
| purchase_date | string ISO 8601. A date-only value ("2026-07-14") is interpreted as start of that day in the workspace timezone. Omitted → now. |
| status | string Enum: "pending" "completed" "failed" "refunded" One-time only. Omitted → |
| interval | string Enum: "weekly" "monthly" "quarterly" "yearly" Recurring only. Omitted → |
| auto_generate | boolean Recurring only: automatically record each cycle when due. |
| record_first_payment | boolean Default: true Recurring only: record the first cycle immediately. |
| recurring_end_at | string Recurring only; ISO 8601 — a date-only value means end of that day in the workspace timezone; must be after the purchase date. |
| recurring_max_occurrences | integer >= 1 Recurring only — total charge cycles ever. |
| vat_mode | string Enum: "inclusive" "exclusive" Recurring plans only, always together with |
| vat_rate | number [ 0 .. 100 ] Recurring only, always together with |
| installment_count | integer [ 2 .. 360 ] Required when |
| external_reference | string <= 255 characters Idempotency key, unique per workspace. |
| metadata | object Free-form JSON stored on the payment — max 2048 bytes serialized (400
|
{- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "phone": "string",
- "email": "user@example.com",
- "name": "string",
- "type": "one_time",
- "amount": 9999999999,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "product_sku": "string",
- "product_external_id": "string",
- "title": "string",
- "note": "string",
- "method": "cash",
- "currency": "str",
- "purchase_date": "string",
- "status": "pending",
- "interval": "weekly",
- "auto_generate": true,
- "record_first_payment": true,
- "recurring_end_at": "string",
- "recurring_max_occurrences": 1,
- "vat_mode": "inclusive",
- "vat_rate": 100,
- "installment_count": 2,
- "external_reference": "string",
- "metadata": { }
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "type": "one_time",
- "title": "string",
- "note": "string",
- "currency": "string",
- "total_amount": 0,
- "arrangement_status": "active",
- "method": "cash",
- "purchase_date": "2019-08-24T14:15:22Z",
- "recurring_interval": "weekly",
- "recurring_next_due_at": "2019-08-24T14:15:22Z",
- "recurring_auto_generate": true,
- "recurring_cancelled_at": "2019-08-24T14:15:22Z",
- "recurring_end_at": "2019-08-24T14:15:22Z",
- "recurring_max_occurrences": 0,
- "recurring_payment_method_id": "40505daa-dc68-4f54-9b57-2fe1c01c228d",
- "recurring_last_attempt_at": "2019-08-24T14:15:22Z",
- "recurring_failure_count": 0,
- "recurring_paused_at": "2019-08-24T14:15:22Z",
- "recurring_next_retry_at": "2019-08-24T14:15:22Z",
- "recurring_dunning_started_at": "2019-08-24T14:15:22Z",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "installment_count": 0,
- "external_reference": "string",
- "source": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "entries": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "sequence": 0,
- "amount": 0,
- "currency": "string",
- "status": "pending",
- "due_date": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "recognized_amount": 0,
- "recognized_at": "2019-08-24T14:15:22Z",
- "kind": "charge",
- "refunds_entry_id": "266ec3df-657a-4943-bda5-a4e4534375ee",
- "note": "string",
- "provider_refund_ref": "string",
- "refund_idempotency_key": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "duplicate": true
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "type": "one_time",
- "title": "string",
- "note": "string",
- "currency": "string",
- "total_amount": 0,
- "arrangement_status": "active",
- "method": "cash",
- "purchase_date": "2019-08-24T14:15:22Z",
- "recurring_interval": "weekly",
- "recurring_next_due_at": "2019-08-24T14:15:22Z",
- "recurring_auto_generate": true,
- "recurring_cancelled_at": "2019-08-24T14:15:22Z",
- "recurring_end_at": "2019-08-24T14:15:22Z",
- "recurring_max_occurrences": 0,
- "recurring_payment_method_id": "40505daa-dc68-4f54-9b57-2fe1c01c228d",
- "recurring_last_attempt_at": "2019-08-24T14:15:22Z",
- "recurring_failure_count": 0,
- "recurring_paused_at": "2019-08-24T14:15:22Z",
- "recurring_next_retry_at": "2019-08-24T14:15:22Z",
- "recurring_dunning_started_at": "2019-08-24T14:15:22Z",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "installment_count": 0,
- "external_reference": "string",
- "source": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "entries": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "sequence": 0,
- "amount": 0,
- "currency": "string",
- "status": "pending",
- "due_date": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "recognized_amount": 0,
- "recognized_at": "2019-08-24T14:15:22Z",
- "kind": "charge",
- "refunds_entry_id": "266ec3df-657a-4943-bda5-a4e4534375ee",
- "note": "string",
- "provider_refund_ref": "string",
- "refund_idempotency_key": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}A one-time amount/status change flows into the payment's single entry and
re-maps the header's arrangement_status. Shortening a recurring plan's end
conditions may auto-complete it; extending them never silently reactivates a
completed plan. A one-time status change fires the matching automation after the
update.
A full vat_mode+vat_rate pair replaces a recurring plan's stored VAT
posture (future cycles are charged and documented with the new pair) — lone legs
and nulls are rejected, and on non-recurring payments the pair returns 400.
metadata replaces the stored object (null clears it; omit to keep it).
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| product_id | string or null <uuid> Attach/replace (must be active unless already attached) or detach ( |
| title | string <= 200 characters Ignored while a product is attached. |
| note | string <= 1000 characters |
| method | string Enum: "cash" "card" "bank_transfer" "other" |
| amount | number [ 0 .. 9999999999 ] One-time only (silently ignored otherwise). |
| status | string Enum: "pending" "completed" "failed" "refunded" One-time only. |
| auto_generate | boolean Recurring only. |
| recurring_end_at | string or null Recurring only; |
| recurring_max_occurrences | integer or null >= 1 Recurring only; |
| vat_mode | string Enum: "inclusive" "exclusive" Recurring only: replace the plan's stored VAT pair — always together with
|
| vat_rate | number [ 0 .. 100 ] Recurring only, always together with |
| metadata | object or null Replace the payment's free-form metadata object (max 2048 bytes serialized),
or |
{- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "title": "string",
- "note": "string",
- "method": "cash",
- "amount": 9999999999,
- "status": "pending",
- "auto_generate": true,
- "recurring_end_at": "string",
- "recurring_max_occurrences": 1,
- "vat_mode": "inclusive",
- "vat_rate": 100,
- "metadata": { }
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "type": "one_time",
- "title": "string",
- "note": "string",
- "currency": "string",
- "total_amount": 0,
- "arrangement_status": "active",
- "method": "cash",
- "purchase_date": "2019-08-24T14:15:22Z",
- "recurring_interval": "weekly",
- "recurring_next_due_at": "2019-08-24T14:15:22Z",
- "recurring_auto_generate": true,
- "recurring_cancelled_at": "2019-08-24T14:15:22Z",
- "recurring_end_at": "2019-08-24T14:15:22Z",
- "recurring_max_occurrences": 0,
- "recurring_payment_method_id": "40505daa-dc68-4f54-9b57-2fe1c01c228d",
- "recurring_last_attempt_at": "2019-08-24T14:15:22Z",
- "recurring_failure_count": 0,
- "recurring_paused_at": "2019-08-24T14:15:22Z",
- "recurring_next_retry_at": "2019-08-24T14:15:22Z",
- "recurring_dunning_started_at": "2019-08-24T14:15:22Z",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "installment_count": 0,
- "external_reference": "string",
- "source": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "entries": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "sequence": 0,
- "amount": 0,
- "currency": "string",
- "status": "pending",
- "due_date": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "recognized_amount": 0,
- "recognized_at": "2019-08-24T14:15:22Z",
- "kind": "charge",
- "refunds_entry_id": "266ec3df-657a-4943-bda5-a4e4534375ee",
- "note": "string",
- "provider_refund_ref": "string",
- "refund_idempotency_key": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Cancels a recurring plan. No request body. Already-recorded entries are untouched.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "type": "one_time",
- "title": "string",
- "note": "string",
- "currency": "string",
- "total_amount": 0,
- "arrangement_status": "active",
- "method": "cash",
- "purchase_date": "2019-08-24T14:15:22Z",
- "recurring_interval": "weekly",
- "recurring_next_due_at": "2019-08-24T14:15:22Z",
- "recurring_auto_generate": true,
- "recurring_cancelled_at": "2019-08-24T14:15:22Z",
- "recurring_end_at": "2019-08-24T14:15:22Z",
- "recurring_max_occurrences": 0,
- "recurring_payment_method_id": "40505daa-dc68-4f54-9b57-2fe1c01c228d",
- "recurring_last_attempt_at": "2019-08-24T14:15:22Z",
- "recurring_failure_count": 0,
- "recurring_paused_at": "2019-08-24T14:15:22Z",
- "recurring_next_retry_at": "2019-08-24T14:15:22Z",
- "recurring_dunning_started_at": "2019-08-24T14:15:22Z",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "installment_count": 0,
- "external_reference": "string",
- "source": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "entries": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "sequence": 0,
- "amount": 0,
- "currency": "string",
- "status": "pending",
- "due_date": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "recognized_amount": 0,
- "recognized_at": "2019-08-24T14:15:22Z",
- "kind": "charge",
- "refunds_entry_id": "266ec3df-657a-4943-bda5-a4e4534375ee",
- "note": "string",
- "provider_refund_ref": "string",
- "refund_idempotency_key": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Marking completed stamps paid_at and recognizes the revenue. A one-time header
mirrors its entry's status; recurring/installment headers are not changed by
marking one cycle. Status-change automations fire as on PATCH (payment-recorded only
for a newly recognized completion).
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| entryId required | string <uuid> The entry to mark; must belong to the payment in the URL. |
| status required | string Enum: "pending" "completed" "failed" "refunded" |
{- "status": "pending"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "type": "one_time",
- "title": "string",
- "note": "string",
- "currency": "string",
- "total_amount": 0,
- "arrangement_status": "active",
- "method": "cash",
- "purchase_date": "2019-08-24T14:15:22Z",
- "recurring_interval": "weekly",
- "recurring_next_due_at": "2019-08-24T14:15:22Z",
- "recurring_auto_generate": true,
- "recurring_cancelled_at": "2019-08-24T14:15:22Z",
- "recurring_end_at": "2019-08-24T14:15:22Z",
- "recurring_max_occurrences": 0,
- "recurring_payment_method_id": "40505daa-dc68-4f54-9b57-2fe1c01c228d",
- "recurring_last_attempt_at": "2019-08-24T14:15:22Z",
- "recurring_failure_count": 0,
- "recurring_paused_at": "2019-08-24T14:15:22Z",
- "recurring_next_retry_at": "2019-08-24T14:15:22Z",
- "recurring_dunning_started_at": "2019-08-24T14:15:22Z",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "installment_count": 0,
- "external_reference": "string",
- "source": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "entries": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "sequence": 0,
- "amount": 0,
- "currency": "string",
- "status": "pending",
- "due_date": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "recognized_amount": 0,
- "recognized_at": "2019-08-24T14:15:22Z",
- "kind": "charge",
- "refunds_entry_id": "266ec3df-657a-4943-bda5-a4e4534375ee",
- "note": "string",
- "provider_refund_ref": "string",
- "refund_idempotency_key": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Records a refund entry against a completed charge. Multiple partial refunds against
one charge are supported, up to its recognized value; refunds are race-safe —
concurrent refund requests cannot over-refund a charge. A fully refunded
one-time payment's header becomes arrangement_status: "cancelled"; partial
refunds and multi-entry deals keep their status. Fires the payment-refunded
automation with the refunded amount.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| entry_id | string <uuid> The charge entry to refund. May be omitted only when the payment has exactly one charge. |
| amount | number ( 0 .. 9999999999 ] Partial refund amount. Omitted → the full remaining refundable balance. |
| note | string <= 1000 characters Stored on the refund entry. |
{- "entry_id": "31513983-613b-4481-b270-ae929f358bae",
- "amount": 9999999999,
- "note": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "type": "one_time",
- "title": "string",
- "note": "string",
- "currency": "string",
- "total_amount": 0,
- "arrangement_status": "active",
- "method": "cash",
- "purchase_date": "2019-08-24T14:15:22Z",
- "recurring_interval": "weekly",
- "recurring_next_due_at": "2019-08-24T14:15:22Z",
- "recurring_auto_generate": true,
- "recurring_cancelled_at": "2019-08-24T14:15:22Z",
- "recurring_end_at": "2019-08-24T14:15:22Z",
- "recurring_max_occurrences": 0,
- "recurring_payment_method_id": "40505daa-dc68-4f54-9b57-2fe1c01c228d",
- "recurring_last_attempt_at": "2019-08-24T14:15:22Z",
- "recurring_failure_count": 0,
- "recurring_paused_at": "2019-08-24T14:15:22Z",
- "recurring_next_retry_at": "2019-08-24T14:15:22Z",
- "recurring_dunning_started_at": "2019-08-24T14:15:22Z",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "installment_count": 0,
- "external_reference": "string",
- "source": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "entries": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "sequence": 0,
- "amount": 0,
- "currency": "string",
- "status": "pending",
- "due_date": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "recognized_amount": 0,
- "recognized_at": "2019-08-24T14:15:22Z",
- "kind": "charge",
- "refunds_entry_id": "266ec3df-657a-4943-bda5-a4e4534375ee",
- "note": "string",
- "provider_refund_ref": "string",
- "refund_idempotency_key": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Hosted pay-by-link requests collected through the workspace's own connected payment provider (Cardcom / Sumit). Mint, list, get, cancel — no idempotency key; a repeat POST mints a second link. Requires the workspace_payments feature.
Uses dedicated query parameters and the deals/payments pagination family
(limit default 25, cap 100; absent or empty values default; malformed values
return 400 "Invalid limit: must be a non-negative integer"). Unlike
deals/payments, an unknown status value returns 400 rather than being silently
ignored. Ordered by created_at descending. Rows include joined
contact_name/contact_phone/contact_email and a computed refunded_total.
| status | string Enum: "pending" "paid" "expired" "cancelled" Unknown values return 400. |
| contact_id | string <uuid> Malformed (non-UUID) values return 400; empty values are treated as absent. |
| deal_id | string <uuid> Malformed (non-UUID) values return 400; empty values are treated as absent. |
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100. Absent or empty values default; a malformed value returns
400 |
| offset | integer >= 0 Default: 0 Default 0, min 0. Absent or empty values default; a malformed value returns 400
|
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "provider": "cardcom",
- "status": "pending",
- "charge_kind": "checkout",
- "amount": 0,
- "currency": "ILS",
- "title": "string",
- "note": "string",
- "max_installments": 0,
- "auto_issue_document": true,
- "document_kind": "string",
- "public_token": "string",
- "provider_checkout_ref": "string",
- "provider_payment_ref": "string",
- "provider_customer_ref": "string",
- "test_mode": true,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "reminders_enabled": true,
- "reminder_count": 0,
- "last_reminder_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "contact_payment_id": "b0059909-e2a0-40f6-be35-1d854b970361",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "pay_url": "string",
- "document": { },
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "refunded_total": 0
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Mints a hosted-checkout pay-link through the workspace's connected payment
provider (Cardcom / Sumit) and returns the row with its shareable pay_url.
Requires a connected provider — otherwise 400 with
error_code: "NO_PAYMENT_PROVIDER".
NOT idempotent — there is no idempotency key on this resource. A repeat POST
mints a second, independently payable link; cancel extras via
POST /v1/payment-requests/{id}/cancel. Do not blindly retry this call after a
network failure without first checking GET /v1/payment-requests for the link
you may have already minted.
The payer contact resolves like payments/deals: contact_id wins; otherwise
phone/email are upserted like POST /v1/contacts (same
409 CONTACT_MERGE_REQUIRED behavior); a deal_id alone also works — the deal's
contact pays. None of the four → 400.
Expiry is clamped server-side to at most 72 hours from now (1 hour for test-mode
requests); reminder emails, payment verification, tax-document auto-issue, and
the payer receipt behave exactly as for in-app-minted links. The response also
carries checkout_url/checkout_error: a provider failure at mint time leaves
the row pending without a checkout session (checkout_error set) — the
public pay page lazily re-creates the session when opened, so the link still
works once the provider recovers.
| contact_id | string <uuid> Existing contact (the payer). Wins over |
| phone | string <= 32 characters Contact phone (E.164). A matching contact is used, or created. |
string <email> Contact email. A matching contact is used, or created. | |
| name | string <= 200 characters Used only when a new contact is created. |
| deal_id | string <uuid> Deal to bind the request to. When no contact is given, the deal's contact is the payer. |
| amount required | number [ 0.01 .. 9999999999.99 ] Amount to collect, in major units (at most 2 decimal places). |
| currency | string Enum: "ILS" "USD" "EUR" "GBP" Omitted → the workspace payment currency. |
| title | string <= 200 characters Payer-facing charge title. |
| note | string <= 2000 characters |
| max_installments | integer [ 1 .. 36 ] Max card installments offered on the hosted page (1–36). |
| document_kind | string Enum: "tax_invoice" "tax_invoice_receipt" "receipt" "receipt_for_invoice" "proforma_invoice" "donation_receipt" "credit_invoice" "credit_invoice_receipt" "credit_receipt" "credit_donation_receipt" "order" "price_quote" "delivery_note" "payment_demand" Tax-document kind to auto-issue on payment; omitted → the provider/account default. |
| auto_issue_document | boolean Default: true Auto-issue an Israeli tax document on successful charge. |
| expires_at | string <date-time> Link expiry (ISO 8601). Clamped server-side to at most 72 hours from now (1 hour for test-mode requests); omitted → the maximum. |
| test_mode | boolean Authorise-only test run — rejected (400) when the connected provider has no test mode. Test requests never record real money. |
| reminders_enabled | boolean Pre-expiry reminder emails for this request; omitted → the workspace default. |
| offer_card_save | boolean Offer the payer a save-my-card checkbox on the pay page — honored only when the connected provider supports card capture at checkout. |
| vat_mode | string Enum: "inclusive" "exclusive" Per-request VAT override — mode and rate travel as a PAIR (each required
when the other is present; a lone leg returns 400). Omitted → the workspace
payments default. VAT-exempt = |
| vat_rate | number [ 0 .. 100 ] VAT rate percent (0–100, at most 2 decimal places); see |
{- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "phone": "string",
- "email": "user@example.com",
- "name": "string",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "amount": 0.01,
- "currency": "ILS",
- "title": "string",
- "note": "string",
- "max_installments": 1,
- "document_kind": "tax_invoice",
- "auto_issue_document": true,
- "expires_at": "2019-08-24T14:15:22Z",
- "test_mode": true,
- "reminders_enabled": true,
- "offer_card_save": true,
- "vat_mode": "inclusive",
- "vat_rate": 100
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "provider": "cardcom",
- "status": "pending",
- "charge_kind": "checkout",
- "amount": 0,
- "currency": "ILS",
- "title": "string",
- "note": "string",
- "max_installments": 0,
- "auto_issue_document": true,
- "document_kind": "string",
- "public_token": "string",
- "provider_checkout_ref": "string",
- "provider_payment_ref": "string",
- "provider_customer_ref": "string",
- "test_mode": true,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "reminders_enabled": true,
- "reminder_count": 0,
- "last_reminder_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "contact_payment_id": "b0059909-e2a0-40f6-be35-1d854b970361",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "pay_url": "string",
- "document": { },
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "refunded_total": 0,
- "checkout_url": "string",
- "checkout_error": "string"
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "provider": "cardcom",
- "status": "pending",
- "charge_kind": "checkout",
- "amount": 0,
- "currency": "ILS",
- "title": "string",
- "note": "string",
- "max_installments": 0,
- "auto_issue_document": true,
- "document_kind": "string",
- "public_token": "string",
- "provider_checkout_ref": "string",
- "provider_payment_ref": "string",
- "provider_customer_ref": "string",
- "test_mode": true,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "reminders_enabled": true,
- "reminder_count": 0,
- "last_reminder_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "contact_payment_id": "b0059909-e2a0-40f6-be35-1d854b970361",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "pay_url": "string",
- "document": { },
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "refunded_total": 0
}Withdraws the pay-link — the hosted page stops accepting payment. No request
body. Pending requests only (the cancel is a compare-and-set on status):
already paid/expired/cancelled rows answer 409, and direct saved-card charge rows
answer 409 TOKEN_REQUEST_NOT_CANCELLABLE. A payer already on the hosted page
can still complete — such late completions are verified and recorded (the row
resurrects to paid) rather than dropping the money.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "provider": "cardcom",
- "status": "pending",
- "charge_kind": "checkout",
- "amount": 0,
- "currency": "ILS",
- "title": "string",
- "note": "string",
- "max_installments": 0,
- "auto_issue_document": true,
- "document_kind": "string",
- "public_token": "string",
- "provider_checkout_ref": "string",
- "provider_payment_ref": "string",
- "provider_customer_ref": "string",
- "test_mode": true,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "reminders_enabled": true,
- "reminder_count": 0,
- "last_reminder_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "contact_payment_id": "b0059909-e2a0-40f6-be35-1d854b970361",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "pay_url": "string",
- "document": { },
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "refunded_total": 0
}Read-only listing of a contact's financial documents (invoices, receipts, credit documents), aggregated from stored document pointers with an opt-in live provider lookup. Requires the payments feature.
Read-only listing of the contact's financial documents — invoices, receipts, and
credit documents — aggregated from every stored document pointer on the contact's
payments, payment entries, and payment requests, deduplicated, merged, and sorted
date-descending (nulls last). Requires the payments plan feature (the same
gate as /v1/payments*), in addition to API access.
Responds { documents, live }. Each document carries kind (canonical type, or
null with the provider's rawType), isCredit, provider, documentId,
number, url, date, amount, currency, origin, and its sources. A
document's url may be null (legacy number-only rows) — always check before
opening.
By default only stored document pointers are returned. Pass ?live=true to
additionally query the connected payment provider for a live document listing and
merge it in (bounded to ~2.5 s; a timeout or provider failure degrades to the
stored listing, reported in live.error — never an error response). With no
connected/entitled provider the live lookup degrades to an empty listing.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| live | string Enum: "true" "false"
|
{- "documents": [
- {
- "key": "string",
- "kind": "tax_invoice",
- "rawType": "string",
- "isCredit": true,
- "provider": "cardcom",
- "documentId": "string",
- "number": "string",
- "url": "string",
- "date": "2019-08-24T14:15:22Z",
- "amount": 0,
- "currency": "string",
- "origin": "stored",
- "sources": [
- {
- "type": "contact_payment",
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "paymentId": "472e651e-5a1e-424d-8098-23858bf03ad7",
- "provider": "string"
}
]
}
], - "live": {
- "attempted": true,
- "ok": true,
- "complete": true,
- "error": "timeout"
}
}E-commerce-grade orders on contacts — idempotent create via external_reference, refund ledger, mark-paid, cancel. No update or delete routes.
Uses dedicated query parameters (no generic filter) and the deals/payments
pagination defaults (limit default 25, cap 100) — with one deliberate
difference: paging on this route never returns 400. Malformed or zero
limit/offset values silently default (25 / 0) and out-of-range values are
clamped. Unrecognized status/source values are silently ignored (unfiltered
result). Ordered by placed_at descending (not created_at). Rows include
the joined contact_name/contact_phone/contact_email and omit
items/refunds — fetch a single order to read them. There is no search
parameter on this route.
| status | string Enum: "pending" "paid" "partially_paid" "refunded" "partially_refunded" "voided" Financial status. Any other value is silently ignored (unfiltered result). |
| contact_id | string <uuid> Exact match. Malformed → 400 |
| source | string Exact match — |
| store_connection_id | string <uuid> Exact match on the store-sync provenance field — orders synced from a
connected store. Malformed → 400 |
| external_reference | string Exact match — look up an order by your idempotency reference. |
| placed_from | string <date-time> Orders placed at/after this instant. Unparseable → 400 |
| placed_to | string <date-time> Orders placed at/before this instant. Unparseable → 400 |
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100, floor 1. Unlike |
| offset | integer >= 0 Default: 0 Default 0, floor 0. Absent, empty, or malformed values silently default to 0; negative values are clamped to 0 — never a 400. |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "order_number": 0,
- "number": "string",
- "platform": "string",
- "source": "string",
- "store_connection_id": "b7a8f80c-9686-4d77-b9e3-5d9db722eef5",
- "store_domain": "string",
- "external_order_id": "string",
- "external_reference": "string",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "string",
- "total": 0,
- "subtotal": 0,
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "refunded_total": 0,
- "item_count": 0,
- "first_item_name": "string",
- "coupon_codes": [
- "string"
], - "placed_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "refunded_at": "2019-08-24T14:15:22Z",
- "external_updated_at": "2019-08-24T14:15:22Z",
- "payment_reference": "string",
- "payment_synced_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Creates an order — or, when external_reference matches an existing order in the
workspace, updates its mutable fields (race-safe: a concurrent create losing
the uniqueness race retries as an update against the winner). On a match:
note, coupon_codes, placed_at,
deal_id.items, currency, discount_total, shipping_total,
tax_total) apply only while the order is still pending — once paid,
money is locked and repeat POSTs silently skip these fields (corrections flow
through refunds/cancel).financial_status is never changed on a match — an exact replay of a
created-as-paid POST is a clean no-op, and a repeat POST can never resurrect a
cancelled order. Status moves ride the mark-paid/cancel/refund endpoints.contact_id or
phone/email (and phone/email upsert side effects still apply to the contact
itself).Contact resolution is shared with the contacts upsert: contact_id wins;
otherwise phone/email are upserted (phone match wins, email fallback). Line
totals are server-computed: line_total = round2(quantity × unit_price ×
(1 − discount_percent/100)); subtotal = the sum of rounded line totals;
total = max(0, subtotal − discount_total + shipping_total + tax_total).
Rounding is 2-decimal half-up.
The response is 201 in both cases with the full order (items + refunds +
joined contact identity). Unlike the other idempotent creates there is no
top-level duplicate field — both outcomes return the same body. To
distinguish, compare created_at or pre-check with
GET /v1/orders?external_reference=....
Creation fires order-created automations (plus order-paid when created as
paid), writes the contact's activity timeline, and emits registered
order.created (and order.paid) webhooks; a paid create records a completed
payment for the full order total in the order's currency on the contact's
payment history. Writes are attributed to source api.
| contact_id | string <uuid> Existing contact (404 if not in this workspace). Wins over |
| phone | string <= 32 characters International format preferred. |
string <email> | |
| name | string <= 200 characters Used only when a new contact is created. |
Array of objects (Order item payload) <= 200 items | |
| currency | string <= 8 characters Uppercased. Omitted → workspace default currency. |
| discount_total | number [ 0 .. 9999999999 ] Document-level discount. |
| shipping_total | number [ 0 .. 9999999999 ] |
| tax_total | number [ 0 .. 9999999999 ] |
| financial_status | string Default: "pending" Enum: "pending" "paid" A |
| placed_at | string <date-time> ISO 8601; defaults to now. |
| coupon_codes | Array of strings <= 50 items |
| note | string <= 5000 characters |
| deal_id | string <uuid> Must be a deal of the same contact — 404 |
| external_reference | string <= 255 characters Idempotency key, unique per workspace. |
{- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "phone": "string",
- "email": "user@example.com",
- "name": "string",
- "items": [
- {
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "product_sku": "string",
- "product_external_id": "string",
- "title": "string",
- "sku": "string",
- "unit_price": 9999999999,
- "quantity": 1,
- "discount_percent": 100
}
], - "currency": "string",
- "discount_total": 9999999999,
- "shipping_total": 9999999999,
- "tax_total": 9999999999,
- "financial_status": "pending",
- "placed_at": "2019-08-24T14:15:22Z",
- "coupon_codes": [
- "string"
], - "note": "string",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "external_reference": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "order_number": 0,
- "number": "string",
- "platform": "string",
- "source": "string",
- "store_connection_id": "b7a8f80c-9686-4d77-b9e3-5d9db722eef5",
- "store_domain": "string",
- "external_order_id": "string",
- "external_reference": "string",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "string",
- "total": 0,
- "subtotal": 0,
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "refunded_total": 0,
- "item_count": 0,
- "first_item_name": "string",
- "coupon_codes": [
- "string"
], - "placed_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "refunded_at": "2019-08-24T14:15:22Z",
- "external_updated_at": "2019-08-24T14:15:22Z",
- "payment_reference": "string",
- "payment_synced_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "position": 0,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "external_product_id": "string",
- "title": "string",
- "sku": "string",
- "quantity": 0,
- "unit_price": 0,
- "discount_percent": 100,
- "line_total": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "refunds": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "external_refund_id": "string",
- "amount": 0,
- "currency": "string",
- "reason": "string",
- "refunded_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
- "workspace_id": "0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0",
- "contact_id": "9e8d7c6b-5a49-4837-9605-f4e3d2c1b0a9",
- "contact_name": "Dana Levi",
- "contact_phone": "+972501234567",
- "contact_email": "dana@example.com",
- "order_number": 1042,
- "number": null,
- "platform": "api",
- "source": "api",
- "store_connection_id": null,
- "store_domain": null,
- "external_order_id": null,
- "external_reference": "shop-88123",
- "deal_id": null,
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "ILS",
- "total": 360,
- "subtotal": 340,
- "discount_total": 0,
- "shipping_total": 20,
- "tax_total": 0,
- "refunded_total": 0,
- "item_count": 2,
- "first_item_name": "Widget",
- "coupon_codes": [ ],
- "placed_at": "2026-07-14T09:58:00.000Z",
- "paid_at": null,
- "cancelled_at": null,
- "refunded_at": null,
- "external_updated_at": null,
- "payment_reference": null,
- "payment_synced_at": null,
- "note": null,
- "metadata": null,
- "created_by": null,
- "created_at": "2026-07-14T09:58:04.000Z",
- "updated_at": "2026-07-14T09:58:04.000Z",
- "items": [
- {
- "id": "1a2b3c4d-5e6f-4a0b-9c8d-7e6f5a4b3c2d",
- "workspace_id": "0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0",
- "order_id": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
- "position": 0,
- "product_id": null,
- "external_product_id": null,
- "title": "Widget",
- "sku": "WID-1",
- "quantity": 2,
- "unit_price": 170,
- "discount_percent": null,
- "line_total": 340,
- "created_at": "2026-07-14T09:58:04.000Z"
}
], - "refunds": [ ]
}Appends to the order's append-only refund ledger, rolls financial_status to
partially_refunded or refunded (a full-rollup refund → refunded; these two
states are reachable only through refunds, never through a status endpoint),
stamps refunded_at on the header, mirrors the refund into the recorded
payment, and fires order-refunded automations + the order.refunded webhook.
Refunds require the order to have ever been paid (paid_at set — includes
partially_paid); refunding a cancelled-but-paid order is allowed (cancellation
doesn't touch the money axis). The amount must not exceed the remaining total
(total − refunded_total).
Idempotency: a repeat POST with the same external_refund_id applies
nothing and returns duplicate: true with the current order state. Without
external_refund_id, refunds are NOT idempotent — every POST appends a new
refund; supply one whenever your system can retry.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| amount required | number ( 0 .. 9999999999 ] In the order's currency; must not exceed the remaining total
( |
| external_refund_id | string <= 255 characters Idempotency key — a repeat POST with the same value on this
order applies nothing and returns |
| reason | string <= 1000 characters |
| refunded_at | string <date-time> ISO 8601; defaults to now. |
{- "amount": 9999999999,
- "external_refund_id": "string",
- "reason": "string",
- "refunded_at": "2019-08-24T14:15:22Z"
}{- "duplicate": false,
- "order": {
- "id": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
- "workspace_id": "0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0",
- "contact_id": "9e8d7c6b-5a49-4837-9605-f4e3d2c1b0a9",
- "contact_name": "Dana Levi",
- "contact_phone": "+972501234567",
- "contact_email": "dana@example.com",
- "order_number": 1042,
- "number": null,
- "platform": "api",
- "source": "api",
- "store_connection_id": null,
- "store_domain": null,
- "external_order_id": null,
- "external_reference": "shop-88123",
- "deal_id": null,
- "financial_status": "partially_refunded",
- "fulfillment_status": "unfulfilled",
- "currency": "ILS",
- "total": 360,
- "subtotal": 340,
- "discount_total": 0,
- "shipping_total": 20,
- "tax_total": 0,
- "refunded_total": 50,
- "item_count": 2,
- "first_item_name": "Widget",
- "coupon_codes": [ ],
- "placed_at": "2026-07-14T09:58:00.000Z",
- "paid_at": "2026-07-14T10:12:00.000Z",
- "cancelled_at": null,
- "refunded_at": "2026-07-15T08:30:00.000Z",
- "external_updated_at": null,
- "payment_reference": "pay-2026-1042",
- "payment_synced_at": "2026-07-14T10:12:01.000Z",
- "note": null,
- "metadata": null,
- "created_by": null,
- "created_at": "2026-07-14T09:58:04.000Z",
- "updated_at": "2026-07-15T08:30:00.000Z",
- "items": [
- {
- "id": "1a2b3c4d-5e6f-4a0b-9c8d-7e6f5a4b3c2d",
- "workspace_id": "0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0",
- "order_id": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
- "position": 0,
- "product_id": null,
- "external_product_id": null,
- "title": "Widget",
- "sku": "WID-1",
- "quantity": 2,
- "unit_price": 170,
- "discount_percent": null,
- "line_total": 340,
- "created_at": "2026-07-14T09:58:04.000Z"
}
], - "refunds": [
- {
- "id": "2b3c4d5e-6f7a-4b1c-8d9e-0f1a2b3c4d5e",
- "workspace_id": "0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0",
- "order_id": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
- "external_refund_id": "r-1",
- "amount": 50,
- "currency": "ILS",
- "reason": "damaged",
- "refunded_at": "2026-07-15T08:30:00.000Z",
- "created_at": "2026-07-15T08:30:00.000Z"
}
]
}
}Moves financial_status to paid, records the payment — a completed one-time
payment for the full order total in the order's currency on the contact's
payment history — or links onto an existing payment via payment_reference,
and fires order-paid automations + the order.paid webhook.
Transition rules: allowed from pending and partially_paid; an already-paid
order is a no-op success (201, no side effects, no duplicate automations);
from refunded/partially_refunded/voided → 409 ORDER_ILLEGAL_TRANSITION
(refund states are set by recording refunds). paid_at is stamped on first
entry into a paid state and is historical — it is kept through later refund
states. A cancelled pending order can still be marked paid (the cancelled_at
stamp does not affect the ladder).
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| payment_reference | string <= 255 characters The |
{- "payment_reference": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "order_number": 0,
- "number": "string",
- "platform": "string",
- "source": "string",
- "store_connection_id": "b7a8f80c-9686-4d77-b9e3-5d9db722eef5",
- "store_domain": "string",
- "external_order_id": "string",
- "external_reference": "string",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "string",
- "total": 0,
- "subtotal": 0,
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "refunded_total": 0,
- "item_count": 0,
- "first_item_name": "string",
- "coupon_codes": [
- "string"
], - "placed_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "refunded_at": "2019-08-24T14:15:22Z",
- "external_updated_at": "2019-08-24T14:15:22Z",
- "payment_reference": "string",
- "payment_synced_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "position": 0,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "external_product_id": "string",
- "title": "string",
- "sku": "string",
- "quantity": 0,
- "unit_price": 0,
- "discount_percent": 100,
- "line_total": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "refunds": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "external_refund_id": "string",
- "amount": 0,
- "currency": "string",
- "reason": "string",
- "refunded_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Stamps cancelled_at and fires order-cancelled automations + the
order.cancelled webhook. No request body.
Cancellation is a stamp, not a status: financial_status keeps its last
state — recorded revenue stands until a refund is recorded (cancel a paid order,
then record refunds as money is returned). Idempotent: cancelling an
already-cancelled order is a 201 no-op (no duplicate automations). A cancelled
pending order can still be marked paid later — cancel is workflow state on the
fulfillment side, not a financial transition.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "order_number": 0,
- "number": "string",
- "platform": "string",
- "source": "string",
- "store_connection_id": "b7a8f80c-9686-4d77-b9e3-5d9db722eef5",
- "store_domain": "string",
- "external_order_id": "string",
- "external_reference": "string",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "string",
- "total": 0,
- "subtotal": 0,
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "refunded_total": 0,
- "item_count": 0,
- "first_item_name": "string",
- "coupon_codes": [
- "string"
], - "placed_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "refunded_at": "2019-08-24T14:15:22Z",
- "external_updated_at": "2019-08-24T14:15:22Z",
- "payment_reference": "string",
- "payment_synced_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "metadata": { },
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "contact_name": "string",
- "contact_phone": "string",
- "contact_email": "string",
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "position": 0,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "external_product_id": "string",
- "title": "string",
- "sku": "string",
- "quantity": 0,
- "unit_price": 0,
- "discount_percent": 100,
- "line_total": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "refunds": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "external_refund_id": "string",
- "amount": 0,
- "currency": "string",
- "reason": "string",
- "refunded_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Sends a single transactional email through your workspace's verified sending domain. Raw-content endpoint: your HTML/text is delivered verbatim — no marketing footer, no unsubscribe link, and no tracking is injected unless you opt in. Requires at least one verified sending domain with a sender profile configured in the app.
Rate limit: 300 requests / minute per key (higher than the API default).
Errors on this endpoint use the structured envelope {"error": {"code","message"}}.
Idempotency: the idempotency_key is claimed before any quota/deliverability
gate, so two concurrent requests with the same key can't both send. Replaying a
resolved key returns 200 with duplicate: true. Validation failures and most
pre-send errors release the key. At-least-once corner case: in a rare
crash-and-retry overlap right after a successful provider handoff, the message can
be delivered twice while your retry receives 200 duplicate: true — design
receivers so a duplicate email is acceptable.
Suppressed recipients are a 2xx, not an error — check status in the body; do
not treat 2xx alone as "delivered to the provider". The suppression reason is
deliberately coarse.
Tracking (opt-in): opens appends a 1×1 pixel to the HTML part; clicks wraps
absolute http(s) links in the HTML part in a first-party redirect that forwards to
your URL verbatim (mailto:, in-page anchors, relative URLs, and extremely long
URLs are never wrapped). The plain-text part is never modified. If the deployment
lacks a usable signing secret for tracking links, the message is sent untracked
rather than failing. Open/click events arrive via the opt-in
email.opened/email.clicked webhooks (first open / first click per send only).
Consent is your responsibility: this endpoint neither checks marketing consent nor creates oToK contact records for recipients. The suppression list is still enforced (fail-closed).
| to required | string [ 1 .. 320 ] characters Must be a syntactically valid email (else 422 |
| subject required | string [ 1 .. 998 ] characters No line breaks or control characters. |
| html | string ≤500 KB. Delivered verbatim. At least one of |
| text | string ≤100 KB. At least one of |
| idempotency_key required | string [ 1 .. 255 ] characters Unique per workspace — makes retries safe. |
| sender_profile_id | string <uuid> Sender identity to use. Omitted → the workspace's default verified sender profile. An id from another workspace behaves like a nonexistent id (404). |
| reply_to | string <email> |
object Allowlist: only | |
object ≤2048 bytes JSON-serialized. Stored with the send and echoed verbatim in
webhook events ( | |
object Both default |
{- "to": "string",
- "subject": "string",
- "html": "string",
- "text": "string",
- "idempotency_key": "string",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "reply_to": "user@example.com",
- "headers": {
- "property1": "string",
- "property2": "string"
}, - "metadata": { },
- "tracking": {
- "opens": false,
- "clicks": false
}
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "status": "sent",
- "duplicate": true,
- "to": "string",
- "idempotency_key": "string",
- "provider_message_id": "string",
- "reason": "string",
- "created_at": "2019-08-24T14:15:22Z"
}The workspace's email suppression list — a send-time overlay deliberately separate from consent. List, idempotent add, remove. Requires the email_marketing feature.
The workspace's email suppression rows, newest first. Workspace-scoped rows only — the HQ-managed global list is enforced at send time but is never returned here.
string Exact-match filter (case-insensitive). | |
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "string",
- "reason": "unsubscribe",
- "source": "string",
- "note": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Blocks every future send to this address, independent of consent — adding a
suppression does NOT change the contact's consent state; the two compose at
send time. Re-adding an already-suppressed address returns the existing row
with duplicate: true (201 for both outcomes).
| email required | string <email> <= 255 characters |
| reason | string Enum: "unsubscribe" "bounce" "complaint" "manual" Why the address is suppressed. Defaults to |
| note | string <= 500 characters Free-form note stored with the row. |
{- "email": "user@example.com",
- "reason": "unsubscribe",
- "note": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "string",
- "reason": "unsubscribe",
- "source": "string",
- "note": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "duplicate": true
}Lifts this workspace's suppression only — it does NOT resubscribe anyone (consent state is unchanged), and HQ-managed global rows cannot be removed through the API.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "statusCode": 0,
- "message": "string",
- "error": "string",
- "error_code": "string"
}Read-only discovery of the workspace's saved audiences — the reusable targeting selectors campaigns and email campaigns accept as audience_id. Rows never include the stored definition; audiences are managed in-app and are not writable through the API.
Read-only discovery of the workspace's saved audiences — the reusable targeting
selectors POST /v1/campaigns and POST /v1/email-campaigns accept as
audience_id. Newest first. Rows carry the summary columns plus the advisory
last_count/last_counted_at size cache — never the stored definition
(see the schema). Audiences are managed in-app and are not writable through the
public API. An empty kind value (?kind=) is treated as absent; an unknown
value returns 400.
| kind | string Enum: "dynamic" "static" Exact kind filter. Unknown values return 400 ( |
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100. Absent or empty values default; a malformed value returns
400 |
| offset | integer >= 0 Default: 0 Default 0, min 0. Absent or empty values default; a malformed value returns 400
|
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "kind": "dynamic",
- "last_count": 0,
- "last_counted_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Read-only discovery of the workspace's email from-identities — the selectors email campaigns accept as sender_profile_id, each with its sending domain's verification status. Managed in-app; requires the email_marketing feature.
Read-only discovery of the workspace's email from-identities — the selectors
POST /v1/email-campaigns accepts as sender_profile_id. Newest first. Each
row carries the from identity (from_name, the composed from_email,
reply_to), the delivery provider, the default flag, and the sending-domain
linkage with its verification status — prefer profiles with verified: true
when creating campaigns (see the schema). DKIM/DNS material is never returned.
Profiles are managed in-app and are not writable through the public API.
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100. Absent or empty values default; a malformed value returns
400 |
| offset | integer >= 0 Default: 0 Default 0, min 0. Absent or empty values default; a malformed value returns 400
|
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "from_name": "string",
- "from_email": "user@example.com",
- "reply_to": "string",
- "provider": "smtp",
- "is_default": true,
- "sending_domain_id": "4dc1df09-f07f-4cc2-9e75-1240084fe120",
- "domain": "string",
- "domain_status": "pending",
- "verified": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Broadcast email campaigns — author (markdown / blocks / design_json), estimate, launch, schedule. Idempotent create via external_reference. Requires the email_marketing feature.
Newest first. List rows omit the content columns (design_json,
compiled_html, compiled_styles, plain_text) and the in-app-only A/B fields —
fetch a single campaign for the content. Delivery counters are included on list
rows. An empty status value (?status=) is treated as absent; an unknown value
returns 400.
| status | string Enum: "draft" "scheduled" "sending" "paused" "sent" "failed" "cancelled" Exact status filter. Unknown values return 400 ( |
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100. Absent or empty values default; a malformed value returns
400 |
| offset | integer >= 0 Default: 0 Default 0, min 0. Absent or empty values default; a malformed value returns 400
|
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "status": "draft",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "subject": "string",
- "preheader": "string",
- "direction": "rtl",
- "topic_key": "string",
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "open_count": 0,
- "click_count": 0,
- "bounce_count": 0,
- "complaint_count": 0,
- "unsubscribe_count": 0,
- "failed_count": 0,
- "skipped_count": 0,
- "pending_retry_count": 0,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Creates a draft broadcast email campaign. content (see the Content
input schema) maps to the campaign's email
body and compiles immediately through the same pipeline the in-app launch
gate runs — the response's compile: {ok, errors, warnings} envelope reports the
result up front, not at send time. The campaign does not send until you call
POST /v1/email-campaigns/{id}/send or …/schedule.
Targeting is optional and validated at write time: audience_id must belong to
your workspace, audience_filters is structurally validated and test-compiled
(failures return 400 with an errors array). At send time the audience can only
narrow the built-in email send-eligibility baseline (opted in, not
suppressed) — never widen it. With no targeting at all, the campaign goes to
every eligible contact — check GET /v1/email-campaigns/{id}/estimate before
sending.
Idempotency: when external_reference matches an existing campaign in the
workspace, the response carries duplicate: true and:
draft/scheduled, the call updates its
fields exactly as a PATCH would — never status or scheduled_at;sending/sent/failed/…), the campaign is
returned verbatim — nothing is mutated, nothing is recompiled, and the
response carries no compile envelope.The response is 201 in both cases. One reference maps to one campaign per workspace. A/B testing is deliberately not exposed on the public API — create A/B campaigns in-app.
| name required | string [ 1 .. 200 ] characters |
| subject required | string [ 1 .. 400 ] characters May embed |
| preheader | string <= 400 characters The inbox preview line rendered after the subject. |
| sender_profile_id required | string <uuid> A sender profile in this workspace (unknown → 400
|
| external_reference | string <= 255 characters Idempotency key: a repeat POST with the same value updates the matched
campaign (while draft/scheduled) instead of creating a duplicate, and the
response carries |
required | object (ContentInput) The shared authoring contract for email-campaign bodies and newsletter issues: an
optional Total content size is capped at 512,000 characters (measured on the source and
again on the built document) — an oversized body returns 400 Variable tokens work in all three sources: any Snippets (reusable content blocks from the workspace library, managed in
Settings → Snippets) can be spliced in by UUID or by case-insensitive exact
name — via the |
| audience_id | string <uuid> A saved audience; wins over |
| audience_filters | object An ad-hoc |
| contact_group_ids | Array of strings <uuid> [ items <uuid > ] Additional contact-group targeting (OR semantics), narrowing the audience. |
| topic_key | string <= 200 characters Preference-center topic key — contacts who opted out of it are excluded. |
{- "name": "string",
- "subject": "Big news, [[first_name : there]]!",
- "preheader": "string",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "external_reference": "string",
- "content": {
- "direction": "ltr",
- "markdown": "# Hello [[first_name : there]]!\n\nWelcome to our July update.\n\n::button[Read the full story](https://example.com/blog/july)\n"
}, - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "topic_key": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "status": "draft",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "subject": "string",
- "preheader": "string",
- "direction": "rtl",
- "topic_key": "string",
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "open_count": 0,
- "click_count": 0,
- "bounce_count": 0,
- "complaint_count": 0,
- "unsubscribe_count": 0,
- "failed_count": 0,
- "skipped_count": 0,
- "pending_retry_count": 0,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "duplicate": true,
- "compile": {
- "ok": true,
- "errors": [ ],
- "warnings": [
- "Headings deeper than level 3 were clamped to level 3"
]
}
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "status": "draft",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "subject": "string",
- "preheader": "string",
- "direction": "rtl",
- "topic_key": "string",
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "open_count": 0,
- "click_count": 0,
- "bounce_count": 0,
- "complaint_count": 0,
- "unsubscribe_count": 0,
- "failed_count": 0,
- "skipped_count": 0,
- "pending_retry_count": 0,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Same field set as create minus external_reference; only present fields are
touched, and the nullable fields clear on an explicit null. A content change
recompiles (the response carries a fresh compile envelope) and detaches any
in-app template the campaign referenced — the patched content is what sends.
Campaigns the launch already claimed (sending/sent/failed/…) answer 409
campaign_not_editable; a scheduled campaign stays scheduled (PATCH never
touches status or scheduled_at — use …/schedule to move the launch time).
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| name | string [ 1 .. 200 ] characters |
| subject | string or null [ 1 .. 400 ] characters
|
| preheader | string or null <= 400 characters |
| sender_profile_id | string <uuid> |
object (ContentInput) The shared authoring contract for email-campaign bodies and newsletter issues: an
optional Total content size is capped at 512,000 characters (measured on the source and
again on the built document) — an oversized body returns 400 Variable tokens work in all three sources: any Snippets (reusable content blocks from the workspace library, managed in
Settings → Snippets) can be spliced in by UUID or by case-insensitive exact
name — via the | |
| audience_id | string or null <uuid> |
| audience_filters | object or null |
| contact_group_ids | Array of strings or null <uuid> [ items <uuid > ] |
| topic_key | string or null <= 200 characters |
{- "name": "string",
- "subject": "string",
- "preheader": "string",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "content": {
- "direction": "ltr",
- "markdown": "# Hello [[first_name : there]]!\n\nWelcome to our July update.\n\n::button[Read the full story](https://example.com/blog/july)\n"
}, - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "topic_key": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "status": "draft",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "subject": "string",
- "preheader": "string",
- "direction": "rtl",
- "topic_key": "string",
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "open_count": 0,
- "click_count": 0,
- "bounce_count": 0,
- "complaint_count": 0,
- "unsubscribe_count": 0,
- "failed_count": 0,
- "skipped_count": 0,
- "pending_retry_count": 0,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "duplicate": true,
- "compile": {
- "ok": true,
- "errors": [ ],
- "warnings": [
- "Headings deeper than level 3 were clamped to level 3"
]
}
}Runs the campaign's stored targeting through the same resolver pipeline the
send path uses — the email consent + suppression baseline, then the saved
audience or ad-hoc filters, contact-group narrowing, and topic_key opt-outs —
so the estimate always matches send-time resolution (minus recipients already
claimed mid-flight). Update targeting first (PATCH), then re-estimate.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "estimated_recipients": 1284
}Draft/scheduled campaigns only. No request body. Marks the campaign scheduled
for now and drives the launch synchronously through the exact in-app launch
gates — sender readiness (verified domain + legal footer fields), inline
content compile, and the content lint. On success the response is the campaign
with its post-launch status (typically sending; poll
GET /v1/email-campaigns/{id} for counter progress). A gate failure answers
422 with the gate's message and the campaign's final status — the gate marks
the campaign failed as a side effect, so error.campaign_status tells you
where it landed.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "status": "draft",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "subject": "string",
- "preheader": "string",
- "direction": "rtl",
- "topic_key": "string",
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "open_count": 0,
- "click_count": 0,
- "bounce_count": 0,
- "complaint_count": 0,
- "unsubscribe_count": 0,
- "failed_count": 0,
- "skipped_count": 0,
- "pending_retry_count": 0,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Draft/scheduled campaigns only. scheduled_at is an ISO 8601 UTC instant in the
future; the every-minute sweep launches the campaign when due (running the same
launch gates as …/send). Calling it again while still scheduled moves the
launch time.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| scheduled_at required | string <date-time> ISO 8601 UTC instant; must be in the future (otherwise 400
|
{- "scheduled_at": "2026-08-01T09:00:00Z"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "status": "draft",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "subject": "string",
- "preheader": "string",
- "direction": "rtl",
- "topic_key": "string",
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "open_count": 0,
- "click_count": 0,
- "bounce_count": 0,
- "complaint_count": 0,
- "unsubscribe_count": 0,
- "failed_count": 0,
- "skipped_count": 0,
- "pending_retry_count": 0,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}No request body. Conditional on status scheduled — when the minute sweep has
already claimed the campaign for sending, the call answers 409 already_sending
rather than silently no-opping, so you know the sends are going out.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "status": "draft",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "subject": "string",
- "preheader": "string",
- "direction": "rtl",
- "topic_key": "string",
- "contact_group_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
- "audience_filters": { },
- "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "started_at": "2019-08-24T14:15:22Z",
- "completed_at": "2019-08-24T14:15:22Z",
- "total_recipients": 0,
- "sent_count": 0,
- "delivered_count": 0,
- "open_count": 0,
- "click_count": 0,
- "bounce_count": 0,
- "complaint_count": 0,
- "unsubscribe_count": 0,
- "failed_count": 0,
- "skipped_count": 0,
- "pending_retry_count": 0,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Smart newsletters and their sequenced issues — create, author, publish, schedule. Idempotent issue create via external_reference; issue numbers are assigned at publish. Requires the newsletters feature.
Newest first. Each row carries its computed active_subscriber_count. List rows
return the documented summary fields only — fetch a single newsletter for the
full stored configuration.
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100. Absent or empty values default; a malformed value returns
400 |
| offset | integer >= 0 Default: 0 Default 0, min 0. Absent or empty values default; a malformed value returns 400
|
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "description": "string",
- "status": "active",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "active_subscriber_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}A name alone suffices — cadence, enrollment policy, and archive settings take
their in-app defaults (adjust them in the app). Omit sender_profile_id to fall
back to the workspace default profile at send time. Newsletter names are unique
per workspace (case-insensitive) — a duplicate answers 409 duplicate_name.
Subject to the plan's newsletter cap (403 PLAN_LIMIT_EXCEEDED when reached).
| name required | string [ 1 .. 120 ] characters Unique per workspace, case-insensitive (409 |
| description | string <= 2000 characters |
| sender_profile_id | string <uuid> Omit to fall back to the workspace default profile at send time. Unknown →
400 |
{- "name": "string",
- "description": "string",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "description": "string",
- "status": "active",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "active_subscriber_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "description": "string",
- "status": "active",
- "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
- "active_subscriber_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Newest first. List rows omit the content columns (design_json,
compiled_html, compiled_styles, plain_text) — fetch a single issue for
those. An empty status value (?status=) is treated as absent; an unknown
value returns 400 ("Invalid status: must be draft, scheduled or published").
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| status | string Enum: "draft" "scheduled" "published" Exact status filter. Unknown values return 400; empty is treated as absent. |
| limit | integer [ 1 .. 100 ] Default: 25 Default 25, cap 100. Absent or empty values default; a malformed value returns
400 |
| offset | integer >= 0 Default: 0 Default 0, min 0. Absent or empty values default; a malformed value returns 400
|
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "newsletter_id": "854c5e2d-fe5e-4354-976d-dc3015528911",
- "issue_number": 0,
- "subject": "string",
- "preheader": "string",
- "status": "draft",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "published_at": "2019-08-24T14:15:22Z",
- "include_in_archive": true,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Creates a draft issue. All fields are optional — an issue can start as an
empty placeholder — but publishing/scheduling later requires a subject and
compiled content. content (see the Content input
schema) maps to the issue's email
body and compiles immediately; the response's compile: {ok, errors, warnings}
envelope reports the result.
Idempotency: when external_reference matches an existing issue in the
workspace, the call updates that issue's content/fields instead of creating
a new one and answers duplicate: true — a replay never touches status,
scheduled_at, or issue_number, so re-running an export pipeline can refresh
a draft's content without re-publishing anything. One reference maps to one
issue per workspace; a reference that belongs to an issue of a different
newsletter answers 409 external_reference_in_use. The response is 201 in
both cases.
Issue numbers are assigned at publish (issue_number stays null on
drafts): issue #N always means the Nth published issue.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| subject | string <= 400 characters May embed |
| preheader | string <= 400 characters |
| include_in_archive | boolean Default: true |
| external_reference | string <= 255 characters Idempotency key: a repeat POST with the same value updates the matched
issue's content/fields instead of creating a duplicate ( |
object (ContentInput) The shared authoring contract for email-campaign bodies and newsletter issues: an
optional Total content size is capped at 512,000 characters (measured on the source and
again on the built document) — an oversized body returns 400 Variable tokens work in all three sources: any Snippets (reusable content blocks from the workspace library, managed in
Settings → Snippets) can be spliced in by UUID or by case-insensitive exact
name — via the |
{- "subject": "string",
- "preheader": "string",
- "include_in_archive": true,
- "external_reference": "string",
- "content": {
- "direction": "ltr",
- "markdown": "# Hello [[first_name : there]]!\n\nWelcome to our July update.\n\n::button[Read the full story](https://example.com/blog/july)\n"
}
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "newsletter_id": "854c5e2d-fe5e-4354-976d-dc3015528911",
- "issue_number": 0,
- "subject": "string",
- "preheader": "string",
- "status": "draft",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "published_at": "2019-08-24T14:15:22Z",
- "include_in_archive": true,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "duplicate": true,
- "compile": {
- "ok": true,
- "errors": [ ],
- "warnings": [
- "Headings deeper than level 3 were clamped to level 3"
]
}
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "newsletter_id": "854c5e2d-fe5e-4354-976d-dc3015528911",
- "issue_number": 0,
- "subject": "string",
- "preheader": "string",
- "status": "draft",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "published_at": "2019-08-24T14:15:22Z",
- "include_in_archive": true,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Only present fields are touched; subject/preheader clear on an explicit
null. A content change recompiles (fresh compile envelope). Published
issues stay editable — a typo fix recompiles for future catch-up deliveries;
already-delivered copies are naturally immutable. Content cannot be cleared
through the API (content: null fails validation) — replace it with new
content, or manage that in-app.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| subject | string or null <= 400 characters |
| preheader | string or null <= 400 characters |
| include_in_archive | boolean |
object (ContentInput) The shared authoring contract for email-campaign bodies and newsletter issues: an
optional Total content size is capped at 512,000 characters (measured on the source and
again on the built document) — an oversized body returns 400 Variable tokens work in all three sources: any Snippets (reusable content blocks from the workspace library, managed in
Settings → Snippets) can be spliced in by UUID or by case-insensitive exact
name — via the |
{- "subject": "string",
- "preheader": "string",
- "include_in_archive": true,
- "content": {
- "direction": "ltr",
- "markdown": "# Hello [[first_name : there]]!\n\nWelcome to our July update.\n\n::button[Read the full story](https://example.com/blog/july)\n"
}
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "newsletter_id": "854c5e2d-fe5e-4354-976d-dc3015528911",
- "issue_number": 0,
- "subject": "string",
- "preheader": "string",
- "status": "draft",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "published_at": "2019-08-24T14:15:22Z",
- "include_in_archive": true,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "duplicate": true,
- "compile": {
- "ok": true,
- "errors": [ ],
- "warnings": [
- "Headings deeper than level 3 were clamped to level 3"
]
}
}Published issues can never be deleted — deleting one would free its issue number
and corrupt subscriber catch-up cursors. Hide one from the public archive with
include_in_archive: false instead.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "success": true
}No request body. Assigns the next issue_number (per-newsletter, monotonic) and
wakes caught-up subscribers — delivery behaves exactly like an in-app publish:
caught-up subscribers receive the issue on the next delivery sweep, while new or
behind subscribers reach it through their catch-up drip. Requires a subject and
compiled content (409 issue_missing_content otherwise). Idempotent — an
already-published issue is returned as-is with a 200.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "newsletter_id": "854c5e2d-fe5e-4354-976d-dc3015528911",
- "issue_number": 0,
- "subject": "string",
- "preheader": "string",
- "status": "draft",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "published_at": "2019-08-24T14:15:22Z",
- "include_in_archive": true,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}scheduled_at is an ISO 8601 UTC instant in the future; the every-minute sweep
publishes the issue when due (same effects as …/publish). Requires a subject
and compiled content up front, so the sweep can't hit a content check failure
later. Calling it again while still scheduled moves the publish time.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| scheduled_at required | string <date-time> ISO 8601 UTC instant; must be in the future (otherwise 400
|
{- "scheduled_at": "2026-08-01T09:00:00Z"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "newsletter_id": "854c5e2d-fe5e-4354-976d-dc3015528911",
- "issue_number": 0,
- "subject": "string",
- "preheader": "string",
- "status": "draft",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "published_at": "2019-08-24T14:15:22Z",
- "include_in_archive": true,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}No request body. Only currently scheduled issues can be unscheduled.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "newsletter_id": "854c5e2d-fe5e-4354-976d-dc3015528911",
- "issue_number": 0,
- "subject": "string",
- "preheader": "string",
- "status": "draft",
- "design_json": { },
- "compiled_html": "string",
- "compiled_styles": "string",
- "plain_text": "string",
- "scheduled_at": "2019-08-24T14:15:22Z",
- "published_at": "2019-08-24T14:15:22Z",
- "include_in_archive": true,
- "external_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Register receivers for outbound events — email delivery/engagement events on API-originated sends, plus the opt-in order.*, payment_request.*, contact.*, message.received, deal.*, booking.*, event.attendance.changed, and form.submitted families.
Default-subscription delivery event.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Email event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked"
Value: "email.delivered" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "email.delivered",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "send_id": "30f48f1c-f412-4e56-a761-08101c8b9966",
- "idempotency_key": "string",
- "to": "string",
- "reason": "string",
- "metadata": { }
}
}Default-subscription delivery event. data.bounce_type is included when known:
hard, soft, or block.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Email event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked"
Value: "email.bounced" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "b2c3d4e5-f607-1829-3a4b-5c6d7e8f9012",
- "type": "email.bounced",
- "created_at": "2026-07-14T10:05:32.000Z",
- "data": {
- "send_id": "f0e1d2c3-b4a5-9687-7869-5a4b3c2d1e0f",
- "idempotency_key": "order-88123-receipt",
- "to": "customer@example.com",
- "reason": "550 5.1.1 user unknown",
- "bounce_type": "hard",
- "metadata": {
- "order_id": "88123"
}
}
}Default-subscription delivery event.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Email event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked"
Value: "email.complained" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "email.complained",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "send_id": "30f48f1c-f412-4e56-a761-08101c8b9966",
- "idempotency_key": "string",
- "to": "string",
- "reason": "string",
- "metadata": { }
}
}Deprecated — never delivered; nothing produces this event. It is no longer
part of the default subscription set. Listing it explicitly at registration still
succeeds (no error, no warning; it is echoed in events) so existing integrations
keep working, but no delivery will ever arrive. A failing POST /v1/emails send
fails synchronously on the request itself (e.g. 502 provider_error,
409 send_in_progress) — handle send failures from the send response.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Email event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked"
Value: "email.failed" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "email.failed",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "send_id": "30f48f1c-f412-4e56-a761-08101c8b9966",
- "idempotency_key": "string",
- "to": "string",
- "reason": "string",
- "metadata": { }
}
}Opt-in engagement event: at most one per send, for sends with tracking.opens.
data.machine_open is always present — true means the open was attributed to an
automated mail scanner / prefetcher (e.g. Apple Mail privacy) rather than a human.
Machine opens are forwarded flagged, never dropped.
A click does not emit an implied email.opened event — treat a send as opened
when you've received either email.opened or email.clicked.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Email event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked"
Value: "email.opened" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "email.opened",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "send_id": "30f48f1c-f412-4e56-a761-08101c8b9966",
- "idempotency_key": "string",
- "to": "string",
- "reason": "string",
- "metadata": { },
- "machine_open": true
}
}Opt-in engagement event: at most one per send, for sends with tracking.clicks.
data.url is always present: the original destination URL (may be an empty string
in rare cases). Clicks carry no machine flag.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Email event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked"
Value: "email.clicked" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "email.clicked",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "send_id": "30f48f1c-f412-4e56-a761-08101c8b9966",
- "idempotency_key": "string",
- "to": "string",
- "reason": "string",
- "metadata": { },
- "url": "string"
}
}Opt-in order lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires for every order write source (API, in-app,
automations), never for historical import ingestion. A paid create also emits
order.paid.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Order event type) Enum: "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" The five order lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "order.created" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "order.created",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "external_id": "string",
- "number": "string",
- "platform": "string",
- "store_connection_id": "b7a8f80c-9686-4d77-b9e3-5d9db722eef5",
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "string",
- "total": 0,
- "subtotal": 0,
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "refunded_total": 0,
- "coupon_codes": [
- "string"
], - "item_count": 0,
- "first_item_name": "string",
- "placed_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "refunded_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
}Opt-in order lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires when the order enters a paid state: a create with
financial_status: "paid", or POST /v1/orders/{id}/mark-paid (and equivalent
in-app/automation writes).
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Order event type) Enum: "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" The five order lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "order.paid" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "order.paid",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "external_id": "string",
- "number": "string",
- "platform": "string",
- "store_connection_id": "b7a8f80c-9686-4d77-b9e3-5d9db722eef5",
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "string",
- "total": 0,
- "subtotal": 0,
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "refunded_total": 0,
- "coupon_codes": [
- "string"
], - "item_count": 0,
- "first_item_name": "string",
- "placed_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "refunded_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
}Opt-in order lifecycle event — delivered only to endpoints that list it
explicitly in events. In addition to the shared order data fields,
data.refund carries the refund that fired this event.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Order event type) Enum: "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" The five order lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "order.refunded" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "a1b2c3d4-e5f6-0718-2930-4a5b6c7d8e9f",
- "type": "order.refunded",
- "created_at": "2026-07-15T09:30:00.000Z",
- "data": {
- "order_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
- "external_id": null,
- "number": "1042",
- "platform": "api",
- "store_connection_id": null,
- "financial_status": "partially_refunded",
- "fulfillment_status": "unfulfilled",
- "currency": "ILS",
- "total": 360,
- "subtotal": 340,
- "discount_total": 0,
- "shipping_total": 20,
- "tax_total": 0,
- "refunded_total": 50,
- "coupon_codes": [
- "SUMMER10"
], - "item_count": 3,
- "first_item_name": "Widget",
- "placed_at": "2026-07-14T10:00:00.000Z",
- "paid_at": "2026-07-14T10:00:00.000Z",
- "cancelled_at": null,
- "refunded_at": "2026-07-15T09:30:00.000Z",
- "created_at": "2026-07-14T10:00:00.000Z",
- "refund": {
- "amount": 50,
- "external_refund_id": "r-1",
- "reason": "damaged",
- "refunded_at": "2026-07-15T09:30:00.000Z"
}
}
}Opt-in order lifecycle event — delivered only to endpoints that list it
explicitly in events. Cancellation is the cancelled_at stamp — the
financial_status in data keeps its last state (cancellation is not a
financial status).
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Order event type) Enum: "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" The five order lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "order.cancelled" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "order.cancelled",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "external_id": "string",
- "number": "string",
- "platform": "string",
- "store_connection_id": "b7a8f80c-9686-4d77-b9e3-5d9db722eef5",
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "string",
- "total": 0,
- "subtotal": 0,
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "refunded_total": 0,
- "coupon_codes": [
- "string"
], - "item_count": 0,
- "first_item_name": "string",
- "placed_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "refunded_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
}Opt-in order lifecycle event — delivered only to endpoints that list it
explicitly in events. Fulfillment is recorded in-app — there is no /v1
fulfillment route, so this event only ever originates from non-API writes.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Order event type) Enum: "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" The five order lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "order.fulfilled" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "order.fulfilled",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "order_id": "93101167-9065-4b9c-b98b-5d789a3ed9fe",
- "external_id": "string",
- "number": "string",
- "platform": "string",
- "store_connection_id": "b7a8f80c-9686-4d77-b9e3-5d9db722eef5",
- "financial_status": "pending",
- "fulfillment_status": "unfulfilled",
- "currency": "string",
- "total": 0,
- "subtotal": 0,
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "refunded_total": 0,
- "coupon_codes": [
- "string"
], - "item_count": 0,
- "first_item_name": "string",
- "placed_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "refunded_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
}Opt-in pay-link lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires for every hosted pay-link mint (API and in-app).
Hosted pay-links only: direct saved-card charges and internal
dunning-recovery links never emit payment_request.* events.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Payment-request event type) Enum: "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" The four pay-link lifecycle events. All are opt-in: delivered only to
endpoints that list them explicitly in Value: "payment_request.created" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "payment_request.created",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "status": "pending",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "provider": "string",
- "amount": 0,
- "currency": "string",
- "title": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "test_mode": true,
- "pay_url": "string",
- "contact_payment_id": "b0059909-e2a0-40f6-be35-1d854b970361",
- "expires_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
}Opt-in pay-link lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires when the payment is verified with the provider,
including a late completion of an already-cancelled link (a payer who was on
the hosted page when it was cancelled). data.contact_payment_id links the
settled payment ledger row. Test-mode completions fire too — check
data.test_mode before recording revenue.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Payment-request event type) Enum: "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" The four pay-link lifecycle events. All are opt-in: delivered only to
endpoints that list them explicitly in Value: "payment_request.paid" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "c3d4e5f6-0718-2930-4a5b-6c7d8e9f0a1b",
- "type": "payment_request.paid",
- "created_at": "2026-07-15T11:20:00.000Z",
- "data": {
- "payment_request_id": "0b1c2d3e-4f50-6172-8394-a5b6c7d8e9f0",
- "status": "paid",
- "contact_id": "9c2f1a4e-3b7d-4e2a-9f0c-1d2e3f4a5b6c",
- "deal_id": null,
- "provider": "sumit",
- "amount": 250,
- "currency": "ILS",
- "title": "Onboarding session",
- "vat_mode": "inclusive",
- "vat_rate": 18,
- "test_mode": false,
- "contact_payment_id": "7b6a5c4d-3e2f-1a0b-9c8d-7e6f5a4b3c2d",
- "expires_at": "2026-07-18T09:00:00.000Z",
- "paid_at": "2026-07-15T11:20:00.000Z",
- "cancelled_at": null,
- "created_at": "2026-07-15T09:00:00.000Z"
}
}Opt-in pay-link lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires when a pending link passes expires_at unpaid —
from the expiry sweep, or lazily when the expired link is opened.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Payment-request event type) Enum: "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" The four pay-link lifecycle events. All are opt-in: delivered only to
endpoints that list them explicitly in Value: "payment_request.expired" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "payment_request.expired",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "status": "pending",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "provider": "string",
- "amount": 0,
- "currency": "string",
- "title": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "test_mode": true,
- "pay_url": "string",
- "contact_payment_id": "b0059909-e2a0-40f6-be35-1d854b970361",
- "expires_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
}Opt-in pay-link lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires on POST /v1/payment-requests/{id}/cancel and on
in-app cancels. A payer already on the hosted page can still complete after the
cancel — a later payment_request.paid for the same request supersedes this
event.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Payment-request event type) Enum: "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" The four pay-link lifecycle events. All are opt-in: delivered only to
endpoints that list them explicitly in Value: "payment_request.cancelled" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "payment_request.cancelled",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "status": "pending",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "provider": "string",
- "amount": 0,
- "currency": "string",
- "title": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "test_mode": true,
- "pay_url": "string",
- "contact_payment_id": "b0059909-e2a0-40f6-be35-1d854b970361",
- "expires_at": "2019-08-24T14:15:22Z",
- "paid_at": "2019-08-24T14:15:22Z",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
}Opt-in contact lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires post-commit at the intentional write seams (API
upserts that create, in-app creates, form/lead capture, integration syncs) —
CSV imports, bulk edits, and merge-internal writes are deliberately quiet.
data.contact is a fixed compact projection — fetch the full row with
GET /v1/contacts/{id}. The creation-time consent seed does not emit a
separate contact.consent_changed — this event covers the creation.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | any Value: "contact.created" |
| created_at required | string <date-time> |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "contact.created",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "phone": "string",
- "email": "string",
- "name": "string",
- "first_name": "string",
- "last_name": "string",
- "lifecycle_stage": "string",
- "source": "string",
- "block_state": "string",
- "lead_score": 0
}, - "source": "string",
- "duplicate": true
}
}Opt-in contact lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires post-commit for intentional single-contact writes;
bulk edits, CSV-import updates, merge-internal writes, and engine/noise writers
(scoring, touch fields, tracking rollups) are deliberately quiet.
data.changed_fields names what changed — the same field list the in-app
Activity timeline records, including tags/groups junction keys and
custom_fields.<key> entries. data.contact stays the compact core — never
junction arrays; fetch the full row with GET /v1/contacts/{id}.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | any Value: "contact.updated" |
| created_at required | string <date-time> |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "contact.updated",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "changed_fields": [
- "string"
], - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "phone": "string",
- "email": "string",
- "name": "string",
- "first_name": "string",
- "last_name": "string",
- "lifecycle_stage": "string",
- "source": "string",
- "block_state": "string",
- "lead_score": 0
}, - "source": "string"
}
}Opt-in contact lifecycle event — delivered only to endpoints that list it
explicitly in events. Carries last-known identifiers only (the row is gone) —
key your mirror off data.contact_id. Bulk deletes emit one event per contact;
contact merges are fully silent (a merge is not a deletion — the absorbed
contact emits nothing).
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | any Value: "contact.deleted" |
| created_at required | string <date-time> |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "contact.deleted",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "phone": "string",
- "email": "string",
- "name": "string"
}
}Opt-in consent event — delivered only to endpoints that list it explicitly in
events. Fires on real consent-state transitions (opt-in, unsubscribe,
resubscribe) plus deliverability-driven suppress escalations, for every consent
surface (API, forms, unsubscribe pages, in-app edits). Quiet by design: the
contact-create consent seed (see contact.created), same-state re-assertions,
double-opt-in ceremony markers, and preference-center metadata updates do not
emit.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | any Value: "contact.consent_changed" |
| created_at required | string <date-time> |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "contact.consent_changed",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "channel": "whatsapp",
- "action": "string",
- "consent_state": "subscribed",
- "previous_state": "subscribed",
- "basis": "express_opt_in",
- "source": "string",
- "consent_event_id": "7b932686-ba82-4f3d-baf6-5cd8ef7c7f9f"
}
}Opt-in inbound-message event — delivered only to endpoints that list it
explicitly in events. Real WhatsApp inbound only, exactly once per WhatsApp
message (post-dedup): reactions, messages from blocked contacts, and WhatsApp
coexistence echoes/history imports are all silent. Media rides as metadata
only (mime_type, filename, bytes) — never presigned URLs or storage
keys.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | any Value: "message.received" |
| created_at required | string <date-time> |
required | object |
{- "id": "d4e5f607-1829-3a4b-5c6d-7e8f90123456",
- "type": "message.received",
- "created_at": "2026-07-16T08:12:05.000Z",
- "data": {
- "message_id": "1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809",
- "conversation_id": "2b3c4d5e-6f70-8192-a3b4-c5d6e7f8091a",
- "contact_id": "9c2f1a4e-3b7d-4e2a-9f0c-1d2e3f4a5b6c",
- "channel": "whatsapp_api",
- "type": "image",
- "text": "Here is the signed form",
- "media": {
- "mime_type": "image/jpeg",
- "filename": "signed-form.jpg",
- "bytes": 482113
}, - "wa_message_id": "wamid.HBgMOTcyNTA1NTU1NTU1FQIAEhg=",
- "timestamp": "2026-07-16T08:12:03.000Z"
}
}Opt-in deal lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires for every write source — manual, API,
automations, and Salesforce sync — data.source says which.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Deal event type) Enum: "deal.created" "deal.stage_changed" "deal.won" "deal.lost" The four deal lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "deal.created" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "deal.created",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "pipeline_name": "string",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "stage_name": "string",
- "from_stage_id": "7421a0fa-83ec-448a-9d53-53e45c72802a",
- "from_stage_name": "string",
- "status": "open",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "external_reference": "string",
- "source": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "lost_reason": "string"
}
}Opt-in deal lifecycle event — delivered only to endpoints that list it
explicitly in events. data.from_stage_id/data.from_stage_name carry the
origin stage — they are null on every other deal event.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Deal event type) Enum: "deal.created" "deal.stage_changed" "deal.won" "deal.lost" The four deal lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "deal.stage_changed" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "e5f60718-293a-4b5c-6d7e-8f9012345678",
- "type": "deal.stage_changed",
- "created_at": "2026-07-16T09:00:00.000Z",
- "data": {
- "deal_id": "3c4d5e6f-7081-92a3-b4c5-d6e7f8091a2b",
- "contact_id": "9c2f1a4e-3b7d-4e2a-9f0c-1d2e3f4a5b6c",
- "pipeline_id": "4d5e6f70-8192-a3b4-c5d6-e7f8091a2b3c",
- "pipeline_name": "Sales",
- "stage_id": "5e6f7081-92a3-b4c5-d6e7-f8091a2b3c4d",
- "stage_name": "Negotiation",
- "from_stage_id": "6f708192-a3b4-c5d6-e7f8-091a2b3c4d5e",
- "from_stage_name": "Qualified",
- "status": "open",
- "title": "Onboarding package",
- "amount": 3600,
- "currency": "ILS",
- "owner_user_id": "708192a3-b4c5-d6e7-f809-1a2b3c4d5e6f",
- "external_reference": "crm-9912",
- "source": "api",
- "expected_close_at": "2026-08-01T00:00:00.000Z",
- "closed_at": null,
- "lost_reason": null
}
}Opt-in deal lifecycle event — delivered only to endpoints that list it
explicitly in events. data.status is won and data.closed_at is stamped;
the deal keeps its last stage.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Deal event type) Enum: "deal.created" "deal.stage_changed" "deal.won" "deal.lost" The four deal lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "deal.won" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "deal.won",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "pipeline_name": "string",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "stage_name": "string",
- "from_stage_id": "7421a0fa-83ec-448a-9d53-53e45c72802a",
- "from_stage_name": "string",
- "status": "open",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "external_reference": "string",
- "source": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "lost_reason": "string"
}
}Opt-in deal lifecycle event — delivered only to endpoints that list it
explicitly in events. data.status is lost; data.lost_reason carries the
stored reason (or null).
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Deal event type) Enum: "deal.created" "deal.stage_changed" "deal.won" "deal.lost" The four deal lifecycle events. All are opt-in: delivered only to endpoints
that list them explicitly in Value: "deal.lost" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "deal.lost",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "pipeline_id": "ec036e81-7903-4e4d-bbfa-ac8516341cf0",
- "pipeline_name": "string",
- "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
- "stage_name": "string",
- "from_stage_id": "7421a0fa-83ec-448a-9d53-53e45c72802a",
- "from_stage_name": "string",
- "status": "open",
- "title": "string",
- "amount": 0,
- "currency": "string",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "external_reference": "string",
- "source": "string",
- "expected_close_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "lost_reason": "string"
}
}Opt-in booking lifecycle event — delivered only to endpoints that list it
explicitly in events. Fires for every booking source — data.source says
which. booking.completed / booking.no_show deliberately do not exist as
webhooks: those statuses are sweep-derived, not user actions.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Booking event type) Enum: "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" The four booking lifecycle events. All are opt-in: delivered only to
endpoints that list them explicitly in Value: "booking.created" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "booking.created",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "booking_id": "b0ae0641-0cd4-4f7f-8550-dcd550941f4a",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "meeting_type_name": "string",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "host_name": "string",
- "previous_host_name": "string",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "status": "string",
- "location_type": "string",
- "cancelled_by": "string",
- "cancel_reason": "string",
- "source": "string"
}
}Opt-in booking lifecycle event — delivered only to endpoints that list it
explicitly in events. data.start_at/data.end_at are the NEW times.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Booking event type) Enum: "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" The four booking lifecycle events. All are opt-in: delivered only to
endpoints that list them explicitly in Value: "booking.rescheduled" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "booking.rescheduled",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "booking_id": "b0ae0641-0cd4-4f7f-8550-dcd550941f4a",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "meeting_type_name": "string",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "host_name": "string",
- "previous_host_name": "string",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "status": "string",
- "location_type": "string",
- "cancelled_by": "string",
- "cancel_reason": "string",
- "source": "string"
}
}Opt-in booking lifecycle event — delivered only to endpoints that list it
explicitly in events. data.cancelled_by and data.cancel_reason carry who
cancelled and why (when recorded).
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Booking event type) Enum: "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" The four booking lifecycle events. All are opt-in: delivered only to
endpoints that list them explicitly in Value: "booking.cancelled" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "booking.cancelled",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "booking_id": "b0ae0641-0cd4-4f7f-8550-dcd550941f4a",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "meeting_type_name": "string",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "host_name": "string",
- "previous_host_name": "string",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "status": "string",
- "location_type": "string",
- "cancelled_by": "string",
- "cancel_reason": "string",
- "source": "string"
}
}Opt-in booking lifecycle event — delivered only to endpoints that list it
explicitly in events. data.previous_host_name carries the outgoing host —
it is null on every other booking event.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | string (Booking event type) Enum: "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" The four booking lifecycle events. All are opt-in: delivered only to
endpoints that list them explicitly in Value: "booking.reassigned" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "booking.reassigned",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "booking_id": "b0ae0641-0cd4-4f7f-8550-dcd550941f4a",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "meeting_type_name": "string",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "host_name": "string",
- "previous_host_name": "string",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "status": "string",
- "location_type": "string",
- "cancelled_by": "string",
- "cancel_reason": "string",
- "source": "string"
}
}Opt-in event-attendance event — delivered only to endpoints that list it
explicitly in events. ONE event type for the whole family — filter on
data.status; data.previous_status carries the transition (null for fresh
registrations and for set-based bulk status updates, which have no per-row
prior state). Fires per attendance row at the writer seams (single edits, bulk
operations, automation actions).
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | any Value: "event.attendance.changed" |
| created_at required | string <date-time> |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "event.attendance.changed",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "status": "string",
- "previous_status": "string",
- "registered_at": "2019-08-24T14:15:22Z",
- "attended_at": "2019-08-24T14:15:22Z",
- "unregistered_at": "2019-08-24T14:15:22Z",
- "event": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "start_at": "2019-08-24T14:15:22Z"
}
}
}Opt-in form event — delivered only to endpoints that list it explicitly in
events. ONE event type for every surface — data.origin distinguishes a
standalone form embed (form), a landing-page form (landing_page), and an
on-site popup (popup). Fires post-persist even when no contact was resolved
(data.contact_id is then null); data.fields carries the submitted answers
keyed by form field ids.
| X-Otok-Event required | string (Webhook event type) Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" The event type (e.g. |
| X-Otok-Event-Id required | string <uuid> The event id (same as the payload |
| X-Otok-Signature required | string^t=\d+,v1=[0-9a-f]{64}$
|
| id required | string <uuid> Event id — stable across retries and shared across your endpoints. Use it as your dedup key. |
| type required | any Value: "form.submitted" |
| created_at required | string <date-time> |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "form.submitted",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "form_name": "string",
- "submission_id": "string",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "origin": "form",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "popup_id": "0a11bf4f-e2e3-4b0b-be20-c4623bd8ee59",
- "fields": { }
}
}No parameters (max 3 rows, unpaginated). Secrets are never included.
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "url": "string",
- "events": [
- "email.delivered"
], - "is_active": true,
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Maximum 3 endpoints per workspace (409 endpoint_limit_reached); the cap is
enforced safely under concurrency.
The
secretis returned only here, once. It cannot be retrieved again. There is no rotation endpoint: to rotate, register a new endpoint with the same URL, switch verification to the new secret, then delete the old endpoint.
| url required | string [ 1 .. 2048 ] characters
|
| events | Array of strings (Webhook event type) non-empty Items Enum: "email.delivered" "email.bounced" "email.complained" "email.failed" "email.opened" "email.clicked" "order.created" "order.paid" "order.refunded" "order.cancelled" "order.fulfilled" "payment_request.created" "payment_request.paid" "payment_request.expired" "payment_request.cancelled" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" Event types to receive. Must be non-empty when present. Omitted →
the three delivery events ( |
{- "url": "string",
- "events": [
- "email.delivered"
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "url": "string",
- "events": [
- "email.delivered"
], - "is_active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "secret": "string"
}Deliveries stop immediately; anything still queued for the deleted endpoint is dropped.
| id required | string Endpoint id. Unlike other routes, a malformed (non-UUID) id returns 404, not 400 — deliberately indistinguishable from an unknown id. |
{- "statusCode": 0,
- "message": "string",
- "error": "string",
- "error_code": "string"
}Standard list conventions apply.
| filter | string Default: "{}" Example: filter={"lifecycle_stage":"lead"} String-encoded JSON object. Invalid JSON → 400 Grammar: exact match ( Filter values are type-checked against the target field before the query
runs — a mistyped value returns 400 |
| sort | string Default: "-created_at" Field name; |
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
| search | string Free-text search (fields vary per resource). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "slug": "string",
- "description": "string",
- "duration_minutes": 0,
- "location_type": "string",
- "is_active": true,
- "scheduling_kind": "string",
- "host": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "hosts": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "name": "string"
}
], - "created_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "slug": "string",
- "description": "string",
- "duration_minutes": 0,
- "location_type": "string",
- "is_active": true,
- "scheduling_kind": "string",
- "host": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "hosts": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "name": "string"
}
], - "created_at": "2019-08-24T14:15:22Z"
}Returns the open start instants for the meeting type — the same availability the
public booking page offers. Slot instants are UTC ISO timestamps; timezone is the
host schedule's IANA zone.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| from required | string <date-time> Range start (inclusive), ISO 8601. |
| to required | string <date-time> Range end (exclusive); must be after |
{- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "timezone": "string",
- "duration_minutes": 0,
- "slots": [
- {
- "start": "2019-08-24T14:15:22Z",
- "end": "2019-08-24T14:15:22Z"
}
]
}Everything needed to put the booking calendar on your own website: the hosted
booking page URL, the workspace's publishable embed key, and a ready-to-paste
snippet. embed_key (bk_…) is safe in page HTML by design and is NOT the
secret API key — rotation, the origin allowlist, and the embed on/off switch
live in the oToK app under Settings → Booking. Bookings made through the embed
carry source: "embed".
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "workspace_ref": "string",
- "slug": "string",
- "embed_key": "string",
- "snippet_html": "string"
}All filters optional and combined with AND. Malformed values → 400.
| status | string Enum: "confirmed" "cancelled" "completed" "no_show" |
| meeting_type_id | string <uuid> |
| from | string <date-time>
|
| to | string <date-time>
|
| sort | string Default: "-start_at" Default |
| limit | integer [ 0 .. 500 ] Default: 50 Hard cap 500. Non-integer or negative → 400. |
| offset | integer >= 0 Default: 0 Non-integer or negative → 400. |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "hosts": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "name": "string",
- "role": "string"
}
], - "status": "confirmed",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "invitee_name": "string",
- "invitee_email": "string",
- "invitee_phone": "string",
- "join_url": "string",
- "notes": "string",
- "source": "string",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancelled_by": "string",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Books a slot. The slot must be open at booking time; slot-taking is race-safe. The usual booking side effects run (confirmation email, calendar/Zoom setup where configured).
Idempotency is server-derived — you do not send a key. A double submit of the
same slot + meeting type + contact (+ pinned host, when used) returns the existing
confirmed booking as a success instead of failing or double-booking — still 201,
with duplicate: true (a fresh create answers duplicate: false). For round-robin
types, a pinned create can never silently replay onto a different host than the
one pinned.
| meeting_type_id required | string <uuid> Must be active. |
| start_at required | string <date-time> Must be an open slot (use the slots endpoint). |
| timezone required | string Valid IANA zone (e.g. |
| contact_id | string <uuid> Existing contact. |
object Upserted into contacts by phone/email; the email receives the confirmation and manage link. | |
| notes | string <= 2000 characters |
| host_user_id | string <uuid> Round-robin meeting types only: pin the booking to this pool host (must be an active pool member with the slot free). Pinned bookings still count toward round-robin fairness. |
{- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "start_at": "2019-08-24T14:15:22Z",
- "timezone": "string",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "invitee": {
- "name": "string",
- "email": "user@example.com",
- "phone": "string"
}, - "notes": "string",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "hosts": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "name": "string",
- "role": "string"
}
], - "status": "confirmed",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "invitee_name": "string",
- "invitee_email": "string",
- "invitee_phone": "string",
- "join_url": "string",
- "notes": "string",
- "source": "string",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancelled_by": "string",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "duplicate": true
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "hosts": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "name": "string",
- "role": "string"
}
], - "status": "confirmed",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "invitee_name": "string",
- "invitee_email": "string",
- "invitee_phone": "string",
- "join_url": "string",
- "notes": "string",
- "source": "string",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancelled_by": "string",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}API cancellations are attributed to the host side.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| reason | string <= 1000 characters |
{- "reason": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "hosts": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "name": "string",
- "role": "string"
}
], - "status": "confirmed",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "invitee_name": "string",
- "invitee_email": "string",
- "invitee_phone": "string",
- "join_url": "string",
- "notes": "string",
- "source": "string",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancelled_by": "string",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| start_at required | string <date-time> ISO 8601 — a new open slot. |
| timezone | string <= 64 characters Updates the invitee timezone; omitted keeps the existing one. |
{- "start_at": "2019-08-24T14:15:22Z",
- "timezone": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "hosts": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "name": "string",
- "role": "string"
}
], - "status": "confirmed",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "invitee_name": "string",
- "invitee_email": "string",
- "invitee_phone": "string",
- "join_url": "string",
- "notes": "string",
- "source": "string",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancelled_by": "string",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Change which host owns a confirmed booking. Reassignment is pre-start only and not available for collective meeting types (where every pool host attends).
409 handling: HOST_UNAVAILABLE is overridable by repeating the request with
"force": true; SLOT_TAKEN (a genuine conflicting booking) is never
overridable, even with force; BOOKING_MODIFIED means the booking changed
concurrently — re-read and retry.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| user_id | string <uuid> Any active workspace member. Omitted → automatic round-robin re-pick excluding the current host (round-robin types only). |
| reason | string <= 1000 characters Recorded on the assignment history. |
| force | boolean Override host-availability failures. Must be literally Value: true |
{- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "reason": "string",
- "force": true
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607",
- "hosts": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "name": "string",
- "role": "string"
}
], - "status": "confirmed",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "host_timezone": "string",
- "invitee_timezone": "string",
- "invitee_name": "string",
- "invitee_email": "string",
- "invitee_phone": "string",
- "join_url": "string",
- "notes": "string",
- "source": "string",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancelled_by": "string",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}