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, the
sales ledger, transactional email, broadcast email campaigns, newsletters, email suppressions,
read-only audience and sender-profile discovery, product cycles, shared saved reports,
outbound webhooks, bookings, and customer tickets.
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), sales
(sales), campaigns
(campaigns), email campaigns (email_marketing), newsletters (newsletters),
bookings/meeting-types (booking), customer tickets (customer_tickets). 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*) requires the
email_marketing feature.
Contacts (including consent), notes, tags, contact groups, templates, products,
audiences, product cycles, shared report listing, emails, sender profiles, and webhook endpoints require only plan-wide API access.
Report runs additionally check the dataset feature.
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
manual_status: "archived" 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).
DELETE /v1/sales/{id} (an unfunded, unlinked sale) and
DELETE /v1/sales/{id}/allocations/{allocationId} additionally need the
Allow permanent deletion through the API key capability (allow_hard_delete),
granted per key by the workspace owner — 403 HARD_DELETE_OWNER_ONLY otherwise.
| 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/sales,
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 writes nothing and returns
the original sale with duplicate: true. Sale refunds are idempotent per
idempotency_key — only when you supply one.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.external_reference (a replay changes nothing and
answers the original ticket); ticket replies are idempotent per idempotency_key.
Both answer 201 fresh / 200 replay.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, POST /v1/tickets, and POST /v1/tickets/{id}/replies, which
answer 201 fresh / 200 replay.
/v1/sales*): record sales with line items, link
payments to them, cancel, reinstate, refund, reassign the owner and link a deal. Gated
by the sales plan feature; refunds need an API key with refund access./v1/tickets*): open, read, answer and triage customer
tickets. Gated by the customer_tickets plan feature.GET /v1/contacts/{id}/payment-methods and POST /v1/contacts/{id}/charges
(requires an API key with the charges capability; idempotency_key required).POST /v1/payment-requests/{id}/send-link, /document and /refund;
product_id, sale_id and an optional idempotency_key on create.POST /v1/payments/{id}/refund now takes a required reason and mode and returns the
refund result (payment, entry, outcome, duplicate).sale_id.collect_payment_on_registration, and registrations carry a payment block.
Bookings carry a deposit block.dynamic_pricing, recurring_sale_policy and
rearm_date_triggers.payment.refunded, sale.recorded, sale.paid,
sale.refunded and ticket.*.GET /v1/sender-profiles no longer needs a plan feature.event_type_id and event_type_name on POST /v1/events link the event to a
saved event — a reusable event definition managed in the oToK app (Events → Saved
events). Resolve-only: the API never creates a saved event. An unknown id or name
answers 400 event_type_not_found; an id and a name that disagree answer 400
event_type_mismatch. The event object carries event_type_id and an event_type
block ({id, name}, or null), and GET /v1/events filters on event_type_id./v1/events*, PATCH /v1/attendances/{id}) — events
and their registrations. A registration made here fires exactly what an in-app one
fires: the event automations, the event.attendance.changed webhook, lead scoring,
and the Zoom registrant push that produces the attendee's personal join link. Gated
by the events plan feature.GET /v1/contacts/{id}/acquisitions — the per-submission record behind the
in-app Journey, newest first. Gated by the attribution plan feature.first_touch and last_touch on the contact object — the contact's first
and most recent acquisition, in the same shape as an acquisitions row, or null.
Both keys are absent (not null) without the attribution feature.event.attendance.changed webhook's
status and previous_status were documented as
registered | attending | attended | no_show | cancelled. The values the API has
always emitted are registered | attended | no_show | waitlist | unregistered:
attending never existed, cancelled is accepted only as a write-time alias for
unregistered, and waitlist / unregistered were missing. Nothing changed on the
wire — but a consumer branching on the documented spelling was never matching those
two states./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.{- "name": "string",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "open",
- "price": 0,
- "capacity": 1,
- "is_archived": false,
- "dynamic_pricing": true,
- "rearmed_fires": 0,
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "sales_count": 0,
- "units_taken": 0
}Partial update; archive with is_archived: true. No DELETE endpoint. Invalid date ranges or status overrides return 400.
| id required | string <uuid> |
| name | string [ 1 .. 200 ] characters Case-insensitive per-product upsert key. |
| starts_on | string or null <date> Workspace calendar start date. Must be a real YYYY-MM-DD date. |
| ends_on | string or null <date> Workspace calendar end date, not before starts_on. |
| duration_unit | string Default: "days" Enum: "days" "weeks" "months" "years" |
| manual_status | string or null Enum: "open" "ongoing" "ended" "undated" "cancelled" null With either date set, only cancelled or null is accepted; null resumes automatic status. |
| price | number or null decimal places <= 2 [ 0 .. 1000000000 ] Workspace-currency cycle price override; null uses product price. On a product with |
| capacity | integer or null [ 1 .. 1000000 ] Seat cap in occupied seats ( |
| is_archived | boolean Default: false |
| dynamic_pricing | boolean Default: true Allow sales in this cycle to override the cycle's price. |
| rearm_date_triggers | boolean Default: false When this call CHANGES |
{- "name": "string",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "open",
- "price": 0,
- "capacity": 1,
- "is_archived": false,
- "dynamic_pricing": true,
- "rearm_date_triggers": false
}{- "name": "string",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "open",
- "price": 0,
- "capacity": 1,
- "is_archived": false,
- "dynamic_pricing": true,
- "rearmed_fires": 0,
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "sales_count": 0,
- "units_taken": 0
}Includes archived cycles unless is_archived is specified. Ordered by start date (undated last), then creation time. Includes sales_count and units_taken.
| productId required | string <uuid> |
| is_archived | boolean |
| limit | integer [ 1 .. 500 ] Default: 50 Page size; larger values are clamped. |
| offset | integer >= 0 Default: 0 |
{- "data": [
- {
- "name": "string",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "open",
- "price": 0,
- "capacity": 1,
- "is_archived": false,
- "dynamic_pricing": true,
- "rearmed_fires": 0,
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "sales_count": 0,
- "units_taken": 0
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Case-insensitive name match within this product updates only supplied fields and returns duplicate: true. A new cycle returns duplicate: false. Both return 201.
| productId required | string <uuid> |
| name required | string [ 1 .. 200 ] characters Case-insensitive per-product upsert key. |
| starts_on | string or null <date> Workspace calendar start date. Must be a real YYYY-MM-DD date. |
| ends_on | string or null <date> Workspace calendar end date, not before starts_on. |
| duration_unit | string Default: "days" Enum: "days" "weeks" "months" "years" |
| manual_status | string or null Enum: "open" "ongoing" "ended" "undated" "cancelled" null With either date set, only cancelled or null is accepted; null resumes automatic status. |
| price | number or null decimal places <= 2 [ 0 .. 1000000000 ] Workspace-currency cycle price override; null uses product price. On a product with |
| capacity | integer or null [ 1 .. 1000000 ] Seat cap in occupied seats ( |
| is_archived | boolean Default: false |
| dynamic_pricing | boolean Default: true Allow sales in this cycle to override the cycle's price. |
| rearm_date_triggers | boolean Default: false When this call CHANGES |
{- "name": "string",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "open",
- "price": 0,
- "capacity": 1,
- "is_archived": false,
- "dynamic_pricing": true,
- "rearm_date_triggers": false
}{- "name": "string",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "open",
- "price": 0,
- "capacity": 1,
- "is_archived": false,
- "dynamic_pricing": true,
- "rearmed_fires": 0,
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "sales_count": 0,
- "units_taken": 0,
- "duplicate": true
}Runs a shared, unarchived report with workspace-wide data. Private, archived or unknown reports return 404. The dataset plan feature is checked on each run. Configure and share reports in the app; inline definitions are not accepted.
| id required | string <uuid> |
object | |
Array of objects <= 3 items |
{- "page": {
- "size": 1,
- "offset": 0
}, - "sort": [
- {
- "by": "string",
- "dir": "asc"
}
]
}{- "shape": "table",
- "columns": [
- {
- "key": "string",
- "kind": "string"
}
], - "rows": [
- { }
], - "page": {
- "size": 0,
- "offset": 0,
- "total": 0
}, - "meta": {
- "truncated": true,
- "dataset": "string",
- "shape": "table",
- "currency": "string",
- "currencyMode": "converted",
- "currencyWarnings": [
- {
- "currency": "string"
}
], - "refusedRelations": [
- "string"
], - "timezone": "string",
- "weekStart": "string",
- "generatedAt": "2019-08-24T14:15:22Z"
}
}Only shared, unarchived reports in the workspace. Returns metadata, never the report definition. Most recently updated first. Requires API access.
| limit | integer [ 1 .. 100 ] Default: 25 Page size; larger values are clamped. |
| offset | integer >= 0 Default: 0 |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "description": "string",
- "dataset": "string",
- "shape": "table",
- "chart_type": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Newest start date first. Requires the events plan feature.
| q | string Substring match on the event name. |
| external_id | string Exact, case-insensitive lookup by your own event id. |
| event_type_id | string <uuid> Only events created from this saved event. Non-UUID values answer 400. |
| limit | integer Page size (default 50, max 500). |
| offset | integer Rows to skip (default 0). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "external_id": "string",
- "external_provider": "string",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "ended_at": "2019-08-24T14:15:22Z",
- "status": "draft",
- "timezone": "string",
- "language": "string",
- "category": "string",
- "presenter": "string",
- "link": "string",
- "use_personal_links": true,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "event_type_id": "a4ec4c3c-a3de-4a8a-983f-1791e72cea83",
- "event_type": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "suppress_event_automations": true,
- "collect_payment_on_registration": true,
- "archived_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
], - "limit": 0,
- "offset": 0
}Creates an event, or updates the one already carrying this external_id (matched
case-insensitively) and answers duplicate: true — so a form can announce its
event on every submission without growing a second copy. There is no
PATCH /v1/events/{id}: the upsert is the update path.
event_type_id or event_type_name files the event under an existing saved
event (resolve-only — never created here): an unknown one answers 400
event_type_not_found, an id and a name that disagree answer 400
event_type_mismatch.
Requires the events plan feature.
| name required | string <= 300 characters |
| external_id | string <= 200 characters |
| start_at | string <date-time> |
| end_at | string <date-time> |
| status | string Enum: "draft" "scheduled" "canceled" "completed" Defaults to |
| timezone | string <= 100 characters |
| language | string <= 10 characters |
| category | string <= 200 characters |
| presenter | string <= 200 characters |
| link | string <= 2000 characters |
| link_password | string <= 200 characters |
| use_personal_links | boolean |
| product_id | string <uuid> |
| cycle_id | string <uuid> |
| event_type_id | string <uuid> The saved event (Events → Saved events in the oToK app) this event is created from, by id. Links only: the API never creates a saved event, and the saved event's defaults are not applied here — send the fields you want. An unknown id answers 400 |
| event_type_name | string <= 200 characters The saved event by its exact name instead of its id (matched case-insensitively, whitespace trimmed). An unknown name answers 400 |
| suppress_event_automations | boolean |
| collect_payment_on_registration | boolean Default: false Priced-event opt-in: every NEW registration — in the app's registration dialogs, by an automation, or through |
{- "name": "string",
- "external_id": "string",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "status": "draft",
- "timezone": "string",
- "language": "string",
- "category": "string",
- "presenter": "string",
- "link": "string",
- "link_password": "string",
- "use_personal_links": true,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "event_type_id": "a4ec4c3c-a3de-4a8a-983f-1791e72cea83",
- "event_type_name": "string",
- "suppress_event_automations": true,
- "collect_payment_on_registration": false
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "external_id": "string",
- "external_provider": "string",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "ended_at": "2019-08-24T14:15:22Z",
- "status": "draft",
- "timezone": "string",
- "language": "string",
- "category": "string",
- "presenter": "string",
- "link": "string",
- "use_personal_links": true,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "event_type_id": "a4ec4c3c-a3de-4a8a-983f-1791e72cea83",
- "event_type": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "suppress_event_automations": true,
- "collect_payment_on_registration": true,
- "archived_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "duplicate": true
}Requires the events plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "external_id": "string",
- "external_provider": "string",
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "ended_at": "2019-08-24T14:15:22Z",
- "status": "draft",
- "timezone": "string",
- "language": "string",
- "category": "string",
- "presenter": "string",
- "link": "string",
- "use_personal_links": true,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "event_type_id": "a4ec4c3c-a3de-4a8a-983f-1791e72cea83",
- "event_type": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "suppress_event_automations": true,
- "collect_payment_on_registration": true,
- "archived_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}Newest first. Each registration carries a payment block on a priced event
(sale_id, sale_status, settlement_status, pay_url — the newest live payment
link), or null. Requires the events plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| status | string Enum: "registered" "attended" "no_show" "waitlist" "unregistered" "cancelled" One of the stored statuses ( |
| limit | integer Page size (default 50, max 500). |
| offset | integer Rows to skip (default 0). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "status": "registered",
- "registered_at": "2019-08-24T14:15:22Z",
- "attended_at": "2019-08-24T14:15:22Z",
- "unregistered_at": "2019-08-24T14:15:22Z",
- "join_url": "string",
- "payment_sale_id": "0239f7f7-dff6-4071-8c7f-9beb1635e80f",
- "payment": {
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "sale_status": "active",
- "settlement_status": "untracked",
- "pay_url": "string"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "limit": 0,
- "offset": 0
}Registers a contact, or sets their status directly. Takes contact_id or an
inline contact to upsert through the same identity resolution
POST /v1/contacts uses.
Idempotent per (event, contact): re-sending the same status answers the same
registration with created: false and fires nothing twice. A registration made
here fires exactly what an in-app one fires — the event automations, the
event.attendance.changed webhook, lead scoring, and (on a Zoom-linked event)
the registrant push that produces the attendee's personal join link.
An optional acquisition object records how the registration was acquired, as one
touch on the contact's journey linked to the registration this call creates;
supplying acquisition.attendance_id yourself is a 400, because this call is the
link.
On an event with collect_payment_on_registration, a NEW registration records a
sale of the event's product and sends the contact a payment link — there is no
per-call opt-out. The payment block (sale_id, sale_status,
settlement_status, pay_url) is null for a registration that carries no sale.
Requires the events plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| contact_id | string <uuid> An existing contact. Mutually exclusive with |
object Identity to upsert when you have no | |
| status | string Default: "registered" Enum: "registered" "attended" "no_show" "waitlist" "unregistered" "cancelled" |
| zoom_registration | string Default: "auto" Enum: "auto" "skip"
|
| join_url | string <= 2000 characters Honoured with |
object (ContactAcquisition) An actual acquisition or submission, including for a returning contact. POST only. Nested source values take precedence over the corresponding top-level values for the event. |
{- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "contact": {
- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "national_id": "string"
}, - "status": "registered",
- "zoom_registration": "auto",
- "join_url": "string",
- "acquisition": {
- "event_id": "string",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "landing_url": "string",
- "referrer_url": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "platform_campaign_id": "string",
- "form_name": "string"
}
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "status": "registered",
- "registered_at": "2019-08-24T14:15:22Z",
- "attended_at": "2019-08-24T14:15:22Z",
- "unregistered_at": "2019-08-24T14:15:22Z",
- "join_url": "string",
- "payment_sale_id": "0239f7f7-dff6-4071-8c7f-9beb1635e80f",
- "payment": {
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "sale_status": "active",
- "settlement_status": "untracked",
- "pay_url": "string"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created": true,
- "previous_status": "string",
- "zoom": {
- "status": "registered",
- "join_url": "string"
}
}The same chokepoint as the POST — marking someone attended or no_show here
fires exactly what marking them in the app fires, and the same event-state
refusals apply. Moving someone back to registered on a priced event charges like
a new registration. Requires the events plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| status required | string Enum: "registered" "attended" "no_show" "waitlist" "unregistered" "cancelled" |
{- "status": "registered"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "status": "registered",
- "registered_at": "2019-08-24T14:15:22Z",
- "attended_at": "2019-08-24T14:15:22Z",
- "unregistered_at": "2019-08-24T14:15:22Z",
- "join_url": "string",
- "payment_sale_id": "0239f7f7-dff6-4071-8c7f-9beb1635e80f",
- "payment": {
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "sale_status": "active",
- "settlement_status": "untracked",
- "pay_url": "string"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created": true,
- "previous_status": "string",
- "zoom": {
- "status": "registered",
- "join_url": "string"
}
}The per-submission record behind the in-app Journey — one row per submission,
newest first (occurred_at descending), never a rollup. Requires the
attribution plan feature, in addition to API access; the same gate hides the
first_touch / last_touch blocks on the contact object.
kind is the transport oToK observed (api for anything sent through this API);
source is what the caller declared produced it. They answer different questions
and are never mapped onto one another.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| kind | string Enum: "web_visit" "form_submission" "ctwa_ad" "email_click" "campaign_reply" "import" "api" "whatconverts_lead" "lead_ad" "legacy" Narrow to one transport. Any value outside the enum returns 400. |
| limit | integer Page size (default 50, max 200). |
| offset | integer Rows to skip (default 0). |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}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",
- "source_page_title": "Newsletter signup",
- "user_agent": "Mozilla/5.0",
- "affiliate": "partner-123",
- "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",
- "national_id": "string",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "msclkid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "owner": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_touch": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_at": "2019-08-24T14:15:22Z"
}, - "last_touch": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_at": "2019-08-24T14:15:22Z"
}
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Creates or updates a contact. Identifier matching is history-aware, with precedence phone, email, then national_id. Scalar fields overwrite, custom_fields shallow-merge, and tag/group names are added. Both outcomes return 201; duplicate indicates whether an existing contact was updated.
When identifiers resolve to different current contacts whose identifiers are compatible, POST can merge them automatically and update the surviving contact. Conflicting or historical split matches require in-app review and return 409 CONTACT_MERGE_REQUIRED. Non-identity fields are held for resolution. Use the id returned by the API. Once a person resolves the conflict in the app with "Keep separate" and picks the contact the submission belongs to, a retry of the same submission is applied to that contact (201, duplicate true) without moving any submitted identifier onto it.
Use acquisition for a genuine submission, including for a returning contact. Reuse acquisition.event_id when retrying that submission for the same contact. Plain updates with top-level marketing fields do not create new acquisition events. Capture is best-effort; a successful profile write does not guarantee attribution capture. inquiry defaults to create (new contact or explicit acquisition); always includes ordinary updates, and never disables inquiry capture. Neither option is accepted by PATCH.
| phone | string <= 32 characters Normalized to E.164 using the workspace default country. An impossible incoming phone does not replace an existing valid phone; an empty string explicitly clears it. |
| 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 |
| source_page_url | string <= 2048 characters Marketing metadata. URL of the source page associated with the contact. |
| source_page_title | string <= 500 characters Marketing metadata. Title of the source page associated with the contact. |
| referrer | string <= 2048 characters Marketing metadata. Referring URL or source associated with the contact. |
| user_agent | string <= 2048 characters Marketing metadata. Browser or client user-agent string associated with the contact. |
| affiliate | string <= 200 characters Marketing metadata. Affiliate name or identifier associated with the contact. |
| 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. |
| national_id | string <= 20 characters Israeli national ID. Normalized to nine digits after check-digit validation; invalid values are ignored. Matches after phone and email. |
| owner_email | string <email> Login email of an active workspace member; takes precedence over owner_user_id. Unknown or inactive member returns 400 INVALID_CONTACT_OWNER. |
| owner_user_id | string or null <uuid> Active workspace member id; null clears ownership. Reassignment also transfers eligible live records owned by the previous owner. |
| msclkid | string <= 500 characters Microsoft advertising click id. |
| gbraid | string <= 500 characters Google advertising click id. |
| wbraid | string <= 500 characters Google advertising click id. |
| ttclid | string <= 500 characters TikTok click id. |
| li_fat_id | string <= 500 characters LinkedIn click id. |
object (ContactAcquisition) An actual acquisition or submission, including for a returning contact. POST only. Nested source values take precedence over the corresponding top-level values for the event. | |
| inquiry | string Default: "create" Enum: "create" "always" "never" create opens an inquiry for a new contact or an explicit acquisition; always also opens one on ordinary updates; never disables inquiry capture. POST only. For bulk backfills and CRM syncs use never. |
{- "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": 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",
- "source_page_title": "Newsletter signup",
- "user_agent": "Mozilla/5.0",
- "affiliate": "partner-123",
- "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"
], - "national_id": "string",
- "owner_email": "user@example.com",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "msclkid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "acquisition": {
- "event_id": "string",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "landing_url": "string",
- "referrer_url": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "platform_campaign_id": "string",
- "form_name": "string"
}, - "inquiry": "create"
}{- "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",
- "source_page_title": "Newsletter signup",
- "user_agent": "Mozilla/5.0",
- "affiliate": "partner-123",
- "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",
- "national_id": "string",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "msclkid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "owner": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_touch": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_at": "2019-08-24T14:15:22Z"
}, - "last_touch": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_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",
- "source_page_title": "Newsletter signup",
- "user_agent": "Mozilla/5.0",
- "affiliate": "partner-123",
- "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",
- "national_id": "string",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "msclkid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "owner": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_touch": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_at": "2019-08-24T14:15:22Z"
}, - "last_touch": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_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/national_id 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.
PATCH rejects acquisition and inquiry with 400 and never opens an inquiry. Automatic compatible-identifier merging belongs to POST upsert.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| phone | string <= 32 characters Normalized to E.164 using the workspace default country. An impossible incoming phone does not replace an existing valid phone; an empty string explicitly clears it. |
| 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 |
| source_page_url | string <= 2048 characters Marketing metadata. URL of the source page associated with the contact. |
| source_page_title | string <= 500 characters Marketing metadata. Title of the source page associated with the contact. |
| referrer | string <= 2048 characters Marketing metadata. Referring URL or source associated with the contact. |
| user_agent | string <= 2048 characters Marketing metadata. Browser or client user-agent string associated with the contact. |
| affiliate | string <= 200 characters Marketing metadata. Affiliate name or identifier associated with the contact. |
| 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. |
| national_id | string <= 20 characters Israeli national ID. Normalized to nine digits after check-digit validation; invalid values are ignored. Matches after phone and email. |
| owner_email | string <email> Login email of an active workspace member; takes precedence over owner_user_id. Unknown or inactive member returns 400 INVALID_CONTACT_OWNER. |
| owner_user_id | string or null <uuid> Active workspace member id; null clears ownership. Reassignment also transfers eligible live records owned by the previous owner. |
| msclkid | string <= 500 characters Microsoft advertising click id. |
| gbraid | string <= 500 characters Google advertising click id. |
| wbraid | string <= 500 characters Google advertising click id. |
| ttclid | string <= 500 characters TikTok click id. |
| li_fat_id | string <= 500 characters LinkedIn click id. |
{- "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": 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",
- "source_page_title": "Newsletter signup",
- "user_agent": "Mozilla/5.0",
- "affiliate": "partner-123",
- "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"
], - "national_id": "string",
- "owner_email": "user@example.com",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "msclkid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "ttclid": "string",
- "li_fat_id": "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",
- "source_page_title": "Newsletter signup",
- "user_agent": "Mozilla/5.0",
- "affiliate": "partner-123",
- "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",
- "national_id": "string",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "msclkid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "owner": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_touch": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_at": "2019-08-24T14:15:22Z"
}, - "last_touch": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "string",
- "kind": "web_visit",
- "source": "form",
- "occurred_at": "2019-08-24T14:15:22Z",
- "visitor_id": "string",
- "form_name": "string",
- "landing_url": "string",
- "conversion_url": "string",
- "landing_referrer": "string",
- "referrer_url": "string",
- "source_page_title": "string",
- "utm_source": "string",
- "utm_medium": "string",
- "utm_campaign": "string",
- "utm_term": "string",
- "utm_content": "string",
- "gclid": "string",
- "gbraid": "string",
- "wbraid": "string",
- "fbclid": "string",
- "msclkid": "string",
- "ttclid": "string",
- "li_fat_id": "string",
- "ctwa_clid": "string",
- "platform_campaign_id": "string",
- "params": {
- "property1": "string",
- "property2": "string"
}, - "user_agent": "string",
- "ip": "string",
- "attendance_id": "f181bede-756c-4d61-b24a-5a6056d815d5",
- "form_id": "46af4ef5-2c18-4098-9bc5-c048fb90b843",
- "landing_page_id": "414a5bce-f320-4a62-9fa5-1a44e5cc334f",
- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "email_campaign_id": "333b46b4-7eef-4149-98aa-6e7dc5d44027",
- "created_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": 0,
- "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",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451"
}
], - "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. |
| cycle_id | string <uuid> Cycle belonging to the attached product; mismatches return 400 CYCLE_PRODUCT_MISMATCH. PATCH null clears the cycle. |
{- "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": 0,
- "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",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451"
}{- "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",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "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",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451"
}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 |
| cycle_id | string or null <uuid> Cycle belonging to the attached product; mismatches return 400 CYCLE_PRODUCT_MISMATCH. PATCH null clears the cycle. |
{- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "title": "string",
- "amount": 0,
- "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",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451"
}{- "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",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451"
}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": 0
}{- "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",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451"
}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",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451"
}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,
- "dynamic_pricing": true,
- "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",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "archived",
- "enforce_cycle_capacity": false,
- "require_cycle": false,
- "attendance_on_sale": true,
- "recurring_sale_policy": "fill_one"
}
], - "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. |
| dynamic_pricing | boolean Default: true
|
| 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 remain attached to existing records. For dated products use manual_status: archived or cancelled; is_active: false alone returns 400. |
| starts_on | string or null <date> Workspace calendar start date. Must be a real YYYY-MM-DD date. |
| ends_on | string or null <date> Workspace calendar end date, not before starts_on. |
| duration_unit | string Default: "days" Enum: "days" "weeks" "months" "years" |
| manual_status | string or null Enum: "archived" "cancelled" null Archive or cancel explicitly; null removes the override. Dated products cannot be deactivated with is_active: false; set manual_status instead. |
| enforce_cycle_capacity | boolean Default: false Reject new sales into a full cycle with 409 CYCLE_FULL. Default is advisory capacity. |
| require_cycle | boolean Default: false Require a cycle on sales (400 CYCLE_REQUIRED). Deal cycles remain optional. |
| recurring_sale_policy | string Enum: "fill_one" "per_period" How a recurring payment plan for this product records sales: |
{- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "dynamic_pricing": true,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "is_active": true,
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "archived",
- "enforce_cycle_capacity": false,
- "require_cycle": false,
- "recurring_sale_policy": "fill_one"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "dynamic_pricing": true,
- "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",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "archived",
- "enforce_cycle_capacity": false,
- "require_cycle": false,
- "attendance_on_sale": true,
- "recurring_sale_policy": "fill_one",
- "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,
- "dynamic_pricing": true,
- "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",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "archived",
- "enforce_cycle_capacity": false,
- "require_cycle": false,
- "attendance_on_sale": true,
- "recurring_sale_policy": "fill_one"
}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. |
| dynamic_pricing | boolean Default: true
|
| 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 remain attached to existing records. For dated products use manual_status: archived or cancelled; is_active: false alone returns 400. |
| starts_on | string or null <date> Workspace calendar start date. Must be a real YYYY-MM-DD date. |
| ends_on | string or null <date> Workspace calendar end date, not before starts_on. |
| duration_unit | string Default: "days" Enum: "days" "weeks" "months" "years" |
| manual_status | string or null Enum: "archived" "cancelled" null Archive or cancel explicitly; null removes the override. Dated products cannot be deactivated with is_active: false; set manual_status instead. |
| enforce_cycle_capacity | boolean Default: false Reject new sales into a full cycle with 409 CYCLE_FULL. Default is advisory capacity. |
| require_cycle | boolean Default: false Require a cycle on sales (400 CYCLE_REQUIRED). Deal cycles remain optional. |
| recurring_sale_policy | string Enum: "fill_one" "per_period" How a recurring payment plan for this product records sales: |
{- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "dynamic_pricing": true,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "is_active": true,
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "archived",
- "enforce_cycle_capacity": false,
- "require_cycle": false,
- "recurring_sale_policy": "fill_one"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "name": "string",
- "sku": "string",
- "external_id": "string",
- "description": "string",
- "price": 0,
- "dynamic_pricing": true,
- "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",
- "starts_on": "2019-08-24",
- "ends_on": "2019-08-24",
- "duration_unit": "days",
- "manual_status": "archived",
- "enforce_cycle_capacity": false,
- "require_cycle": false,
- "attendance_on_sale": true,
- "recurring_sale_policy": "fill_one"
}One-time / recurring / installment payments, entries, refunds (through the payment gateway or recorded). Refunds need the API key's refund access (allow_money_out).
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.
With sale_id, the recorded charge(s) fund that existing sale — capped at what the
sale can still take — and sale_allocations reports what it took on this call.
status: "refunded" requires an API key with refund access (allow_money_out);
re-sending it for an existing one-time payment records a real refund entry (as on
PATCH).
| 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 |
| sale_id | string <uuid> An existing sale of the same contact for the recorded charge(s) to fund
(create only; requires the Sales feature). Errors: 404 |
| 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": 0,
- "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": 0,
- "installment_count": 2,
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "duplicate": true,
- "sale_allocations": [
- {
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "entry_id": "31513983-613b-4481-b270-ae929f358bae",
- "amount": 0
}
]
}| 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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "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).
status: "refunded" on a one-time payment records a real refund entry of the
charge's remaining balance (reason legacy_mark; recorded_outside: true when the
charge was collected through the connected payment provider — no money moves at
the gateway) instead of flipping the status, and requires an API key with refund
access (allow_money_out).
| 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": 0,
- "status": "pending",
- "auto_generate": true,
- "recurring_end_at": "string",
- "recurring_max_occurrences": 1,
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "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).
Marking a charge refunded records a real refund entry for its remaining
balance (reason legacy_mark; recorded_outside: true when the charge was
collected through the connected payment provider) instead of flipping the status,
and requires an API key with refund access (allow_money_out).
| 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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Refunds a completed charge through the same refund path as the in-app Refund
button. Requires an API key with refund access (allow_money_out, granted by
the workspace owner) — 403 API_KEY_MONEY_OUT_DISABLED otherwise.
Routing: a charge recorded through this API or by hand (no payment provider
involved) is always booked (outcome: "ledger_only"). A charge collected through
the connected payment provider is refunded at the gateway with mode: "auto"
(gateway_refunded, or voided when a same-day charge is cancelled before it
settles; needs the workspace_payments plan feature — 409 PLAN_FEATURE_REQUIRED
otherwise) or booked only with mode: "recorded_outside" (the money was, or
will be, returned outside oToK). A store-synced payment answers 409
REFUND_IN_STORE. Some providers answer a gateway refund with a credit document
only — outcome: "credit_document_only": the refund entry stays
status: "pending" until the money is confirmed returned.
Multiple partial refunds against one charge are supported, up to its recognized
value; refunds are race-safe. A fully refunded one-time payment's header
becomes arrangement_status: "cancelled". Fires the payment-refunded automation
and the opt-in payment.refunded webhook. With idempotency_key, a replay returns
the original refund with duplicate: true.
Breaking change: reason and mode are required, and the response
is a RefundResult instead of the bare payment.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| reason required | string (RefundReason) Enum: "requested_by_customer" "duplicate" "fraudulent" "order_change" "product_unsatisfactory" "sale_cancelled" "refunded_outside" "chargeback" "other" Why the refund is given — stored on the refund entry and printed on the provider credit document. |
| mode required | string (RefundMode) Enum: "auto" "recorded_outside"
|
| 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 |
| idempotency_key | string <= 200 characters Unique per workspace. A replay returns the original refund with
|
{- "reason": "requested_by_customer",
- "mode": "auto",
- "entry_id": "31513983-613b-4481-b270-ae929f358bae",
- "amount": 1,
- "note": "string",
- "idempotency_key": "string"
}{- "payment": {
- "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}, - "entry": {
- "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "outcome": "gateway_refunded",
- "duplicate": true,
- "incidentId": "string"
}Hosted pay-by-link requests collected through the workspace's own connected payment provider (Cardcom / Sumit). Mint (idempotent with idempotency_key; without one a repeat POST mints a second link), list, get, cancel, send by email/WhatsApp/SMS, issue the tax document, refund. 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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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",
- "channel": "email",
- "link_emailed_at": "2019-08-24T14:15:22Z",
- "link_whatsapp_sent_at": "2019-08-24T14:15:22Z",
- "link_sms_sent_at": "2019-08-24T14:15:22Z",
- "receipt_email_sent_at": "2019-08-24T14:15:22Z",
- "payment_method_id": "ec18d2d7-d2e0-41e4-98e4-847f14422d8a",
- "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".
Idempotent only with idempotency_key. With a key, a replay of the same charge
(same payer, amount, currency, sale_id, product_id) returns the original row
with duplicate: true and mints nothing; the key reused for a different charge
answers 409 IDEMPOTENCY_KEY_MISMATCH. Without a key a repeat POST mints a
second, independently payable link; cancel extras via
POST /v1/payment-requests/{id}/cancel, and do not blindly retry a keyless call
after a network failure without first checking GET /v1/payment-requests.
Optional sale_id collects for an existing sale of the payer; optional
product_id sells a catalog product (the title derives from its name) and
records a sale when the link is paid.
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 |
| terminal_number | integer >= 1 Configured Cardcom terminal number. Omit for the workspace default. Unknown terminal or a provider without terminals returns 400. |
| product_id | string <uuid> Catalog product — the title then derives from the product name. 400
|
| sale_id | string <uuid> An existing sale of the payer to collect for (allocated when the request is
paid). 404 unknown sale; 409 |
| idempotency_key | string <= 200 characters Unique per workspace. A replay returns the original request with
|
{- "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": 0,
- "terminal_number": 1,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "idempotency_key": "string"
}{- "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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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",
- "channel": "email",
- "link_emailed_at": "2019-08-24T14:15:22Z",
- "link_whatsapp_sent_at": "2019-08-24T14:15:22Z",
- "link_sms_sent_at": "2019-08-24T14:15:22Z",
- "receipt_email_sent_at": "2019-08-24T14:15:22Z",
- "payment_method_id": "ec18d2d7-d2e0-41e4-98e4-847f14422d8a",
- "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,
- "duplicate": true,
- "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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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",
- "channel": "email",
- "link_emailed_at": "2019-08-24T14:15:22Z",
- "link_whatsapp_sent_at": "2019-08-24T14:15:22Z",
- "link_sms_sent_at": "2019-08-24T14:15:22Z",
- "receipt_email_sent_at": "2019-08-24T14:15:22Z",
- "payment_method_id": "ec18d2d7-d2e0-41e4-98e4-847f14422d8a",
- "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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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",
- "channel": "email",
- "link_emailed_at": "2019-08-24T14:15:22Z",
- "link_whatsapp_sent_at": "2019-08-24T14:15:22Z",
- "link_sms_sent_at": "2019-08-24T14:15:22Z",
- "receipt_email_sent_at": "2019-08-24T14:15:22Z",
- "payment_method_id": "ec18d2d7-d2e0-41e4-98e4-847f14422d8a",
- "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
}Sends a pending request's pay-link to its contact on channel (email,
whatsapp or sms) — the same delivery as the in-app Send button. An API send goes
out once per channel: a second send on a channel the link was already sent on
answers 409 LINK_ALREADY_SENT (with the first sent_at) unless resend: true.
Email leaves only from the workspace's verified sender; WhatsApp outside the
contact's 24-hour window uses the payment-link message template, which must be
approved by Meta first. The first successful send sets the request's channel and
link_*_sent_at stamp.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| channel required | string Enum: "email" "whatsapp" "sms" |
| resend | boolean Send again on a channel the link was already sent on. Without it, a second send
on the same channel answers 409 |
| conversation_id | string <uuid> A conversation of the request's contact to associate with the request (the first
one recorded wins). 400 |
{- "channel": "email",
- "resend": true,
- "conversation_id": "cc71b11a-25cd-4c2d-9950-df2cc38e3407"
}{- "sent": true,
- "channel": "email",
- "to": "string",
- "message_id": "string"
}Issues the request's tax document (its document_kind, else the provider/account
default) through the connected provider — for a paid request whose automatic issue
was off or failed. No request body. A credit document kind
(credit_invoice, credit_invoice_receipt, credit_receipt,
credit_donation_receipt) requires an API key with refund access
(allow_money_out).
| 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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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",
- "channel": "email",
- "link_emailed_at": "2019-08-24T14:15:22Z",
- "link_whatsapp_sent_at": "2019-08-24T14:15:22Z",
- "link_sms_sent_at": "2019-08-24T14:15:22Z",
- "receipt_email_sent_at": "2019-08-24T14:15:22Z",
- "payment_method_id": "ec18d2d7-d2e0-41e4-98e4-847f14422d8a",
- "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
}Refunds the payment a paid request settled — the same refund path as
POST /v1/payments/{id}/refund, addressed by the request. mode: auto refunds
through the connected payment gateway; mode: recorded_outside books the refund
only. Requires an API key with refund access (allow_money_out). Refused with 409
REQUEST_NOT_REFUNDABLE (not paid, test mode, or no single refundable charge),
REQUEST_NOT_LEDGERED (not on the payments ledger yet — retry shortly) or
NO_PROVIDER_PAYMENT_REF, plus the payment refund's own refusals.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| reason required | string (RefundReason) Enum: "requested_by_customer" "duplicate" "fraudulent" "order_change" "product_unsatisfactory" "sale_cancelled" "refunded_outside" "chargeback" "other" Why the refund is given — stored on the refund entry and printed on the provider credit document. |
| mode required | string (RefundMode) Enum: "auto" "recorded_outside"
|
| amount | number >= 0.01 In the request's currency. Omitted → the full remaining refundable balance. |
| note | string <= 1000 characters |
| idempotency_key | string <= 200 characters Unique per workspace — a replay returns the original refund with |
{- "reason": "requested_by_customer",
- "mode": "auto",
- "amount": 0.01,
- "note": "string",
- "idempotency_key": "string"
}{- "payment": {
- "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}, - "entry": {
- "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "outcome": "gateway_refunded",
- "duplicate": true,
- "incidentId": "string",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653"
}A contact's saved cards (masked) and the one-click charge against one, through the workspace's own connected payment provider. Charging needs the API key's saved-card charge access (allow_charges). Requires the workspace_payments feature.
The contact's saved cards, newest first — display facts only, never card data or
the provider's card reference. Fields are camelCase. Requires the
workspace_payments feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "payment_methods": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "provider": "string",
- "brand": "string",
- "last4": "string",
- "expiryMonth": 0,
- "expiryYear": 0,
- "tokenExpiresAt": "2019-08-24T14:15:22Z",
- "status": "active",
- "isDefault": true,
- "consentSource": "checkout_optin",
- "consentedAt": "2019-08-24T14:15:22Z",
- "lastUsedAt": "2019-08-24T14:15:22Z",
- "createdAt": "2019-08-24T14:15:22Z"
}
]
}Charges one of the contact's saved cards (method_id, else chosen by
method_selection) through the connected payment provider — the in-app
Charge saved card. The charge is a payment request with
charge_kind: "token", recorded on the payments ledger once paid.
Requires an API key with saved-card charge access (allow_charges) — off by
default on every key, granted only by the workspace owner — and the
workspace_payments feature. The capability is checked first:
403 API_KEY_CHARGES_DISABLED.
Idempotent by the required idempotency_key (scoped to the calling API key): a
replay answers the original outcome with duplicate: true and never charges
twice; the key reused with a different contact, amount or currency answers 409
IDEMPOTENCY_KEY_MISMATCH. A new attempt (e.g. after a decline) needs a new key.
Card data is never accepted — unknown body fields are rejected.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| idempotency_key required | string [ 8 .. 124 ] characters Required. Scoped to the calling API key; a replay answers the original outcome
with |
| method_id | string <uuid> The saved card to charge. Omitted → chosen by |
| method_selection | string Default: "default" Enum: "default" "most_recent" When no |
| amount required | number [ 0.01 .. 9999999999.99 ] Major units, at most 2 decimal places. |
| currency | string Enum: "ILS" "USD" "EUR" "GBP" Omitted → the workspace payment currency. |
| title | string <= 200 characters |
| product_id | string <uuid> Catalog product — the title then derives from the product name. |
| note | string <= 2000 characters |
| document_kind | string Tax-document kind to auto-issue (the |
| auto_issue_document | boolean |
| installments | integer [ 1 .. 36 ] Card installments to charge with. |
| test_mode | boolean Authorise-only test run — 400 |
| deal_id | string <uuid> |
| vat_mode | string Enum: "inclusive" "exclusive" Per-charge VAT override — travels as a pair with |
| vat_rate | number [ 0 .. 100 ] |
| sale_id | string <uuid> An existing sale of this contact to collect for (allocated when the charge settles). |
| terminal_number | integer >= 1 Configured Cardcom terminal number. Omitted → the workspace default terminal. |
{- "idempotency_key": "stringst",
- "method_id": "1c8a5e9f-61ad-49f4-a4e1-be3564560ed2",
- "method_selection": "default",
- "amount": 0.01,
- "currency": "ILS",
- "title": "string",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "note": "string",
- "document_kind": "string",
- "auto_issue_document": true,
- "installments": 1,
- "test_mode": true,
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "terminal_number": 1
}{- "outcome": "paid",
- "duplicate": true,
- "payment_request": {
- "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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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",
- "channel": "email",
- "link_emailed_at": "2019-08-24T14:15:22Z",
- "link_whatsapp_sent_at": "2019-08-24T14:15:22Z",
- "link_sms_sent_at": "2019-08-24T14:15:22Z",
- "receipt_email_sent_at": "2019-08-24T14:15:22Z",
- "payment_method_id": "ec18d2d7-d2e0-41e4-98e4-847f14422d8a",
- "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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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 and — unless the workspace switched it off in its Sales
settings — records the order as a sale (stamping sale_id). Pass sale_id to
fund an existing sale instead. sale_id is a create-time link only: an
external_reference match never re-points it. 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 |
| sale_id | string <uuid> An existing sale of the same contact for this order to fund, instead of
recording a new one (create only). 404 unknown sale; 409
|
| 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": 0,
- "quantity": 1,
- "discount_percent": 0
}
], - "currency": "string",
- "discount_total": 0,
- "shipping_total": 0,
- "tax_total": 0,
- "financial_status": "pending",
- "placed_at": "2019-08-24T14:15:22Z",
- "coupon_codes": [
- "string"
], - "note": "string",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "external_reference": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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": 0,
- "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
(and payment.refunded for the mirrored payment refund).
This route records a refund — it never moves money at a payment gateway —
and requires an API key with refund access (allow_money_out). When the order's
payment was collected through the connected payment provider it answers 409
REFUND_VIA_PROVIDER_REQUIRED with the payment_id to refund through
POST /v1/payments/{id}/refund instead (the order follows automatically); a
payment from a connected store answers 409 REFUND_IN_STORE.
Refunds require the order to have ever been paid (paid_at set) and to be
fully captured — paid, partially_refunded or refunded; a partially_paid
or voided order answers 409 ORDER_NOT_CAPTURED. 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": 1,
- "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,
records the order as a sale (unless the workspace switched it off in its Sales
settings; with sale_id the payment funds that existing sale instead), 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 |
| sale_id | string <uuid> An existing sale this payment funds — no new sale is recorded. Same
checks as on create ( |
{- "payment_reference": "string",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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": 0,
- "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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "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": 0,
- "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"
}
]
}The sales ledger — record sales (idempotent via external_reference), cancel / reinstate items, refund, allocate existing charges. Recording a sale never charges the buyer. Requires the sales feature.
Sale headers, newest sold_at first (no items, allocations or buyer details).
Pages like deals/payments (limit default 25, cap 100 — larger values are clamped;
malformed paging returns 400). Unlike deals/payments, an unknown status or
settlement_status value returns 400. Requires the sales plan feature.
| contact_id | string <uuid> The buyer. Malformed → 400 |
| owner_user_id | string <uuid> Malformed → 400. |
| status | string (Sale status) Enum: "active" "partially_cancelled" "cancelled" Unknown values → 400 |
| settlement_status | string (Sale settlement status) Enum: "untracked" "unpaid" "partially_paid" "paid" "partially_refunded" "refunded" Unknown values → 400 |
| external_reference | string Exact match — look a sale up by your own key. |
| 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",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "currency": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true,
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "external_reference": "string",
- "status": "active",
- "line_count": 0,
- "total_amount": 0,
- "has_unpriced_lines": true,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "settlement_status": "untracked",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}Records a sale — a header over 1–100 line items, each an active catalog product.
Recording a sale does not charge the buyer. A repeated external_reference
writes nothing and returns the original sale with duplicate: true (201 either
way). References starting with order:, quote:, pr:, cycle:, mayab:, store:,
cardcom:, form:, booking: or event: are reserved for sales the app records
itself (400 RESERVED_EXTERNAL_REFERENCE).
Fires the sale-recorded automations and the sale.recorded webhook.
Requires the sales plan feature.
| contact_id required | string <uuid> The buyer. |
required | Array of objects (Sale item (create)) [ 1 .. 100 ] items |
| currency | string = 3 characters Omitted → the workspace payment currency. |
| sold_at | string <date-time> Omitted → now. |
| note | string or null <= 2000 characters |
| deal_id | string or null <uuid> A deal of the same buyer. |
| owner_user_id | string or null <uuid> Omitted → the buyer's owner (while still a member), else unowned; |
| external_reference | string or null <= 200 characters Idempotency key, unique per workspace. |
| vat_mode | string or null Enum: "inclusive" "exclusive" null Always together with |
| vat_rate | number or null [ 0 .. 100 ] |
| expects_payment | boolean Default: true |
{- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "items": [
- {
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "quantity": 1,
- "unit_amount": 0,
- "discount_percent": 0,
- "recipient_contact_id": "5eac5e12-afec-481c-9f5c-255d733b75b3"
}
], - "currency": "str",
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "external_reference": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true
}{- "sale": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "currency": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true,
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "external_reference": "string",
- "status": "active",
- "line_count": 0,
- "total_amount": 0,
- "has_unpriced_lines": true,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "settlement_status": "untracked",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "position": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "recipient_contact_id": "5eac5e12-afec-481c-9f5c-255d733b75b3",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "title": "string",
- "quantity": 0,
- "unit_amount": 0,
- "discount_percent": 0,
- "line_total": 0,
- "currency": "string",
- "purchased_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "status": "active",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancel_reason": "string",
- "cancel_note": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "duplicate": true
}The full sale view. tier is 1 for an ordinary key and 2 for a key with the
Allow refunds capability (allow_money_out), which adds money and
incidents and provider details on pay_links. Sections that do not apply are
omitted, not null. Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "tier": 1,
- "sale": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "currency": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true,
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "external_reference": "string",
- "status": "active",
- "line_count": 0,
- "total_amount": 0,
- "has_unpriced_lines": true,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "settlement_status": "untracked",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "buyer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "phone": "string",
- "email": "string",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "anonymised_at": "2019-08-24T14:15:22Z"
}, - "owner": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "full_name": "string",
- "email": "string"
}, - "created_by_user": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "full_name": "string",
- "email": "string"
}, - "outstanding": 0,
- "allocatable": 0,
- "possible_duplicates": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "source": "string",
- "sold_at": "2019-08-24T14:15:22Z",
- "status": "active",
- "shared_product_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
]
}
], - "fx": {
- "workspace_currency": "string",
- "rate": 0,
- "converted_total": 0,
- "warning": "string"
}
}, - "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "position": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "recipient_contact_id": "5eac5e12-afec-481c-9f5c-255d733b75b3",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "title": "string",
- "quantity": 0,
- "unit_amount": 0,
- "discount_percent": 0,
- "line_total": 0,
- "currency": "string",
- "purchased_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "status": "active",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancel_reason": "string",
- "cancel_note": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "product": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "is_active": true
}, - "cycle": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "starts_on": "string",
- "ends_on": "string"
}
}
], - "allocations": [
- {
- "allocation": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "payment_entry_id": "4ffcead0-00eb-4954-8c43-fab729d91a7a",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "amount": 0,
- "currency": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z"
}, - "entry": { },
- "payment": { },
- "payer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "payer_is_other_contact": true,
- "remaining": 0,
- "twins": [
- {
- "allocation": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "payment_entry_id": "4ffcead0-00eb-4954-8c43-fab729d91a7a",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "amount": 0,
- "currency": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z"
}, - "entry": { }
}
]
}
], - "pay_links": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "status": "string",
- "amount": 0,
- "currency": "string",
- "title": "string",
- "charge_kind": "string",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "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"
}
], - "related": {
- "deal": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "title": "string",
- "status": "string"
}, - "quotes": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "status": "string"
}
], - "orders": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "status": "string"
}
]
}, - "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"
}
]
}
], - "timeline": [
- {
- "kind": "string",
- "at": "2019-08-24T14:15:22Z",
- "id": "string"
}
], - "actions": {
- "property1": {
- "allowed": true,
- "blockers": [
- "string"
]
}, - "property2": {
- "allowed": true,
- "blockers": [
- "string"
]
}
}, - "money": {
- "charges": [
- {
- "paymentId": "472e651e-5a1e-424d-8098-23858bf03ad7",
- "entryId": "09a8b554-45ca-4bab-a638-265db4b3e828",
- "net": 0,
- "remaining": 0,
- "amount": 0,
- "route": "string",
- "blockers": [
- "string"
]
}
]
}, - "incidents": [
- { }
]
}The note is the only editable header field — money, items and the buyer are fixed
once recorded. An empty body returns 400 "Provide a note to update (null clears it).". Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| note | string or null <= 2000 characters
|
{- "note": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "currency": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true,
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "external_reference": "string",
- "status": "active",
- "line_count": 0,
- "total_amount": 0,
- "has_unpriced_lines": true,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "settlement_status": "untracked",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Permanently deletes a sale recorded by mistake, with its items. Needs the
"Allow permanent deletion through the API" key capability (allow_hard_delete,
granted by the workspace owner) — 403 HARD_DELETE_OWNER_ONLY otherwise. A sale
with allocated money, or one a quote, order or pay-link points at, cannot be
deleted (409 SALE_HAS_ALLOCATIONS — cancel it instead). Fires no webhook.
Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
{- "success": true
}Cancels every active item, or the listed ones. Money stays on the sale by default.
A money.mode of refund or recorded_outside needs the Allow refunds key
capability (allow_money_out; 403 API_KEY_MONEY_OUT_DISABLED, checked before
anything is cancelled) and a full cancel (400 PARTIAL_CANCEL_KEEPS_MONEY).
The items are cancelled first; the refunds run afterwards, one per funding charge.
A refused refund does not undo the cancellation — it is reported in refunds[].
A full cancel also withdraws the sale's pending pay-links, waives an unpaid
booking deposit the sale was created for, and — per the workspace's sales
settings — cancels the order the sale was created from. Each cancelled item fires
the sale-cancelled automations and a sale.cancelled webhook.
Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| reason required | string Enum: "customer_request" "duplicate" "mistake" "not_delivered" "payment_failed" "fraud" "other" |
| items | Array of strings <uuid> <= 100 items [ items <uuid > ] Item ids of this sale. Omitted → every active item. |
| note | string or null <= 2000 characters |
object Omitted = |
{- "reason": "customer_request",
- "items": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "note": "string",
- "money": {
- "mode": "keep",
- "reason": "requested_by_customer"
}
}{- "sale": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "currency": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true,
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "external_reference": "string",
- "status": "active",
- "line_count": 0,
- "total_amount": 0,
- "has_unpriced_lines": true,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "settlement_status": "untracked",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "cancelled": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "position": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "recipient_contact_id": "5eac5e12-afec-481c-9f5c-255d733b75b3",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "title": "string",
- "quantity": 0,
- "unit_amount": 0,
- "discount_percent": 0,
- "line_total": 0,
- "currency": "string",
- "purchased_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "status": "active",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancel_reason": "string",
- "cancel_note": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "refunds": [
- {
- "paymentId": "472e651e-5a1e-424d-8098-23858bf03ad7",
- "entryId": "09a8b554-45ca-4bab-a638-265db4b3e828",
- "amount": 0,
- "remaining": 0,
- "mode": "auto",
- "outcome": "gateway_refunded",
- "refundEntryId": "975452fc-f7a5-4ab4-a5c3-67c00f3d0af4",
- "incidentId": "c7a5d545-e8ac-4e92-ab32-6a98dfe2572c",
- "errorCode": "string",
- "error": "string"
}
], - "pay_links_cancelled": 0
}Reinstates every cancelled item, or the listed ones. Money is not touched; no
webhook fires. Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| items | Array of strings <uuid> <= 100 items [ items <uuid > ] Item ids of this sale. Omitted → every cancelled item. |
{- "items": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
]
}{- "sale": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "currency": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true,
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "external_reference": "string",
- "status": "active",
- "line_count": 0,
- "total_amount": 0,
- "has_unpriced_lines": true,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "settlement_status": "untracked",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "reinstated": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "position": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "recipient_contact_id": "5eac5e12-afec-481c-9f5c-255d733b75b3",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "title": "string",
- "quantity": 0,
- "unit_amount": 0,
- "discount_percent": 0,
- "line_total": 0,
- "currency": "string",
- "purchased_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "status": "active",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancel_reason": "string",
- "cancel_note": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Refunds one funding charge, directed at this sale; the items stay active.
Needs the Allow refunds key capability (allow_money_out) — 403
API_KEY_MONEY_OUT_DISABLED otherwise. entry_id may be omitted only when one
charge funds the sale; amount defaults to that charge's remaining share of this
sale. An idempotency_key replay returns the original refund with
duplicate: true. Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| reason required | string (SaleRefundReason) Enum: "requested_by_customer" "duplicate" "fraudulent" "order_change" "product_unsatisfactory" "sale_cancelled" "refunded_outside" "chargeback" "other" |
| mode required | string Enum: "auto" "recorded_outside"
|
| entry_id | string <uuid> The charge entry. May be omitted only when exactly one charge funds the sale. |
| amount | number > 0 Omitted → the charge's whole remaining share of this sale. |
| note | string <= 1000 characters |
| idempotency_key | string <= 200 characters Unique per workspace; a replay returns the original refund with |
{- "reason": "requested_by_customer",
- "mode": "auto",
- "entry_id": "31513983-613b-4481-b270-ae929f358bae",
- "amount": 1,
- "note": "string",
- "idempotency_key": "string"
}{- "payment": {
- "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}, - "entry": {
- "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",
- "reason": "requested_by_customer",
- "recorded_outside": true,
- "is_void": true,
- "client_idempotency_key": "string",
- "performed_by": "cdd093de-e8ce-4039-94a3-ecee6e78cf85",
- "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653",
- "provider": "string",
- "provider_payment_ref": "string",
- "credit_document": { },
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "outcome": "gateway_refunded",
- "incidentId": "c7a5d545-e8ac-4e92-ab32-6a98dfe2572c",
- "duplicate": true
}Does not change the contact's owner. Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| owner_user_id required | string or null <uuid> An active team member, or |
{- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "currency": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true,
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "external_reference": "string",
- "status": "active",
- "line_count": 0,
- "total_amount": 0,
- "has_unpriced_lines": true,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "settlement_status": "untracked",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| deal_id required | string or null <uuid> A deal of the sale's buyer, or |
{- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "currency": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "expects_payment": true,
- "sold_at": "2019-08-24T14:15:22Z",
- "note": "string",
- "source": "string",
- "external_reference": "string",
- "status": "active",
- "line_count": 0,
- "total_amount": 0,
- "has_unpriced_lines": true,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "settlement_status": "untracked",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Links a charge entry from the payments ledger (/v1/payments → entries[].id) to
the sale. Nothing is charged. The charge's earlier refunds are linked too (as
negative allocations). amount defaults to the smaller of what the charge has
not allocated elsewhere and what the sale can still take.
Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| payment_entry_id required | string <uuid> A charge entry in |
| amount | number [ 0.01 .. 1000000000 ] Omitted → the smaller of what the charge has left and what the sale can still take. |
{- "payment_entry_id": "4ffcead0-00eb-4954-8c43-fab729d91a7a",
- "amount": 0.01
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "payment_entry_id": "4ffcead0-00eb-4954-8c43-fab729d91a7a",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "amount": 0,
- "currency": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z"
}The charge no longer funds this sale; its refunds on this sale are released with
it. No money moves. Needs the "Allow permanent deletion through the API" key
capability (allow_hard_delete) — 403 HARD_DELETE_OWNER_ONLY otherwise.
Requires the sales plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| allocationId required | string <uuid> Allocation UUID. A non-UUID value returns 400. |
{- "released": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "payment_entry_id": "4ffcead0-00eb-4954-8c43-fab729d91a7a",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "amount": 0,
- "currency": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z"
}, - "twins": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "payment_entry_id": "4ffcead0-00eb-4954-8c43-fab729d91a7a",
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "amount": 0,
- "currency": "string",
- "source": "string",
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "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; available on every plan with API access.
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.
Not plan-gated: every workspace can list its sender profiles (they are also
what POST /v1/emails sends from), so only plan-wide API access is needed.
| 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.*, payment.*, contact.*, message.received, deal.*, sale.*, booking.*, event.attendance.changed, form.submitted, and ticket.* 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "provider": "string",
- "amount": 0,
- "currency": "string",
- "title": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "test_mode": true,
- "pay_url": "string",
- "channel": "email",
- "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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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,
- "sale_id": null,
- "provider": "sumit",
- "amount": 250,
- "currency": "ILS",
- "title": "Onboarding session",
- "vat_mode": "inclusive",
- "vat_rate": 18,
- "test_mode": false,
- "channel": "email",
- "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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "provider": "string",
- "amount": 0,
- "currency": "string",
- "title": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "test_mode": true,
- "pay_url": "string",
- "channel": "email",
- "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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "provider": "string",
- "amount": 0,
- "currency": "string",
- "title": "string",
- "vat_mode": "inclusive",
- "vat_rate": 0,
- "test_mode": true,
- "pay_url": "string",
- "channel": "email",
- "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 payment event — delivered only to endpoints that list it explicitly in
events. Fires once per refund entry, from any source: the refund routes of this
API, order refunds, marking a charge refunded, in-app refunds, refunds made in a
connected store, and refunds made directly in the payment provider's dashboard. A
pending reversal is silent until the money is confirmed returned.
| 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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. Use it as your dedup key. |
| type required | string (Payment event type) Value: "payment.refunded" The payment ledger event. Opt-in: delivered only to endpoints that list it
explicitly in Value: "payment.refunded" |
| created_at required | string <date-time> |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "payment.refunded",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "payment": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "title": "string",
- "total_amount": 0,
- "currency": "string",
- "arrangement_status": "string",
- "external_reference": "string"
}, - "refund": {
- "entry_id": "31513983-613b-4481-b270-ae929f358bae",
- "amount": 0,
- "currency": "string",
- "reason": "string",
- "note": "string",
- "recorded_outside": true,
- "is_void": true,
- "occurred_at": "2019-08-24T14:15:22Z"
}, - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "phone": "string",
- "email": "string"
}
}
}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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 sale event — delivered only to endpoints that list it explicitly in
events. Fires once per sale, for every source (API, in-app, automations, and
the sales the app records from quotes, orders, pay-links, bookings and event
registrations). An external_reference replay does not fire it again.
| 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 (Sale event type) Enum: "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" The four sale events. All are opt-in: delivered only to endpoints that list
them explicitly in Value: "sale.recorded" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "f6071829-3a4b-4c5d-8e9f-0a1b2c3d4e5f",
- "type": "sale.recorded",
- "created_at": "2026-10-01T09:00:01.000Z",
- "data": {
- "sale": {
- "id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
- "number": 1042,
- "contact_id": "9c2f1a4e-3b7d-4e2a-9f0c-1d2e3f4a5b6c",
- "deal_id": null,
- "owner_user_id": "708192a3-b4c5-d6e7-f809-1a2b3c4d5e6f",
- "status": "active",
- "settlement_status": "unpaid",
- "currency": "ILS",
- "total_amount": 350,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "sold_at": "2026-10-01T09:00:00.000Z",
- "source": "api",
- "external_reference": "crm-invoice-4471",
- "items": [
- {
- "id": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e",
- "position": 0,
- "product_id": "2b3c4d5e-6f70-8192-a3b4-c5d6e7f8091a",
- "cycle_id": null,
- "title": "Onboarding session",
- "quantity": 1,
- "unit_amount": 350,
- "discount_percent": null,
- "line_total": 350,
- "status": "active"
}
]
}, - "contact": {
- "id": "9c2f1a4e-3b7d-4e2a-9f0c-1d2e3f4a5b6c",
- "name": "Dana Levi",
- "phone": "+972501234567",
- "email": "dana@example.com"
}
}
}Opt-in sale event — delivered only to endpoints that list it explicitly in
events. Fires once per cancelled item: cancelling a three-item sale
delivers three events. Unlike the other sale events, data.sale is the
item (data.sale.id is the item id, data.sale.sale_id the sale's id).
Reinstating an item fires 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 (Sale event type) Enum: "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" The four sale events. All are opt-in: delivered only to endpoints that list
them explicitly in Value: "sale.cancelled" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "sale.cancelled",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "sale": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
- "position": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "title": "string",
- "quantity": 0,
- "unit_amount": 0,
- "discount_percent": 0,
- "line_total": 0,
- "currency": "string",
- "purchased_at": "2019-08-24T14:15:22Z",
- "status": "active",
- "cancelled_at": "2019-08-24T14:15:22Z",
- "cancel_reason": "string",
- "cancel_note": "string"
}, - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "phone": "string",
- "email": "string"
}
}
}Opt-in sale event — delivered only to endpoints that list it explicitly in
events. Fires when the sale's settlement_status becomes paid; the
settlement block names the payment that completed it (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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 (Sale event type) Enum: "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" The four sale events. All are opt-in: delivered only to endpoints that list
them explicitly in Value: "sale.paid" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "sale.paid",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "sale": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "status": "active",
- "settlement_status": "untracked",
- "currency": "string",
- "total_amount": 0,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "sold_at": "2019-08-24T14:15:22Z",
- "source": "string",
- "external_reference": "string",
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "position": 0,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "title": "string",
- "quantity": 0,
- "unit_amount": 0,
- "discount_percent": 0,
- "line_total": 0,
- "status": "active"
}
]
}, - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "phone": "string",
- "email": "string"
}, - "settlement": {
- "transition_id": "7a776551-a629-491a-b81a-3d501604a9fd",
- "paid_total": 0,
- "total_amount": 0,
- "payment": {
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "entry_id": "31513983-613b-4481-b270-ae929f358bae",
- "amount": 0
}
}
}
}Opt-in sale event — delivered only to endpoints that list it explicitly in
events. Fires when the sale's settled refunded total grows; a refund that is
still pending fires once it settles. The refund block describes the refund.
| 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 (Sale event type) Enum: "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" The four sale events. All are opt-in: delivered only to endpoints that list
them explicitly in Value: "sale.refunded" |
| created_at required | string <date-time> When the event occurred. |
required | object |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "sale.refunded",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "sale": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
- "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d",
- "status": "active",
- "settlement_status": "untracked",
- "currency": "string",
- "total_amount": 0,
- "paid_total": 0,
- "refunded_total": 0,
- "pending_refund_total": 0,
- "sold_at": "2019-08-24T14:15:22Z",
- "source": "string",
- "external_reference": "string",
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "position": 0,
- "product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
- "cycle_id": "4657c0f7-0ec5-4c38-8330-8d4bbac49451",
- "title": "string",
- "quantity": 0,
- "unit_amount": 0,
- "discount_percent": 0,
- "line_total": 0,
- "status": "active"
}
]
}, - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "phone": "string",
- "email": "string"
}, - "refund": {
- "payment_id": "d43b87f9-9e28-4802-8eaa-6ee91a40ea71",
- "entry_id": "31513983-613b-4481-b270-ae929f358bae",
- "amount": 0,
- "reason": "string",
- "recorded_outside": true,
- "refunded_total": 0
}
}
}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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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": { }
}
}Opt-in ticket event — delivered only to endpoints that list it explicitly in events,
and only while the plan includes Customer tickets. Fires for a ticket opened by the
customer (help page, website widget), by a team member on their behalf, or through the
API. Carries the opening message (data.message) and the assignee the ticket landed
on — there is no separate ticket.message_created or ticket.assigned for the
opening. A ticket filed into spam 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 (Ticket event type) Enum: "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" The four customer-ticket events. All are opt-in: delivered only to endpoints that
list them explicitly in Value: "ticket.created" |
| created_at required | string <date-time> When the event occurred. |
required | object (Ticket event data)
|
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "ticket.created",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "ticket": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "message": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "side": "customer",
- "origin": "customer",
- "author": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "body": "string",
- "attachment": {
- "filename": "string",
- "mime_type": "string",
- "size": 0
}, - "created_at": "2019-08-24T14:15:22Z"
}, - "change": {
- "status": {
- "from": "open",
- "to": "open"
}, - "origin": "agent",
- "assignee": {
- "from_user_id": "472d9673-1636-45ab-85c7-530f7aa9e49c",
- "to_user_id": "bdcaa974-8e23-4fc9-823d-e060850bbef1"
}
}
}
}Opt-in ticket event. Fires for every customer or team message — never an internal
note, and not while the ticket is in spam. data.message.origin (customer, agent,
automation, api) lets a bridge drop its own API posts. A status move the message
caused (a customer reply reopening the ticket, a team reply moving it to Answered)
rides data.ticket.status — no ticket.status_changed is sent for it.
| 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 (Ticket event type) Enum: "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" The four customer-ticket events. All are opt-in: delivered only to endpoints that
list them explicitly in Value: "ticket.message_created" |
| created_at required | string <date-time> When the event occurred. |
required | object (Ticket event data)
|
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "ticket.message_created",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "ticket": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "message": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "side": "customer",
- "origin": "customer",
- "author": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "body": "string",
- "attachment": {
- "filename": "string",
- "mime_type": "string",
- "size": 0
}, - "created_at": "2019-08-24T14:15:22Z"
}, - "change": {
- "status": {
- "from": "open",
- "to": "open"
}, - "origin": "agent",
- "assignee": {
- "from_user_id": "472d9673-1636-45ab-85c7-530f7aa9e49c",
- "to_user_id": "bdcaa974-8e23-4fc9-823d-e060850bbef1"
}
}
}
}Opt-in ticket event. Fires for every explicit status change — by a team member, the
customer, an automation, the API, or the system closing a quietly resolved ticket
(data.change.origin), moves into and out of spam included. data.change.status
carries from → to.
| 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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 (Ticket event type) Enum: "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" The four customer-ticket events. All are opt-in: delivered only to endpoints that
list them explicitly in Value: "ticket.status_changed" |
| created_at required | string <date-time> When the event occurred. |
required | object (Ticket event data)
|
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "ticket.status_changed",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "ticket": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "message": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "side": "customer",
- "origin": "customer",
- "author": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "body": "string",
- "attachment": {
- "filename": "string",
- "mime_type": "string",
- "size": 0
}, - "created_at": "2019-08-24T14:15:22Z"
}, - "change": {
- "status": {
- "from": "open",
- "to": "open"
}, - "origin": "agent",
- "assignee": {
- "from_user_id": "472d9673-1636-45ab-85c7-530f7aa9e49c",
- "to_user_id": "bdcaa974-8e23-4fc9-823d-e060850bbef1"
}
}
}
}Opt-in ticket event. Fires when triage changes a ticket's assignee, unassigning
included. data.change.assignee carries from_user_id → to_user_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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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 (Ticket event type) Enum: "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" The four customer-ticket events. All are opt-in: delivered only to endpoints that
list them explicitly in Value: "ticket.assigned" |
| created_at required | string <date-time> When the event occurred. |
required | object (Ticket event data)
|
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "ticket.assigned",
- "created_at": "2019-08-24T14:15:22Z",
- "data": {
- "ticket": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "message": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "side": "customer",
- "origin": "customer",
- "author": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "body": "string",
- "attachment": {
- "filename": "string",
- "mime_type": "string",
- "size": 0
}, - "created_at": "2019-08-24T14:15:22Z"
}, - "change": {
- "status": {
- "from": "open",
- "to": "open"
}, - "origin": "agent",
- "assignee": {
- "from_user_id": "472d9673-1636-45ab-85c7-530f7aa9e49c",
- "to_user_id": "bdcaa974-8e23-4fc9-823d-e060850bbef1"
}
}
}
}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" "payment.refunded" "contact.created" "contact.updated" "contact.deleted" "contact.consent_changed" "message.received" "deal.created" "deal.stage_changed" "deal.won" "deal.lost" "sale.recorded" "sale.cancelled" "sale.paid" "sale.refunded" "booking.created" "booking.rescheduled" "booking.cancelled" "booking.reassigned" "event.attendance.changed" "form.submitted" "ticket.created" "ticket.message_created" "ticket.status_changed" "ticket.assigned" 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. A blank
limit or offset (?limit=) means the default, not 0.
| 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": "invitee",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "deposit": {
- "state": "none",
- "amount": 0,
- "hold_until": "2019-08-24T14:15:22Z",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33"
}, - "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": "invitee",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "deposit": {
- "state": "none",
- "amount": 0,
- "hold_until": "2019-08-24T14:15:22Z",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33"
}, - "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": "invitee",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "deposit": {
- "state": "none",
- "amount": 0,
- "hold_until": "2019-08-24T14:15:22Z",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33"
}, - "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": "invitee",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "deposit": {
- "state": "none",
- "amount": 0,
- "hold_until": "2019-08-24T14:15:22Z",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33"
}, - "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": "invitee",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "deposit": {
- "state": "none",
- "amount": 0,
- "hold_until": "2019-08-24T14:15:22Z",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33"
}, - "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": "invitee",
- "cancellation_reason": "string",
- "rescheduled_at": "2019-08-24T14:15:22Z",
- "previous_start_at": "2019-08-24T14:15:22Z",
- "reschedule_count": 0,
- "deposit": {
- "state": "none",
- "amount": 0,
- "hold_until": "2019-08-24T14:15:22Z",
- "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Customer tickets — open (idempotent via external_reference), read with a paged conversation, reply to the customer (idempotent via idempotency_key), and triage. No DELETE. Requires the customer_tickets feature.
Filters are ANDed. Newest activity first by default. status=all (the default) is
every status except spam — ask for status=spam explicitly. List rows carry no
messages. Requires the customer_tickets plan feature.
| status | string Default: "all" Enum: "open" "pending" "resolved" "closed" "spam" "all" |
| contact_id | string <uuid> Only this contact's tickets. |
| assignee | string A team member's user id, or |
| category | string <= 80 characters Exact category label, case-insensitive. |
| priority | string (TicketPriority) Enum: "low" "normal" "high" "urgent" |
| source | string (TicketSource) Enum: "portal" "widget" "agent" "api"
|
| external_reference | string <= 200 characters Your own key, exact match (whitespace trimmed). A blank value is a 400, never "no filter". |
| created_from | string <date-time> Created at/after this instant. A date that does not exist → 400 |
| created_to | string <date-time> Created at/before this instant. A date that does not exist → 400 |
| sort | string Default: "-last_message_at" Enum: "last_message_at" "-last_message_at" "created_at" "-created_at" |
| limit | integer >= 0 Default: 50 Page size, default 50, max 200 (larger values are clamped). |
| offset | integer >= 0 Default: 0 |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_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
}Takes contact_id or an inline contact, upserted through the same identity
resolution POST /v1/contacts uses. opened_by: customer (the default) behaves like
a ticket opened on the help page — routing, the first-response target, the team's
new-ticket alert, and a blocked contact's ticket filed silently into spam;
opened_by: team behaves like a ticket a team member logged on the customer's
behalf.
Idempotency: a call carrying an external_reference that already exists changes
nothing and answers the original ticket with duplicate: true and status 200. The
same reference on a different contact answers 409 external_reference_conflict.
The customer is emailed their ticket link (unless notify_customer: false) from the
workspace's own verified sender only — customer_email_skipped says when that email
will not go out. The answer leaves last_customer_email out: the email is sent just
after it. Requires the customer_tickets plan feature.
| contact_id | string <uuid> An existing contact. Mutually exclusive with |
object Identity to upsert — resolved by phone, then email, then national ID, exactly like
| |
| subject required | string <= 200 characters |
| body required | string <= 10000 characters The customer's request — the ticket's opening message. |
| category | string <= 80 characters One of the workspace's ticket categories (case-insensitive). |
| priority | string Default: "normal" Enum: "low" "normal" "high" "urgent" |
| language | string Enum: "en" "he" Default — the contact's language, else the workspace's. |
| opened_by | string Default: "customer" Enum: "customer" "team"
|
| assigned_user_id | string <uuid> Assign to this team member (who must be able to manage tickets). Skips routing. |
| assignee_email | string <email> <= 320 characters Assign by login email. Mutually exclusive with |
| notify_customer | boolean Default: true
|
| external_reference | string <= 200 characters Your own key, unique per workspace. Sending it again answers the original ticket
with |
{- "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
- "contact": {
- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "national_id": "string"
}, - "subject": "string",
- "body": "string",
- "category": "string",
- "priority": "normal",
- "language": "en",
- "opened_by": "customer",
- "assigned_user_id": "fb44a81f-f77a-4323-8a34-49b1af008bf9",
- "assignee_email": "user@example.com",
- "notify_customer": true,
- "external_reference": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "messages": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "side": "customer",
- "origin": "customer",
- "author": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "body": "string",
- "media_url": "string",
- "media_filename": "string",
- "media_mime_type": "string",
- "media_size": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "messages_has_more": true,
- "messages_next_before": "621a0863-7ecf-4331-9b00-dd8e69408552",
- "duplicate": true,
- "customer_email_skipped": "no_verified_sender"
}The ticket, its last_customer_email, and one page of its conversation — the
customer's and the team's messages, never internal notes or system lines. The first
page holds the newest messages; each page is ordered oldest to newest. While
messages_has_more is true, pass messages_next_before as messages_before to get
the page before it. Attachment links (media_url) are signed for 4 hours.
Requires the customer_tickets plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| messages_limit | integer >= 1 Default: 100 Messages per page, default 100, max 200 (larger values are clamped). |
| messages_before | string <uuid> An older page — the previous answer's |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "messages": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "side": "customer",
- "origin": "customer",
- "author": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "body": "string",
- "media_url": "string",
- "media_filename": "string",
- "media_mime_type": "string",
- "media_size": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "messages_has_more": true,
- "messages_next_before": "621a0863-7ecf-4331-9b00-dd8e69408552",
- "last_customer_email": {
- "kind": "reply",
- "status": "queued",
- "reason": "no_verified_sender",
- "created_at": "2019-08-24T14:15:22Z",
- "sent_at": "2019-08-24T14:15:22Z",
- "bounced_at": "2019-08-24T14:15:22Z",
- "complained_at": "2019-08-24T14:15:22Z"
}
}Status (open | resolved | closed — pending comes only from a reply), priority,
category (null clears) and assignee (assigned_user_id, null unassigns, or
assignee_email). Resolving emails the customer when the workspace sends resolved
emails, from its own verified sender only — customer_email_skipped says when that
email will not go out. Requires the customer_tickets plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| status | string Enum: "open" "resolved" "closed" |
| priority | string (TicketPriority) Enum: "low" "normal" "high" "urgent" |
| category | string or null <= 80 characters A ticket category, or |
| assigned_user_id | string or null <uuid> Assign to this team member, or |
| assignee_email | string <email> <= 320 characters Assign by login email. Mutually exclusive with |
{- "status": "open",
- "priority": "low",
- "category": "string",
- "assigned_user_id": "fb44a81f-f77a-4323-8a34-49b1af008bf9",
- "assignee_email": "user@example.com"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "customer_email_skipped": "no_verified_sender"
}A real team reply: the ticket moves to Answered (pending), its response target is
met, first_response_at is stamped on the first answer, and the customer is emailed
once per stretch of unread replies — from the workspace's own verified sender only
(customer_email_skipped says when that email was due and will not go out). Posted
as the team, or as a team member named by author_user_id / author_email. The
message's origin is api. Text only.
Idempotency: the same idempotency_key (scoped to your API key) with the same
body on the same ticket posts nothing and answers the original reply with
duplicate: true and status 200; the same key with a different body or on another
ticket answers 409 idempotency_key_mismatch. Requires the customer_tickets
plan feature.
| id required | string <uuid> Record UUID. A non-UUID value returns 400. |
| body required | string <= 10000 characters |
| author_user_id | string <uuid> Post as this team member (who must be able to manage tickets). Default — the team. |
| author_email | string <email> <= 320 characters Post as the member with this login email. Mutually exclusive with |
| idempotency_key | string <= 200 characters Makes retries safe — the same key answers the original reply. Scoped to your API key. |
{- "body": "string",
- "author_user_id": "13209de9-6c50-42f7-a1fd-5bf87d7b9225",
- "author_email": "user@example.com",
- "idempotency_key": "string"
}{- "message": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "side": "customer",
- "origin": "customer",
- "author": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}, - "body": "string",
- "media_url": "string",
- "media_filename": "string",
- "media_mime_type": "string",
- "media_size": 0,
- "created_at": "2019-08-24T14:15:22Z"
}, - "ticket": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "number": 0,
- "number_label": "string",
- "subject": "string",
- "status": "open",
- "priority": "low",
- "category": "string",
- "source": "portal",
- "language": "string",
- "widget_id": "917f3fdb-2905-481f-8ff6-04c0fb7d466c",
- "external_reference": "string",
- "contact": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "assignee": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string"
}, - "first_response_at": "2019-08-24T14:15:22Z",
- "response_target": {
- "status": "ok",
- "metric": "first_reply",
- "due_at": "2019-08-24T14:15:22Z"
}, - "last_message_at": "2019-08-24T14:15:22Z",
- "resolved_at": "2019-08-24T14:15:22Z",
- "closed_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "duplicate": true,
- "customer_email_skipped": "no_verified_sender"
}