oToK REST API (1.10.0)

Download OpenAPI specification:

URL: https://app.otok.io License: MIT

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.

Authentication

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.

Error shapes

The API uses two error body shapes:

  1. Domain errors (email, email-campaign, newsletter, webhook-endpoint, consent, suppression, product, and ticket APIs, and POST /v1/campaigns/{id}/execute failures) use a structured envelope: {"error": {"code", "message"}} — key on error.code.
  2. Framework errors (validation, auth, rate limiting, most resource errors) use {"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).

Deletion

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.

Rate limits

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.

Pagination

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).

Idempotency

  • Contacts upsert by phone/email.
  • Deals, payments, and orders upsert by external_reference; products upsert by external_id.
  • Sales are idempotent by 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.
  • Email campaigns and newsletter issues upsert by external_reference — a replay updates content/fields while the record is still editable and never touches status, scheduled_at, or an issue's number.
  • Suppression adds are idempotent per address (re-adding answers duplicate: true).
  • Order refunds are idempotent per external_refund_id — only when you supply one.
  • Emails require an explicit idempotency_key.
  • Booking creation is idempotent per slot + meeting type + contact (server-derived, no key).
  • Tickets open idempotently by external_reference (a replay changes nothing and answers the original ticket); ticket replies are idempotent per idempotency_key. Both answer 201 fresh / 200 replay.
  • Payment requests have NO idempotency key — POST /v1/payment-requests is not idempotent: a repeat POST mints a second, independently payable link. Cancel extras with POST /v1/payment-requests/{id}/cancel.

Idempotent create responses carry a top-level boolean duplicate field — false on a fresh create, true when the call matched an existing record — with one exception: POST /v1/orders returns the same full order body for both outcomes and no duplicate marker (compare created_at, or pre-check with GET /v1/orders?external_reference=...); the order refund endpoint does return { "duplicate", "order" }. Status codes are 201 for both outcomes everywhere except POST /v1/emails, POST /v1/tickets, and POST /v1/tickets/{id}/replies, which answer 201 fresh / 200 replay.

Changes in 1.10.0

  • New Sales resource group (/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.
  • New Tickets resource group (/v1/tickets*): open, read, answer and triage customer tickets. Gated by the customer_tickets plan feature.
  • Saved cards: GET /v1/contacts/{id}/payment-methods and POST /v1/contacts/{id}/charges (requires an API key with the charges capability; idempotency_key required).
  • Payment requests: 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).
  • Orders and payments take an optional sale_id.
  • Events: collect_payment_on_registration, and registrations carry a payment block. Bookings carry a deposit block.
  • Products and product cycles: dynamic_pricing, recurring_sale_policy and rearm_date_triggers.
  • New opt-in webhook events: payment.refunded, sale.recorded, sale.paid, sale.refunded and ticket.*.
  • GET /v1/sender-profiles no longer needs a plan feature.

Changes in 1.9.0

  • Events: 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.

Changes in 1.8.0

  • New Events resource group (/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.
  • New GET /v1/contacts/{id}/acquisitions — the per-submission record behind the in-app Journey, newest first. Gated by the attribution plan feature.
  • Contacts: 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.
  • Schema correction for existing consumers: the 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.

Changes in 1.6.0

  • New Email campaigns resource group (/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.
  • New Newsletters resource group (/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.
  • Shared 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.
  • Two new read-only targeting-selector discovery endpoints: 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.

Changes in 1.5.0

  • New 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.

Changes in 1.4.0

  • New consent endpoints on contacts: 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).
  • New Suppressions resource group (/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.
  • New Products resource group (/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).
  • Fifteen new webhook event types across six opt-in families, riding the existing endpoint registry, signing, and retry policy: contact lifecycle + consent (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.

Changes in 1.3.0

  • New Payment requests resource group (/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).
  • New 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.
  • Payments: 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.

Changes in 1.2.0

  • New Orders resource group (/v1/orders*) and the five order.* webhook event types.
  • Schema correction for existing consumers: deal and payment response money — 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.

Product cycles

Product cohorts, runs and versions.

Get a product cycle

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "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
}

Update or archive a product cycle

Partial update; archive with is_archived: true. No DELETE endpoint. Invalid date ranges or status overrides return 400.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>
Request Body schema: application/json
required
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 dynamic_pricing: false, a price different from the product's is refused with 400 PRODUCT_PRICE_LOCKED.

capacity
integer or null [ 1 .. 1000000 ]

Seat cap in occupied seats (units_taken); null means uncapped.

is_archived
boolean
Default: false
dynamic_pricing
boolean
Default: true

Allow sales in this cycle to override the cycle's price. false fixes the price.

rearm_date_triggers
boolean
Default: false

When this call CHANGES starts_on or ends_on and date reminders already fired for the cycle, true fires them again for the new date (only the reminders anchored on the changed date). The response then carries rearmed_fires.

Responses

Request samples

Content type
application/json
{
  • "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
}

Response samples

Content type
application/json
{
  • "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
}

List a product's cycles

Includes archived cycles unless is_archived is specified. Ordered by start date (undated last), then creation time. Includes sales_count and units_taken.

Authorizations:
bearerAuth
path Parameters
productId
required
string <uuid>
query Parameters
is_archived
boolean
limit
integer [ 1 .. 500 ]
Default: 50

Page size; larger values are clamped.

offset
integer >= 0
Default: 0

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create or update a cycle by name

Case-insensitive name match within this product updates only supplied fields and returns duplicate: true. A new cycle returns duplicate: false. Both return 201.

Authorizations:
bearerAuth
path Parameters
productId
required
string <uuid>
Request Body schema: application/json
required
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 dynamic_pricing: false, a price different from the product's is refused with 400 PRODUCT_PRICE_LOCKED.

capacity
integer or null [ 1 .. 1000000 ]

Seat cap in occupied seats (units_taken); null means uncapped.

is_archived
boolean
Default: false
dynamic_pricing
boolean
Default: true

Allow sales in this cycle to override the cycle's price. false fixes the price.

rearm_date_triggers
boolean
Default: false

When this call CHANGES starts_on or ends_on and date reminders already fired for the cycle, true fires them again for the new date (only the reminders anchored on the changed date). The response then carries rearmed_fires.

Responses

Request samples

Content type
application/json
{
  • "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
}

Response samples

Content type
application/json
{
  • "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
}

Reports

Discover and run shared saved reports.

Run a shared saved report

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>
Request Body schema: application/json
optional
object
Array of objects <= 3 items

Responses

Request samples

Content type
application/json
{
  • "page": {
    },
  • "sort": [
    ]
}

Response samples

Content type
application/json
{
  • "shape": "table",
  • "columns": [
    ],
  • "rows": [
    ],
  • "page": {
    },
  • "meta": {
    }
}

List shared saved reports

Only shared, unarchived reports in the workspace. Returns metadata, never the report definition. Most recently updated first. Requires API access.

Authorizations:
bearerAuth
query Parameters
limit
integer [ 1 .. 100 ]
Default: 25

Page size; larger values are clamped.

offset
integer >= 0
Default: 0

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Events

Events and their registrations.

List events

Newest start date first. Requires the events plan feature.

Authorizations:
bearerAuth
query Parameters
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).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "limit": 0,
  • "offset": 0
}

Create an event (idempotent via external_id)

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
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 scheduled — an event created over the API is a real one, not an editor draft.

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_not_found.

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 event_type_not_found — nothing is created. With event_type_id too, both must name the same saved event (400 event_type_mismatch).

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 POST /v1/events/{id}/attendances — records a sale of the event's product (at the cycle's price when the cycle has one) and sends the contact a payment link. Applies only to a priced product that does not already grant attendance on sale, on a workspace with Sales and a connected payment provider; otherwise registrations carry no sale. Zoom-synced and imported registrations never charge.

Responses

Request samples

Content type
application/json
{
  • "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
}

Response samples

Content type
application/json
{
  • "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": {
    },
  • "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
}

Get an event

Requires the events plan feature.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": {
    },
  • "suppress_event_automations": true,
  • "collect_payment_on_registration": true,
  • "archived_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z"
}

List an event's registrations

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

query Parameters
status
string
Enum: "registered" "attended" "no_show" "waitlist" "unregistered" "cancelled"

One of the stored statuses (cancelled is accepted as an alias for unregistered). Any other value returns 400 invalid_status.

limit
integer

Page size (default 50, max 500).

offset
integer

Rows to skip (default 0).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "limit": 0,
  • "offset": 0
}

Register a contact for an event

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
contact_id
string <uuid>

An existing contact. Mutually exclusive with contact.

object

Identity to upsert when you have no contact_id — resolved by phone, then email, then national ID, exactly like POST /v1/contacts. At least one identifier is required. Values must not contain NUL characters (400).

status
string
Default: "registered"
Enum: "registered" "attended" "no_show" "waitlist" "unregistered" "cancelled"
zoom_registration
string
Default: "auto"
Enum: "auto" "skip"

auto registers the attendee with Zoom like every in-app registration does. Use skip only when you registered them with Zoom yourself.

join_url
string <= 2000 characters

Honoured with zoom_registration set to skip; a blank value never clears an existing link.

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.

Responses

Request samples

Content type
application/json
{
  • "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
  • "contact": {
    },
  • "status": "registered",
  • "zoom_registration": "auto",
  • "join_url": "string",
  • "acquisition": {
    }
}

Response samples

Content type
application/json
{
  • "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": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "created": true,
  • "previous_status": "string",
  • "zoom": {
    }
}

Attendances

Event registrations, moved by id.

Move a registration to another status

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
status
required
string
Enum: "registered" "attended" "no_show" "waitlist" "unregistered" "cancelled"

Responses

Request samples

Content type
application/json
{
  • "status": "registered"
}

Response samples

Content type
application/json
{
  • "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": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "created": true,
  • "previous_status": "string",
  • "zoom": {
    }
}

Acquisitions

A contact's per-submission acquisition record.

List a contact's acquisition touches

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

query Parameters
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).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Contacts

Contact upsert & update, plus the merge-conflict contract. There is no DELETE endpoint for contacts.

List 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.

Authorizations:
bearerAuth
query Parameters
filter
string
Default: "{}"
Example: filter={"lifecycle_stage":"lead"}

String-encoded JSON object. Invalid JSON → 400 "Invalid filter: must be valid JSON"; a non-object → 400 "Invalid filter: must be a JSON object". Any workspace_id in the filter is ignored and replaced with your authenticated workspace.

Grammar: exact match ({"lifecycle_stage": "lead"}; array values match any member), $where (nested condition tree, max 100 nodes, max 200 items per array value), and $gt (the only top-level comparison shortcut). Contacts additionally support $jsonb_contains, $event_attendance, and virtual consent fields. Note: the full set of $where operators is not exhaustively enumerated in the public docs — see the Getting Started guide for the documented examples.

Filter values are type-checked against the target field before the query runs — a mistyped value returns 400 Invalid filter value for "<field>": … (per field kind: not a date / not a UUID / must be one of / not a number / not a boolean). Date-only strings ("2026-01-01"), numeric strings ("125"), and "true"/"false" strings are accepted and coerced. Substring operators (contains, not_contains, starts_with, ends_with) in $where trees are rejected on non-text fields (400 Invalid filter on "<field>": substring operators (contains, starts_with, ends_with) only apply to text fields).

sort
string
Default: "-created_at"

Field name; - prefix for descending.

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).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Upsert a contact by phone, email or national ID

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
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
email
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-none value without a blacklist match is recorded with an import block source.

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 null to remove it.

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.

Responses

Request samples

Content type
application/json
{
  • "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_url": "https://example.com/signup",
  • "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": [
    ],
  • "groups": [
    ],
  • "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": {
    },
  • "inquiry": "create"
}

Response samples

Content type
application/json
{
  • "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_url": "https://example.com/signup",
  • "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": [
    ],
  • "groups": [
    ],
  • "event_attendances": [
    ],
  • "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": {
    },
  • "first_touch": {
    },
  • "last_touch": {
    },
  • "duplicate": true
}

Get one contact

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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_url": "https://example.com/signup",
  • "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": [
    ],
  • "groups": [
    ],
  • "event_attendances": [
    ],
  • "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": {
    },
  • "first_touch": {
    },
  • "last_touch": {
    }
}

Update a contact by id

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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
email
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-none value without a blacklist match is recorded with an import block source.

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 null to remove it.

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.

Responses

Request samples

Content type
application/json
{
  • "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_url": "https://example.com/signup",
  • "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": [
    ],
  • "groups": [
    ],
  • "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"
}

Response samples

Content type
application/json
{
  • "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_url": "https://example.com/signup",
  • "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": [
    ],
  • "groups": [
    ],
  • "event_attendances": [
    ],
  • "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": {
    },
  • "first_touch": {
    },
  • "last_touch": {
    }
}

Get a contact's per-channel consent

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
  • "block_state": "none",
  • "channels": {
    }
}

Set a contact's consent on one channel

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).

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

channel
required
string
Enum: "whatsapp" "email"

whatsapp or email. Any other value returns 400 invalid_channel.

Request Body schema: application/json
required
state
required
string
Enum: "subscribed" "unsubscribed"

The consent decision to record. "unknown" is a system state and cannot be set.

basis
string
Enum: "express_opt_in" "double_opt_in" "soft_opt_in" "implied" "imported"

Legal basis of the decision. Defaults to express_opt_in when subscribing.

source
string <= 100 characters

Where the decision came from in your system; recorded as api:<source> in the consent evidence trail (plain api when omitted).

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).

Responses

Request samples

Content type
application/json
{
  • "state": "subscribed",
  • "basis": "express_opt_in",
  • "source": "string",
  • "expires_at": "2019-08-24T14:15:22Z",
  • "ip": "string",
  • "user_agent": "string"
}

Response samples

Content type
application/json
{
  • "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"
}

Contact notes

Plain-text notes on a contact. API note payloads are text only.

List a contact's notes

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Add a note

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
body
required
string <= 5000 characters

Trimmed — empty after trim → 400 "Note body cannot be empty".

pinned
boolean

true pins the note immediately.

Responses

Request samples

Content type
application/json
{
  • "body": "string",
  • "pinned": true
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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"
}

Edit / pin a note

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).

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
body
string <= 5000 characters

Empty → 400.

pinned
boolean

Pin/unpin.

Responses

Request samples

Content type
application/json
{
  • "body": "string",
  • "pinned": true
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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"
}

Delete a note

Deleting the contact's profile note also clears the contact's notes field. A deletion breadcrumb is kept in the contact's activity timeline.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "success": true
}

Tags

Tag CRUD (no delete). Names are unique per workspace (case-insensitive).

List tags

Standard list conventions; search matches the name field.

Authorizations:
bearerAuth
query Parameters
filter
string
Default: "{}"
Example: filter={"lifecycle_stage":"lead"}

String-encoded JSON object. Invalid JSON → 400 "Invalid filter: must be valid JSON"; a non-object → 400 "Invalid filter: must be a JSON object". Any workspace_id in the filter is ignored and replaced with your authenticated workspace.

Grammar: exact match ({"lifecycle_stage": "lead"}; array values match any member), $where (nested condition tree, max 100 nodes, max 200 items per array value), and $gt (the only top-level comparison shortcut). Contacts additionally support $jsonb_contains, $event_attendance, and virtual consent fields. Note: the full set of $where operators is not exhaustively enumerated in the public docs — see the Getting Started guide for the documented examples.

Filter values are type-checked against the target field before the query runs — a mistyped value returns 400 Invalid filter value for "<field>": … (per field kind: not a date / not a UUID / must be one of / not a number / not a boolean). Date-only strings ("2026-01-01"), numeric strings ("125"), and "true"/"false" strings are accepted and coerced. Substring operators (contains, not_contains, starts_with, ends_with) in $where trees are rejected on non-text fields (400 Invalid filter on "<field>": substring operators (contains, starts_with, ends_with) only apply to text fields).

sort
string
Default: "-created_at"

Field name; - prefix for descending.

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).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a tag

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
name
string [ 1 .. 100 ] characters
color
string <= 20 characters
type
string
Default: "both"
Enum: "contact" "conversation" "both"

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "color": "string",
  • "type": "contact"
}

Response samples

Content type
application/json
{
  • "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"
}

Get one tag

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

Update a tag

Same fields as create, all optional.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
name
string [ 1 .. 100 ] characters
color
string <= 20 characters
type
string
Default: "both"
Enum: "contact" "conversation" "both"

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "color": "string",
  • "type": "contact"
}

Response samples

Content type
application/json
{
  • "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"
}

Contact groups

Contact group CRUD (no delete). Names are unique per workspace (case-insensitive).

List groups

Standard list conventions; search matches the name field.

Authorizations:
bearerAuth
query Parameters
filter
string
Default: "{}"
Example: filter={"lifecycle_stage":"lead"}

String-encoded JSON object. Invalid JSON → 400 "Invalid filter: must be valid JSON"; a non-object → 400 "Invalid filter: must be a JSON object". Any workspace_id in the filter is ignored and replaced with your authenticated workspace.

Grammar: exact match ({"lifecycle_stage": "lead"}; array values match any member), $where (nested condition tree, max 100 nodes, max 200 items per array value), and $gt (the only top-level comparison shortcut). Contacts additionally support $jsonb_contains, $event_attendance, and virtual consent fields. Note: the full set of $where operators is not exhaustively enumerated in the public docs — see the Getting Started guide for the documented examples.

Filter values are type-checked against the target field before the query runs — a mistyped value returns 400 Invalid filter value for "<field>": … (per field kind: not a date / not a UUID / must be one of / not a number / not a boolean). Date-only strings ("2026-01-01"), numeric strings ("125"), and "true"/"false" strings are accepted and coerced. Substring operators (contains, not_contains, starts_with, ends_with) in $where trees are rejected on non-text fields (400 Invalid filter on "<field>": substring operators (contains, starts_with, ends_with) only apply to text fields).

sort
string
Default: "-created_at"

Field name; - prefix for descending.

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).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a group

Group names are unique per workspace (case-insensitive). A name that already exists returns 409 Conflict.

Authorizations:
bearerAuth
Request Body schema: application/json
required
name
string [ 1 .. 100 ] characters
description
string <= 500 characters
color
string <= 20 characters

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "description": "string",
  • "color": "string"
}

Response samples

Content type
application/json
{
  • "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"
}

Get one group

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

Update a group

Same fields as create, all optional.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
name
string [ 1 .. 100 ] characters
description
string <= 500 characters
color
string <= 20 characters

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "description": "string",
  • "color": "string"
}

Response samples

Content type
application/json
{
  • "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"
}

Campaigns

WhatsApp campaigns — create, schedule, execute.

List campaigns

Standard list conventions. List rows omit variables and audience_filters — fetch a single campaign to read them.

Authorizations:
bearerAuth
query Parameters
filter
string
Default: "{}"
Example: filter={"lifecycle_stage":"lead"}

String-encoded JSON object. Invalid JSON → 400 "Invalid filter: must be valid JSON"; a non-object → 400 "Invalid filter: must be a JSON object". Any workspace_id in the filter is ignored and replaced with your authenticated workspace.

Grammar: exact match ({"lifecycle_stage": "lead"}; array values match any member), $where (nested condition tree, max 100 nodes, max 200 items per array value), and $gt (the only top-level comparison shortcut). Contacts additionally support $jsonb_contains, $event_attendance, and virtual consent fields. Note: the full set of $where operators is not exhaustively enumerated in the public docs — see the Getting Started guide for the documented examples.

Filter values are type-checked against the target field before the query runs — a mistyped value returns 400 Invalid filter value for "<field>": … (per field kind: not a date / not a UUID / must be one of / not a number / not a boolean). Date-only strings ("2026-01-01"), numeric strings ("125"), and "true"/"false" strings are accepted and coerced. Substring operators (contains, not_contains, starts_with, ends_with) in $where trees are rejected on non-text fields (400 Invalid filter on "<field>": substring operators (contains, starts_with, ends_with) only apply to text fields).

sort
string
Default: "-created_at"

Field name; - prefix for descending.

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).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a campaign

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
name
string [ 1 .. 200 ] characters
description
string <= 2000 characters
status
string
Enum: "draft" "scheduled"

draft, scheduled only — execution states cannot be set via the API. Omitted on create → draft.

type
string
Enum: "broadcast" "drip" "triggered"

Omitted on create → broadcast.

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. Must belong to your workspace (404 otherwise).

audience_filters
object

A $where condition tree (same grammar as the contacts filter $where); validated structurally and test-compiled at write time — failures return 400 with an errors array.

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 Invalid date value for "scheduled_at": "<value>".

timezone
string <= 64 characters

Omitted → UTC.

instance_id
string <uuid>

WhatsApp instance (phone number connection) to send from.

variables
object

Template variable mappings.

Responses

Request samples

Content type
application/json
{
  • "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": { }
}

Response samples

Content type
application/json
{
  • "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"
}

Get one campaign

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

Update a campaign

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
name
string [ 1 .. 200 ] characters
description
string <= 2000 characters
status
string
Enum: "draft" "scheduled"

draft, scheduled only — execution states cannot be set via the API. Omitted on create → draft.

type
string
Enum: "broadcast" "drip" "triggered"

Omitted on create → broadcast.

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. Must belong to your workspace (404 otherwise).

audience_filters
object

A $where condition tree (same grammar as the contacts filter $where); validated structurally and test-compiled at write time — failures return 400 with an errors array.

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 Invalid date value for "scheduled_at": "<value>".

timezone
string <= 64 characters

Omitted → UTC.

instance_id
string <uuid>

WhatsApp instance (phone number connection) to send from.

variables
object

Template variable mappings.

Responses

Request samples

Content type
application/json
{
  • "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": { }
}

Response samples

Content type
application/json
{
  • "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"
}

Queue a scheduled campaign for immediate execution

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}.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "success": true,
  • "message": "Campaign queued for execution",
  • "jobId": "execute-1f2e3d4c-..."
}

Templates

WhatsApp message templates — read and send only; template management is in-app.

List templates

Standard list conventions. Only templates with an approved status can be delivered by WhatsApp.

Authorizations:
bearerAuth
query Parameters
filter
string
Default: "{}"
Example: filter={"lifecycle_stage":"lead"}

String-encoded JSON object. Invalid JSON → 400 "Invalid filter: must be valid JSON"; a non-object → 400 "Invalid filter: must be a JSON object". Any workspace_id in the filter is ignored and replaced with your authenticated workspace.

Grammar: exact match ({"lifecycle_stage": "lead"}; array values match any member), $where (nested condition tree, max 100 nodes, max 200 items per array value), and $gt (the only top-level comparison shortcut). Contacts additionally support $jsonb_contains, $event_attendance, and virtual consent fields. Note: the full set of $where operators is not exhaustively enumerated in the public docs — see the Getting Started guide for the documented examples.

Filter values are type-checked against the target field before the query runs — a mistyped value returns 400 Invalid filter value for "<field>": … (per field kind: not a date / not a UUID / must be one of / not a number / not a boolean). Date-only strings ("2026-01-01"), numeric strings ("125"), and "true"/"false" strings are accepted and coerced. Substring operators (contains, not_contains, starts_with, ends_with) in $where trees are rejected on non-text fields (400 Invalid filter on "<field>": substring operators (contains, starts_with, ends_with) only apply to text fields).

sort
string
Default: "-created_at"

Field name; - prefix for descending.

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).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Get one template

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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"
}

Send a template message

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
to
required
string

Recipient phone, international format (e.g. +972501234567).

Array of objects

Values for the template's body placeholders. param_name matches named placeholders; otherwise values apply by position. Entries are strictly validated on the request — type and text are required, and unknown properties are rejected.

object

{ "type": "text", "variables": ["..."] } for text headers, or { "type": "media", "media_type": "image"|"video"|"document", "media_link": "https://..." } for media headers. Strictly validated on the request — unknown properties are rejected.

Array of objects

Values for dynamic buttons (e.g. dynamic-URL suffixes). The set of valid type values is not enumerated in the public docs. Entries are strictly validated on the request — parameters is required; send [] for a button with no dynamic values.

Responses

Request samples

Content type
application/json
{
  • "to": "string",
  • "body_variables": [
    ],
  • "header_config": {
    },
  • "button_configs": [
    ]
}

Response samples

Content type
application/json
{
  • "success": true,
  • "wamid": "string",
  • "message_id": "d7d9d9fd-478f-40e6-b651-49b7f19878a2",
  • "conversation_id": "cc71b11a-25cd-4c2d-9950-df2cc38e3407"
}

Pipelines

Read pipelines and their stages.

List pipelines with their stages

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.

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Deals

Deal CRUD-minus-delete, stage moves, win/lose, idempotent upsert via external_reference.

List deals

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.

Authorizations:
bearerAuth
query Parameters
pipeline_id
string <uuid>

Exact match — take the id from GET /v1/pipelines. Malformed → 400 "Invalid pipeline_id: must be a UUID"; empty is treated as absent.

stage_id
string <uuid>

Exact match. Malformed → 400 "Invalid stage_id: must be a UUID".

status
string
Enum: "open" "won" "lost"

Any other value is silently ignored (unfiltered result).

contact_id
string <uuid>

Exact match. Malformed → 400 "Invalid contact_id: must be a UUID".

owner_user_id
string <uuid>

Exact match. Malformed → 400 "Invalid owner_user_id: must be a UUID".

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 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a deal (idempotent upsert via `external_reference`)

Creates a deal — or, when external_reference matches an existing deal in the workspace, updates it:

  • Fields updated (only those present in the body): 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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
contact_id
string <uuid>

Existing contact (404 if not in this workspace). Wins over phone/email.

phone
string <= 32 characters

International format preferred.

email
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 external_reference match.

stage_id
string <uuid>

Omitted → the pipeline's first stage. On an external_reference match, a different stage moves the deal.

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.

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "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
}

Get one deal

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

Update deal fields

Field updates do not write stage-history entries and do not fire deal automations — only the /stage and /status routes do.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
product_id
string or null <uuid>

UUID attaches/replaces (must be active unless it's the already-attached product); null detaches — the title keeps its current text.

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>

null unassigns; a UUID must be a workspace agent (400 INVALID_DEAL_OWNER).

expected_close_at
string or null <date-time>

null clears.

note
string or null <= 4000 characters

Trimmed; empty becomes null.

cycle_id
string or null <uuid>

Cycle belonging to the attached product; mismatches return 400 CYCLE_PRODUCT_MISMATCH. PATCH null clears the cycle.

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "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 a stage

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
stage_id
required
string <uuid>

Target stage.

index
integer [ 0 .. 100000 ]

Position within the stage column, 0 = top. Omitted → top.

Responses

Request samples

Content type
application/json
{
  • "stage_id": "f38d2b9c-0a0a-4eea-91a8-e974413bb670",
  • "index": 0
}

Response samples

Content type
application/json
{
  • "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"
}

Set status: open / won / lost

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).

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
status
required
string
Enum: "open" "won" "lost"

open reopens a closed deal.

lost_reason
string <= 1000 characters

Stored only when status is lost; cleared otherwise.

Responses

Request samples

Content type
application/json
{
  • "status": "open",
  • "lost_reason": "string"
}

Response samples

Content type
application/json
{
  • "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"
}

Products

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.

List products

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.

Authorizations:
bearerAuth
query Parameters
q
string

Substring match on name or description.

sku
string

Exact-match lookup by SKU.

external_id
string

Exact-match lookup by external id.

is_active
boolean

true | false; any other value returns 400.

limit
integer [ 0 .. 500 ]
Default: 50

Hard cap 500. Non-integer or negative → 400.

offset
integer >= 0
Default: 0

Non-integer or negative → 400.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a product (idempotent upsert via external_id)

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).

Authorizations:
bearerAuth
Request Body schema: application/json
required
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. null = no default price (deals fall back to an explicit amount).

dynamic_pricing
boolean
Default: true

false fixes the price: a deal without an amount defaults to price even when its cycle has its own price, a different cycle price is refused with 400 PRODUCT_PRICE_LOCKED, and a sale at a different unit amount with 400 SALE_PRICE_LOCKED.

vat_mode
string or null
Enum: "inclusive" "exclusive" null

Per-product VAT override. Travels with vat_rate as one both-or-neither pair; send both null to clear the override.

vat_rate
number or null [ 0 .. 100 ]

VAT percent (0–100, max 2 decimals); paired with vat_mode.

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: fill_one (default) keeps paying into one sale until it is fully paid; per_period opens a new sale for every billing period. A plan keeps the policy it started with — a change applies to new plans only.

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "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
}

Get a product

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

Update a product

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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. null = no default price (deals fall back to an explicit amount).

dynamic_pricing
boolean
Default: true

false fixes the price: a deal without an amount defaults to price even when its cycle has its own price, a different cycle price is refused with 400 PRODUCT_PRICE_LOCKED, and a sale at a different unit amount with 400 SALE_PRICE_LOCKED.

vat_mode
string or null
Enum: "inclusive" "exclusive" null

Per-product VAT override. Travels with vat_rate as one both-or-neither pair; send both null to clear the override.

vat_rate
number or null [ 0 .. 100 ]

VAT percent (0–100, max 2 decimals); paired with vat_mode.

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: fill_one (default) keeps paying into one sale until it is fully paid; per_period opens a new sale for every billing period. A plan keeps the policy it started with — a change applies to new plans only.

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "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"
}

Payments

One-time / recurring / installment payments, entries, refunds (through the payment gateway or recorded). Refunds need the API key's refund access (allow_money_out).

List payments

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.

Authorizations:
bearerAuth
query Parameters
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 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a payment (idempotent upsert via `external_reference`)

Creates a payment — or, when external_reference matches an existing payment, updates it. Only mutable fields are applied on a match:

  • Always (when present): product_id, title (subject to the product title lock), note, method, metadata (the provided object replaces the stored one — omit it to keep it).
  • Only when the existing payment is one_time: amount, status.
  • Only when it is 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).
  • Never restructured on a match: 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).

Authorizations:
bearerAuth
Request Body schema: application/json
required
contact_id
string <uuid>

Existing contact (404 if not in this workspace). Wins over phone/email.

phone
string <= 32 characters
email
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 → completed.

interval
string
Enum: "weekly" "monthly" "quarterly" "yearly"

Recurring only. Omitted → monthly.

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 (a full pair — on other types the pair returns 400 "vatMode/vatRate apply to recurring plans only"; a lone leg returns 400). The VAT posture the plan's cycles are charged and documented with: inclusive (VAT included in the amount) or exclusive (the amount is net; VAT is added on top; exclusive + rate 0 = VAT-exempt). Omitted → the plan stores the attached product's VAT pair, else the workspace default. On an external_reference match a provided pair re-prices the plan.

vat_rate
number [ 0 .. 100 ]

Recurring only, always together with vat_mode: VAT rate percent (0–100, at most 2 decimal places).

installment_count
integer [ 2 .. 360 ]

Required when type is installments. Over the ceiling → 400 "installmentCount must be at most 360".

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 "Sale not found", 409 SALE_ALREADY_CANCELLED / SALE_CONTACT_MISMATCH / PLAN_FEATURE_REQUIRED, 400 CURRENCY_MISMATCH.

external_reference
string <= 255 characters

Idempotency key, unique per workspace.

metadata
object

Free-form JSON stored on the payment — max 2048 bytes serialized (400 "metadata exceeds 2048 bytes serialized" over the cap). Returned on reads and surfaced to payment automations. On an external_reference match the provided object replaces the stored one (omit to keep it).

Responses

Request samples

Content type
application/json
{
  • "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": { }
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "duplicate": true,
  • "sale_allocations": [
    ]
}

Get a payment with its entries

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ]
}

Update a payment

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).

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
product_id
string or null <uuid>

Attach/replace (must be active unless already attached) or detach (null); title derives from the product while attached.

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; null clears; must be after the purchase date.

recurring_max_occurrences
integer or null >= 1

Recurring only; null clears.

vat_mode
string
Enum: "inclusive" "exclusive"

Recurring only: replace the plan's stored VAT pair — always together with vat_rate. Unlike the other type-restricted patch fields this is NOT silently ignored on other types (400 "vatMode/vatRate apply to recurring plans only"), and lone legs / nulls are rejected (400 "vatMode and vatRate must be provided together") — the stored pair is a complete value. Future cycles are charged and documented with the new pair. Omit both to leave the pair as-is.

vat_rate
number [ 0 .. 100 ]

Recurring only, always together with vat_mode: VAT rate percent (0–100, at most 2 decimal places).

metadata
object or null

Replace the payment's free-form metadata object (max 2048 bytes serialized), or null to clear it. Omit to leave it as-is.

Responses

Request samples

Content type
application/json
{
  • "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": { }
}

Response samples

Content type
application/json
{
  • "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": [
    ]
}

Cancel a recurring plan

Cancels a recurring plan. No request body. Already-recorded entries are untouched.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ]
}

Set one entry's status

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).

Authorizations:
bearerAuth
path Parameters
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.

Request Body schema: application/json
required
status
required
string
Enum: "pending" "completed" "failed" "refunded"

Responses

Request samples

Content type
application/json
{
  • "status": "pending"
}

Response samples

Content type
application/json
{
  • "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": [
    ]
}

Refund a charge (through the gateway, or recorded)

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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"

auto — refund through the connected payment gateway when the charge was collected by it (a charge recorded by hand or through this API is booked only); recorded_outside — book the refund only, the money was (or will be) returned outside oToK.

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 duplicate: true and refunds nothing; the same key for a different payment → 409 IDEMPOTENCY_KEY_MISMATCH.

Responses

Request samples

Content type
application/json
{
  • "reason": "requested_by_customer",
  • "mode": "auto",
  • "entry_id": "31513983-613b-4481-b270-ae929f358bae",
  • "amount": 1,
  • "note": "string",
  • "idempotency_key": "string"
}

Response samples

Content type
application/json
{
  • "payment": {
    },
  • "entry": {
    },
  • "outcome": "gateway_refunded",
  • "duplicate": true,
  • "incidentId": "string"
}

Payment requests

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.

List payment requests

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.

Authorizations:
bearerAuth
query Parameters
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 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a payment request (mint a pay-link)

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
contact_id
string <uuid>

Existing contact (the payer). Wins over phone/email.

phone
string <= 32 characters

Contact phone (E.164). A matching contact is used, or created.

email
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 = exclusive + rate 0.

vat_rate
number [ 0 .. 100 ]

VAT rate percent (0–100, at most 2 decimal places); see vat_mode — the pair travels together.

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 INVALID_PRODUCT / PRODUCT_INACTIVE.

sale_id
string <uuid>

An existing sale of the payer to collect for (allocated when the request is paid). 404 unknown sale; 409 PLAN_FEATURE_REQUIRED / SALE_CONTACT_MISMATCH / SALE_ALREADY_CANCELLED; 400 CURRENCY_MISMATCH.

idempotency_key
string <= 200 characters

Unique per workspace. A replay returns the original request with duplicate: true; the same key with a different payer, amount, currency, sale_id or product_id → 409 IDEMPOTENCY_KEY_MISMATCH; a concurrent request still in flight → 409 IDEMPOTENCY_CLAIM_IN_FLIGHT (retry shortly).

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "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"
}

Get a payment request

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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
}

Cancel a pending payment request

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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
}

Issue the tax document for a paid request

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).

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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
}

Refund a paid payment request

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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"

auto — refund through the connected payment gateway when the charge was collected by it (a charge recorded by hand or through this API is booked only); recorded_outside — book the refund only, the money was (or will be) returned outside oToK.

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 duplicate: true.

Responses

Request samples

Content type
application/json
{
  • "reason": "requested_by_customer",
  • "mode": "auto",
  • "amount": 0.01,
  • "note": "string",
  • "idempotency_key": "string"
}

Response samples

Content type
application/json
{
  • "payment": {
    },
  • "entry": {
    },
  • "outcome": "gateway_refunded",
  • "duplicate": true,
  • "incidentId": "string",
  • "payment_request_id": "630b1da3-a998-4401-ac3b-2ab2aec5f653"
}

Saved cards

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.

List a contact's saved cards (masked)

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "payment_methods": [
    ]
}

Charge a contact's saved card

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
idempotency_key
required
string [ 8 .. 124 ] characters

Required. Scoped to the calling API key; a replay answers the original outcome with duplicate: true and never charges twice. The same key with a different contact, amount or currency → 409 IDEMPOTENCY_KEY_MISMATCH.

method_id
string <uuid>

The saved card to charge. Omitted → chosen by method_selection.

method_selection
string
Default: "default"
Enum: "default" "most_recent"

When no method_id is given: the contact's default card, or the most recently charged one (else the newest). Expired cards are skipped.

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 PaymentRequestCreate.document_kind vocabulary).

auto_issue_document
boolean
installments
integer [ 1 .. 36 ]

Card installments to charge with.

test_mode
boolean

Authorise-only test run — 400 TOKEN_TEST_MODE_NOT_SUPPORTED when the provider has none.

deal_id
string <uuid>
vat_mode
string
Enum: "inclusive" "exclusive"

Per-charge VAT override — travels as a pair with vat_rate.

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.

Responses

Request samples

Content type
application/json
{
  • "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
}

Response samples

Content type
application/json
{
  • "outcome": "paid",
  • "duplicate": true,
  • "payment_request": {
    }
}

Contact documents

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.

List a contact's financial documents

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

query Parameters
live
string
Enum: "true" "false"

true | false (default false). Any other value returns 400 ("Invalid live: must be true or false").

Responses

Response samples

Content type
application/json
{
  • "documents": [
    ],
  • "live": {
    }
}

Orders

E-commerce-grade orders on contacts — idempotent create via external_reference, refund ledger, mark-paid, cancel. No update or delete routes.

List orders

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.

Authorizations:
bearerAuth
query Parameters
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 "contact_id must be a UUID" (note the wording differs from deals/payments); empty (?contact_id=) is treated as absent.

source
string

Exact match — manual, api, or automation; orders synced from a connected store carry the store platform name.

store_connection_id
string <uuid>

Exact match on the store-sync provenance field — orders synced from a connected store. Malformed → 400 "store_connection_id must be a UUID".

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_from is not a valid date".

placed_to
string <date-time>

Orders placed at/before this instant. Unparseable → 400 "placed_to is not a valid date".

limit
integer [ 1 .. 100 ]
Default: 25

Default 25, cap 100, floor 1. Unlike GET /v1/deals/GET /v1/payments, paging here never returns 400 — absent, empty, malformed, or zero values silently default to 25, and out-of-range values are clamped into 1–100.

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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create an order (idempotent upsert via `external_reference`)

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:

  • Always updated (when present in the body): note, coupon_codes, placed_at, deal_id.
  • Money fields (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.
  • The contact is never re-pointed on a match (unlike deals). Contact resolution still runs first, so a repeat POST must still carry 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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
contact_id
string <uuid>

Existing contact (404 if not in this workspace). Wins over phone/email.

phone
string <= 32 characters

International format preferred.

email
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 paid create records the payment and fires order-paid automations. Never applied on an external_reference match.

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 ORDER_DEAL_NOT_FOUND when unknown to the workspace, 409 ORDER_DEAL_CONTACT_MISMATCH when it belongs to another contact.

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 ORDER_SALE_CONTACT_MISMATCH / ORDER_SALE_ALREADY_LINKED / SALE_ALREADY_CANCELLED / PLAN_FEATURE_REQUIRED; 400 CURRENCY_MISMATCH.

external_reference
string <= 255 characters

Idempotency key, unique per workspace.

Responses

Request samples

Content type
application/json
{
  • "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
  • "phone": "string",
  • "email": "user@example.com",
  • "name": "string",
  • "items": [
    ],
  • "currency": "string",
  • "discount_total": 0,
  • "shipping_total": 0,
  • "tax_total": 0,
  • "financial_status": "pending",
  • "placed_at": "2019-08-24T14:15:22Z",
  • "coupon_codes": [
    ],
  • "note": "string",
  • "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3",
  • "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33",
  • "external_reference": "string"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": [
    ],
  • "refunds": [
    ]
}

Get one order with items and refunds

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "refunds": [ ]
}

Record a refund (idempotent via `external_refund_id`)

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
amount
required
number ( 0 .. 9999999999 ]

In the order's currency; must not exceed the remaining total (total − refunded_total).

external_refund_id
string <= 255 characters

Idempotency key — a repeat POST with the same value on this order applies nothing and returns duplicate: true. Strongly recommended whenever your system can retry.

reason
string <= 1000 characters
refunded_at
string <date-time>

ISO 8601; defaults to now.

Responses

Request samples

Content type
application/json
{
  • "amount": 1,
  • "external_refund_id": "string",
  • "reason": "string",
  • "refunded_at": "2019-08-24T14:15:22Z"
}

Response samples

Content type
application/json
{
  • "duplicate": false,
  • "order": {
    }
}

Mark an order paid

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).

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
payment_reference
string <= 255 characters

The external_reference of an existing payment row (e.g. one your system already recorded via POST /v1/payments) to link the order onto instead of recording a new payment. Link-only — the payment's amount is never rewritten. Validated before linking (see the 404/409 responses).

sale_id
string <uuid>

An existing sale this payment funds — no new sale is recorded. Same checks as on create (CURRENCY_MISMATCH, "Sale not found", PLAN_FEATURE_REQUIRED, SALE_ALREADY_CANCELLED, ORDER_SALE_CONTACT_MISMATCH, ORDER_SALE_ALREADY_LINKED).

Responses

Request samples

Content type
application/json
{
  • "payment_reference": "string",
  • "sale_id": "7e9bee0e-09d3-495f-848b-5926aa2ded33"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": [
    ],
  • "refunds": [
    ]
}

Cancel an order

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": [
    ],
  • "refunds": [
    ]
}

Sales

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.

List sales

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.

Authorizations:
bearerAuth
query Parameters
contact_id
string <uuid>

The buyer. Malformed → 400 "Invalid contact_id: must be a UUID".

owner_user_id
string <uuid>

Malformed → 400.

status
string (Sale status)
Enum: "active" "partially_cancelled" "cancelled"

Unknown values → 400 "Invalid sale status".

settlement_status
string (Sale settlement status)
Enum: "untracked" "unpaid" "partially_paid" "paid" "partially_refunded" "refunded"

Unknown values → 400 "Invalid settlement status".

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 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Record a sale (idempotent via external_reference)

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
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; null → 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.

vat_rate
number or null [ 0 .. 100 ]
expects_payment
boolean
Default: true

Responses

Request samples

Content type
application/json
{
  • "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
  • "items": [
    ],
  • "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
}

Response samples

Content type
application/json
{
  • "sale": {
    },
  • "items": [
    ],
  • "duplicate": true
}

Get a sale with its items, payments and documents

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "tier": 1,
  • "sale": {
    },
  • "items": [
    ],
  • "allocations": [
    ],
  • "pay_links": [
    ],
  • "related": {
    },
  • "documents": [
    ],
  • "timeline": [
    ],
  • "actions": {
    },
  • "money": {
    },
  • "incidents": [
    ]
}

Edit a sale's note

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
note
string or null <= 2000 characters

null clears it.

Responses

Request samples

Content type
application/json
{
  • "note": "string"
}

Response samples

Content type
application/json
{
  • "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"
}

Delete an unfunded, unlinked sale

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "success": true
}

Cancel a sale (or some of its items)

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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 = { "mode": "keep" }.

Responses

Request samples

Content type
application/json
{
  • "reason": "customer_request",
  • "items": [
    ],
  • "note": "string",
  • "money": {
    }
}

Response samples

Content type
application/json
{
  • "sale": {
    },
  • "cancelled": [
    ],
  • "refunds": [
    ],
  • "pay_links_cancelled": 0
}

Reinstate cancelled sale items

Reinstates every cancelled item, or the listed ones. Money is not touched; no webhook fires. Requires the sales plan feature.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
optional
items
Array of strings <uuid> <= 100 items [ items <uuid > ]

Item ids of this sale. Omitted → every cancelled item.

Responses

Request samples

Content type
application/json
{
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "sale": {
    },
  • "reinstated": [
    ]
}

Refund a charge that funds this sale

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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"

auto = refund through the connected payment provider when the charge was taken through one (needs the workspace_payments feature); recorded_outside = record only.

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 duplicate: true.

Responses

Request samples

Content type
application/json
{
  • "reason": "requested_by_customer",
  • "mode": "auto",
  • "entry_id": "31513983-613b-4481-b270-ae929f358bae",
  • "amount": 1,
  • "note": "string",
  • "idempotency_key": "string"
}

Response samples

Content type
application/json
{
  • "payment": {
    },
  • "entry": {
    },
  • "outcome": "gateway_refunded",
  • "incidentId": "c7a5d545-e8ac-4e92-ab32-6a98dfe2572c",
  • "duplicate": true
}

Assign or clear the sale's owner

Does not change the contact's owner. Requires the sales plan feature.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
owner_user_id
required
string or null <uuid>

An active team member, or null for unowned.

Responses

Request samples

Content type
application/json
{
  • "owner_user_id": "65139110-7c3c-4777-b692-80c218be3b9d"
}

Response samples

Content type
application/json
{
  • "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"
}

Link or unlink the buyer's deal

Requires the sales plan feature.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
deal_id
required
string or null <uuid>

A deal of the sale's buyer, or null to unlink.

Responses

Request samples

Content type
application/json
{
  • "deal_id": "2fa5b64f-5b72-4263-958e-07b76f41abe3"
}

Response samples

Content type
application/json
{
  • "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"
}

Allocate an existing charge to this sale

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
payment_entry_id
required
string <uuid>

A charge entry in pending or completed status, in the sale's currency.

amount
number [ 0.01 .. 1000000000 ]

Omitted → the smaller of what the charge has left and what the sale can still take.

Responses

Request samples

Content type
application/json
{
  • "payment_entry_id": "4ffcead0-00eb-4954-8c43-fab729d91a7a",
  • "amount": 0.01
}

Response samples

Content type
application/json
{
  • "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"
}

Release a payment allocation

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

allocationId
required
string <uuid>

Allocation UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "released": {
    },
  • "twins": [
    ]
}

Emails

POST /v1/emails — idempotent raw transactional sends with opt-in tracking.

Send a transactional email (idempotent)

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).

Authorizations:
bearerAuth
Request Body schema: application/json
required
to
required
string [ 1 .. 320 ] characters

Must be a syntactically valid email (else 422 invalid_recipient).

subject
required
string [ 1 .. 998 ] characters

No line breaks or control characters.

html
string

≤500 KB. Delivered verbatim. At least one of html/text is required; the missing part is derived from the other.

text
string

≤100 KB. At least one of html/text is required (text → HTML-escaped with line breaks preserved; html → a plain-text rendering).

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 List-Unsubscribe and List-Unsubscribe-Post (case-insensitive). Values: strings, ≤1000 chars, no control characters. Any other header key → 400 validation_failed.

object

≤2048 bytes JSON-serialized. Stored with the send and echoed verbatim in webhook events (data.metadata).

object

Both default false. Preferences are not persisted across replays.

Responses

Request samples

Content type
application/json
{
  • "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": {
    },
  • "metadata": { },
  • "tracking": {
    }
}

Response samples

Content type
application/json
{
  • "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"
}

Suppressions

The workspace's email suppression list — a send-time overlay deliberately separate from consent. List, idempotent add, remove. Requires the email_marketing feature.

List suppressions

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.

Authorizations:
bearerAuth
query Parameters
email
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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Suppress an email address (idempotent)

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).

Authorizations:
bearerAuth
Request Body schema: application/json
required
email
required
string <email> <= 255 characters
reason
string
Enum: "unsubscribe" "bounce" "complaint" "manual"

Why the address is suppressed. Defaults to manual.

note
string <= 500 characters

Free-form note stored with the row.

Responses

Request samples

Content type
application/json
{
  • "email": "user@example.com",
  • "reason": "unsubscribe",
  • "note": "string"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "email": "string",
  • "reason": "unsubscribe",
  • "source": "string",
  • "note": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "duplicate": true
}

Remove a suppression

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "statusCode": 0,
  • "message": "string",
  • "error": "string",
  • "error_code": "string"
}

Audiences

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.

List audiences

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.

Authorizations:
bearerAuth
query Parameters
kind
string
Enum: "dynamic" "static"

Exact kind filter. Unknown values return 400 ("Invalid kind: must be one of dynamic, static"); 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 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Sender profiles

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.

List email sender profiles

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.

Authorizations:
bearerAuth
query Parameters
limit
integer [ 1 .. 100 ]
Default: 25

Default 25, cap 100. Absent or empty values default; a malformed value returns 400 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Email campaigns

Broadcast email campaigns — author (markdown / blocks / design_json), estimate, launch, schedule. Idempotent create via external_reference. Requires the email_marketing feature.

List email campaigns

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.

Authorizations:
bearerAuth
query Parameters
status
string
Enum: "draft" "scheduled" "sending" "paused" "sent" "failed" "cancelled"

Exact status filter. Unknown values return 400 ("Invalid status: must be one of draft, scheduled, sending, paused, sent, failed, cancelled"); 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 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a draft campaign (idempotent upsert via `external_reference`)

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:

  • while the campaign is still draft/scheduled, the call updates its fields exactly as a PATCH would — never status or scheduled_at;
  • once the launch has claimed it (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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
name
required
string [ 1 .. 200 ] characters
subject
required
string [ 1 .. 400 ] characters

May embed [[…]] variable tokens.

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 sender_profile_not_found). Send-readiness — verified domain + legal footer fields — is asserted at launch, not here.

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 duplicate: true.

required
object (ContentInput)

The shared authoring contract for email-campaign bodies and newsletter issues: an optional direction plus exactly one of markdown, blocks, or design_json — zero or two-plus sources return 400 invalid_content. Whichever source you send is converted to the same internal email document the in-app editor produces and compiled immediately; the write response's compile envelope reports errors and warnings up front, not at send time.

Total content size is capped at 512,000 characters (measured on the source and again on the built document) — an oversized body returns 400 invalid_content.

Variable tokens work in all three sources: any [[…]] token in text becomes a personalization pill resolved per recipient at send time. The token grammar is [[path]], [[path : fallback]], or [[path | modifier(arg) : fallback]] — path is a contact field (first_name, email, custom field keys, or the explicit contact. prefix), the optional fallback renders when the value is empty, and optional modifiers format the value (e.g. upper, date). Example: Hi [[first_name : there]]!. Tokens also work inside the campaign subject and preheader.

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 ::snippet[…] markdown directive or a snippet block. A reference that matches no workspace snippet returns 400 unknown_snippet with the available names listed in the message. The stored document keeps the reference, but the two surfaces resolve it at different moments: email campaigns re-expand snippet references at launch, so a snippet edited after the campaign was written still reaches the send; newsletter issues bake snippet content into the compiled document at save — re-save the issue (PATCH, or an external_reference create replay) to pick up later snippet edits.

audience_id
string <uuid>

A saved audience; wins over audience_filters. Must belong to your workspace (404 otherwise).

audience_filters
object

An ad-hoc $where condition tree; validated structurally and test-compiled at write time (failures return 400 with an errors array). Ignored when audience_id is set.

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.

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "subject": "Big news, [[first_name : there]]!",
  • "preheader": "string",
  • "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
  • "external_reference": "string",
  • "content": {
    },
  • "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
  • "audience_filters": { },
  • "contact_group_ids": [
    ],
  • "topic_key": "string"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": {
    }
}

Get one campaign

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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"
}

Update a draft/scheduled campaign

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).

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
name
string [ 1 .. 200 ] characters
subject
string or null [ 1 .. 400 ] characters

null clears it; an empty string is rejected.

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 direction plus exactly one of markdown, blocks, or design_json — zero or two-plus sources return 400 invalid_content. Whichever source you send is converted to the same internal email document the in-app editor produces and compiled immediately; the write response's compile envelope reports errors and warnings up front, not at send time.

Total content size is capped at 512,000 characters (measured on the source and again on the built document) — an oversized body returns 400 invalid_content.

Variable tokens work in all three sources: any [[…]] token in text becomes a personalization pill resolved per recipient at send time. The token grammar is [[path]], [[path : fallback]], or [[path | modifier(arg) : fallback]] — path is a contact field (first_name, email, custom field keys, or the explicit contact. prefix), the optional fallback renders when the value is empty, and optional modifiers format the value (e.g. upper, date). Example: Hi [[first_name : there]]!. Tokens also work inside the campaign subject and preheader.

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 ::snippet[…] markdown directive or a snippet block. A reference that matches no workspace snippet returns 400 unknown_snippet with the available names listed in the message. The stored document keeps the reference, but the two surfaces resolve it at different moments: email campaigns re-expand snippet references at launch, so a snippet edited after the campaign was written still reaches the send; newsletter issues bake snippet content into the compiled document at save — re-save the issue (PATCH, or an external_reference create replay) to pick up later snippet edits.

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

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "subject": "string",
  • "preheader": "string",
  • "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b",
  • "content": {
    },
  • "audience_id": "875075bb-caff-4035-8f5f-9fb1430dc417",
  • "audience_filters": { },
  • "contact_group_ids": [
    ],
  • "topic_key": "string"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": {
    }
}

Estimate the campaign's audience size

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "estimated_recipients": 1284
}

Launch the campaign now

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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"
}

Schedule (or reschedule) a future launch

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
scheduled_at
required
string <date-time>

ISO 8601 UTC instant; must be in the future (otherwise 400 invalid_scheduled_at). A non-ISO-8601 value fails request validation with the standard body.

Responses

Request samples

Content type
application/json
{
  • "scheduled_at": "2026-08-01T09:00:00Z"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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"
}

Cancel a scheduled launch (back to draft)

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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"
}

Newsletters

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.

List newsletters

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.

Authorizations:
bearerAuth
query Parameters
limit
integer [ 1 .. 100 ]
Default: 25

Default 25, cap 100. Absent or empty values default; a malformed value returns 400 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a newsletter

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).

Authorizations:
bearerAuth
Request Body schema: application/json
required
name
required
string [ 1 .. 120 ] characters

Unique per workspace, case-insensitive (409 duplicate_name).

description
string <= 2000 characters
sender_profile_id
string <uuid>

Omit to fall back to the workspace default profile at send time. Unknown → 400 sender_profile_not_found.

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "description": "string",
  • "sender_profile_id": "e604e9b6-6330-49cb-af8f-2abc7526b66b"
}

Response samples

Content type
application/json
{
  • "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"
}

Get one newsletter

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

List a newsletter's issues

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").

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

query Parameters
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 "Invalid limit: must be a non-negative integer".

offset
integer >= 0
Default: 0

Default 0, min 0. Absent or empty values default; a malformed value returns 400 "Invalid offset: must be a non-negative integer".

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a draft issue (idempotent upsert via `external_reference`)

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
subject
string <= 400 characters

May embed [[…]] variable tokens.

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 (duplicate: true in the response) — never its status, scheduled_at, or issue_number.

object (ContentInput)

The shared authoring contract for email-campaign bodies and newsletter issues: an optional direction plus exactly one of markdown, blocks, or design_json — zero or two-plus sources return 400 invalid_content. Whichever source you send is converted to the same internal email document the in-app editor produces and compiled immediately; the write response's compile envelope reports errors and warnings up front, not at send time.

Total content size is capped at 512,000 characters (measured on the source and again on the built document) — an oversized body returns 400 invalid_content.

Variable tokens work in all three sources: any [[…]] token in text becomes a personalization pill resolved per recipient at send time. The token grammar is [[path]], [[path : fallback]], or [[path | modifier(arg) : fallback]] — path is a contact field (first_name, email, custom field keys, or the explicit contact. prefix), the optional fallback renders when the value is empty, and optional modifiers format the value (e.g. upper, date). Example: Hi [[first_name : there]]!. Tokens also work inside the campaign subject and preheader.

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 ::snippet[…] markdown directive or a snippet block. A reference that matches no workspace snippet returns 400 unknown_snippet with the available names listed in the message. The stored document keeps the reference, but the two surfaces resolve it at different moments: email campaigns re-expand snippet references at launch, so a snippet edited after the campaign was written still reaches the send; newsletter issues bake snippet content into the compiled document at save — re-save the issue (PATCH, or an external_reference create replay) to pick up later snippet edits.

Responses

Request samples

Content type
application/json
{
  • "subject": "string",
  • "preheader": "string",
  • "include_in_archive": true,
  • "external_reference": "string",
  • "content": {
    }
}

Response samples

Content type
application/json
{
  • "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": {
    }
}

Get one issue

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

Update an issue

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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 direction plus exactly one of markdown, blocks, or design_json — zero or two-plus sources return 400 invalid_content. Whichever source you send is converted to the same internal email document the in-app editor produces and compiled immediately; the write response's compile envelope reports errors and warnings up front, not at send time.

Total content size is capped at 512,000 characters (measured on the source and again on the built document) — an oversized body returns 400 invalid_content.

Variable tokens work in all three sources: any [[…]] token in text becomes a personalization pill resolved per recipient at send time. The token grammar is [[path]], [[path : fallback]], or [[path | modifier(arg) : fallback]] — path is a contact field (first_name, email, custom field keys, or the explicit contact. prefix), the optional fallback renders when the value is empty, and optional modifiers format the value (e.g. upper, date). Example: Hi [[first_name : there]]!. Tokens also work inside the campaign subject and preheader.

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 ::snippet[…] markdown directive or a snippet block. A reference that matches no workspace snippet returns 400 unknown_snippet with the available names listed in the message. The stored document keeps the reference, but the two surfaces resolve it at different moments: email campaigns re-expand snippet references at launch, so a snippet edited after the campaign was written still reaches the send; newsletter issues bake snippet content into the compiled document at save — re-save the issue (PATCH, or an external_reference create replay) to pick up later snippet edits.

Responses

Request samples

Content type
application/json
{
  • "subject": "string",
  • "preheader": "string",
  • "include_in_archive": true,
  • "content": {
    }
}

Response samples

Content type
application/json
{
  • "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": {
    }
}

Delete a draft/scheduled issue

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "success": true
}

Publish an issue now

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

Schedule (or reschedule) a future publish

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
scheduled_at
required
string <date-time>

ISO 8601 UTC instant; must be in the future (otherwise 400 invalid_scheduled_at). A non-ISO-8601 value fails request validation with the standard body.

Responses

Request samples

Content type
application/json
{
  • "scheduled_at": "2026-08-01T09:00:00Z"
}

Response samples

Content type
application/json
{
  • "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"
}

Cancel a scheduled publish (back to draft)

No request body. Only currently scheduled issues can be unscheduled.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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"
}

Webhook endpoints

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.

email.delivered — the provider confirmed delivery of an API send Webhook

Default-subscription delivery event.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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"

email.delivered/email.bounced/email.complained are the default subscription; email.opened/email.clicked are opt-in. email.failed is deprecated — still accepted at registration (echoed in events) but never delivered; nothing produces this event, and a failing POST /v1/emails send fails synchronously on the request itself.

Value: "email.delivered"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "email.delivered",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

email.bounced — the send bounced Webhook

Default-subscription delivery event. data.bounce_type is included when known: hard, soft, or block.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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"

email.delivered/email.bounced/email.complained are the default subscription; email.opened/email.clicked are opt-in. email.failed is deprecated — still accepted at registration (echoed in events) but never delivered; nothing produces this event, and a failing POST /v1/emails send fails synchronously on the request itself.

Value: "email.bounced"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "b2c3d4e5-f607-1829-3a4b-5c6d7e8f9012",
  • "type": "email.bounced",
  • "created_at": "2026-07-14T10:05:32.000Z",
  • "data": {
    }
}

email.complained — the recipient marked the message as spam (feedback loop) Webhook

Default-subscription delivery event.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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"

email.delivered/email.bounced/email.complained are the default subscription; email.opened/email.clicked are opt-in. email.failed is deprecated — still accepted at registration (echoed in events) but never delivered; nothing produces this event, and a failing POST /v1/emails send fails synchronously on the request itself.

Value: "email.complained"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "email.complained",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

email.failed — deprecated (never delivered) Deprecated Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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"

email.delivered/email.bounced/email.complained are the default subscription; email.opened/email.clicked are opt-in. email.failed is deprecated — still accepted at registration (echoed in events) but never delivered; nothing produces this event, and a failing POST /v1/emails send fails synchronously on the request itself.

Value: "email.failed"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "email.failed",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

email.opened — first open only (opt-in) Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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"

email.delivered/email.bounced/email.complained are the default subscription; email.opened/email.clicked are opt-in. email.failed is deprecated — still accepted at registration (echoed in events) but never delivered; nothing produces this event, and a failing POST /v1/emails send fails synchronously on the request itself.

Value: "email.opened"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "email.opened",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

email.clicked — first click only (opt-in) Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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"

email.delivered/email.bounced/email.complained are the default subscription; email.opened/email.clicked are opt-in. email.failed is deprecated — still accepted at registration (echoed in events) but never delivered; nothing produces this event, and a failing POST /v1/emails send fails synchronously on the request itself.

Value: "email.clicked"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "email.clicked",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

order.created — an order was created Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. They fire for every order write source (API, in-app, automations), never for historical import ingestion. Payload shape: OrderEventBase (plus the refund block on OrderRefundedEvent).

Value: "order.created"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "order.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

order.paid — the order entered a paid state Webhook

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).

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. They fire for every order write source (API, in-app, automations), never for historical import ingestion. Payload shape: OrderEventBase (plus the refund block on OrderRefundedEvent).

Value: "order.paid"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "order.paid",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

order.refunded — a refund was recorded on the order Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. They fire for every order write source (API, in-app, automations), never for historical import ingestion. Payload shape: OrderEventBase (plus the refund block on OrderRefundedEvent).

Value: "order.refunded"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "a1b2c3d4-e5f6-0718-2930-4a5b6c7d8e9f",
  • "type": "order.refunded",
  • "created_at": "2026-07-15T09:30:00.000Z",
  • "data": {
    }
}

order.cancelled — the order was cancelled Webhook

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).

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. They fire for every order write source (API, in-app, automations), never for historical import ingestion. Payload shape: OrderEventBase (plus the refund block on OrderRefundedEvent).

Value: "order.cancelled"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "order.cancelled",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

order.fulfilled — the order was fulfilled Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. They fire for every order write source (API, in-app, automations), never for historical import ingestion. Payload shape: OrderEventBase (plus the refund block on OrderRefundedEvent).

Value: "order.fulfilled"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "order.fulfilled",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

payment_request.created — a pay-link was minted Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. Hosted pay-links only (charge_kind: "checkout"): direct saved-card charges and internal dunning-recovery links never emit these events. Payload shape: PaymentRequestEventBase.

Value: "payment_request.created"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "payment_request.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

payment_request.paid — the payer completed the pay-link Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. Hosted pay-links only (charge_kind: "checkout"): direct saved-card charges and internal dunning-recovery links never emit these events. Payload shape: PaymentRequestEventBase.

Value: "payment_request.paid"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "c3d4e5f6-0718-2930-4a5b-6c7d8e9f0a1b",
  • "type": "payment_request.paid",
  • "created_at": "2026-07-15T11:20:00.000Z",
  • "data": {
    }
}

payment_request.expired — the pay-link passed its deadline unpaid Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. Hosted pay-links only (charge_kind: "checkout"): direct saved-card charges and internal dunning-recovery links never emit these events. Payload shape: PaymentRequestEventBase.

Value: "payment_request.expired"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "payment_request.expired",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

payment_request.cancelled — the pay-link was withdrawn Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events — an endpoint registered without an events list receives none of them. Hosted pay-links only (charge_kind: "checkout"): direct saved-card charges and internal dunning-recovery links never emit these events. Payload shape: PaymentRequestEventBase.

Value: "payment_request.cancelled"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "payment_request.cancelled",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

payment.refunded — a refund was recorded on a payment Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. Fires once per refund entry recorded on a payment, from any source (API, in-app, order refunds, connected stores, refunds made in the payment provider's dashboard). A pending reversal is silent until it settles. Payload shape: PaymentRefundedEvent.

Value: "payment.refunded"
created_at
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "payment.refunded",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

contact.created — a contact was created Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "contact.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

contact.updated — a contact's fields changed Webhook

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}.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "contact.updated",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

contact.deleted — a contact was deleted Webhook

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).

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "contact.deleted",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

contact.consent_changed — a channel's consent state changed Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "contact.consent_changed",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

message.received — an inbound WhatsApp message arrived Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "id": "d4e5f607-1829-3a4b-5c6d-7e8f90123456",
  • "type": "message.received",
  • "created_at": "2026-07-16T08:12:05.000Z",
  • "data": {
    }
}

deal.created — a deal was created Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. They fire for every deal write source — manual, API, automations, and Salesforce sync — data.source says which. Payload shape: DealEventBase.

Value: "deal.created"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "deal.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

deal.stage_changed — a deal moved to another stage Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. They fire for every deal write source — manual, API, automations, and Salesforce sync — data.source says which. Payload shape: DealEventBase.

Value: "deal.stage_changed"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "e5f60718-293a-4b5c-6d7e-8f9012345678",
  • "type": "deal.stage_changed",
  • "created_at": "2026-07-16T09:00:00.000Z",
  • "data": {
    }
}

deal.won — a deal was marked won Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. They fire for every deal write source — manual, API, automations, and Salesforce sync — data.source says which. Payload shape: DealEventBase.

Value: "deal.won"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "deal.won",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

deal.lost — a deal was marked lost Webhook

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).

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. They fire for every deal write source — manual, API, automations, and Salesforce sync — data.source says which. Payload shape: DealEventBase.

Value: "deal.lost"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "deal.lost",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

sale.recorded — a sale was recorded Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. They fire for every sale write source — API, in-app, automations, and the sales the app records from other records. sale.cancelled fires once per cancelled item; reinstating fires nothing. Payload shapes: SaleRecordedEvent, SaleCancelledEvent, SalePaidEvent, SaleRefundedEvent.

Value: "sale.recorded"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "f6071829-3a4b-4c5d-8e9f-0a1b2c3d4e5f",
  • "type": "sale.recorded",
  • "created_at": "2026-10-01T09:00:01.000Z",
  • "data": {
    }
}

sale.cancelled — a sale item was cancelled Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. They fire for every sale write source — API, in-app, automations, and the sales the app records from other records. sale.cancelled fires once per cancelled item; reinstating fires nothing. Payload shapes: SaleRecordedEvent, SaleCancelledEvent, SalePaidEvent, SaleRefundedEvent.

Value: "sale.cancelled"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "sale.cancelled",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

sale.paid — a sale became fully paid Webhook

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).

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. They fire for every sale write source — API, in-app, automations, and the sales the app records from other records. sale.cancelled fires once per cancelled item; reinstating fires nothing. Payload shapes: SaleRecordedEvent, SaleCancelledEvent, SalePaidEvent, SaleRefundedEvent.

Value: "sale.paid"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "sale.paid",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

sale.refunded — money was refunded from a sale Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. They fire for every sale write source — API, in-app, automations, and the sales the app records from other records. sale.cancelled fires once per cancelled item; reinstating fires nothing. Payload shapes: SaleRecordedEvent, SaleCancelledEvent, SalePaidEvent, SaleRefundedEvent.

Value: "sale.refunded"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "sale.refunded",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

booking.created — a booking was made Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. booking.completed / booking.no_show deliberately do not exist as webhooks — those statuses are sweep-derived (a cron inferring the past), not user actions. Payload shape: BookingEventBase.

Value: "booking.created"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "booking.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

booking.rescheduled — a booking moved to a new time Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. booking.completed / booking.no_show deliberately do not exist as webhooks — those statuses are sweep-derived (a cron inferring the past), not user actions. Payload shape: BookingEventBase.

Value: "booking.rescheduled"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "booking.rescheduled",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

booking.cancelled — a booking was cancelled Webhook

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).

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. booking.completed / booking.no_show deliberately do not exist as webhooks — those statuses are sweep-derived (a cron inferring the past), not user actions. Payload shape: BookingEventBase.

Value: "booking.cancelled"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "booking.cancelled",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

booking.reassigned — the booking's host changed Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events. booking.completed / booking.no_show deliberately do not exist as webhooks — those statuses are sweep-derived (a cron inferring the past), not user actions. Payload shape: BookingEventBase.

Value: "booking.reassigned"
created_at
required
string <date-time>

When the event occurred.

required
object

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "booking.reassigned",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

event.attendance.changed — an attendee's status changed Webhook

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).

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "event.attendance.changed",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

form.submitted — a form was submitted Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "form.submitted",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

ticket.created — a ticket was opened Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events, and only while the plan includes Customer tickets. They fire for every write source (help page, widget, the app, automations, the API). Quiet: a ticket in spam emits no created / message_created; internal notes never emit; reassignments that follow a contact's owner hand-over or a team member's offboarding are quiet; nothing is sent about an anonymised contact. Payload shape: TicketEventData.

Value: "ticket.created"
created_at
required
string <date-time>

When the event occurred.

required
object (Ticket event data)

ticket is exactly the /v1/tickets ticket object at the moment of the change; contact repeats its contact block.

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "ticket.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

ticket.message_created — a customer or team message was added Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events, and only while the plan includes Customer tickets. They fire for every write source (help page, widget, the app, automations, the API). Quiet: a ticket in spam emits no created / message_created; internal notes never emit; reassignments that follow a contact's owner hand-over or a team member's offboarding are quiet; nothing is sent about an anonymised contact. Payload shape: TicketEventData.

Value: "ticket.message_created"
created_at
required
string <date-time>

When the event occurred.

required
object (Ticket event data)

ticket is exactly the /v1/tickets ticket object at the moment of the change; contact repeats its contact block.

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "ticket.message_created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

ticket.status_changed — the status was changed explicitly Webhook

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.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events, and only while the plan includes Customer tickets. They fire for every write source (help page, widget, the app, automations, the API). Quiet: a ticket in spam emits no created / message_created; internal notes never emit; reassignments that follow a contact's owner hand-over or a team member's offboarding are quiet; nothing is sent about an anonymised contact. Payload shape: TicketEventData.

Value: "ticket.status_changed"
created_at
required
string <date-time>

When the event occurred.

required
object (Ticket event data)

ticket is exactly the /v1/tickets ticket object at the moment of the change; contact repeats its contact block.

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "ticket.status_changed",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

ticket.assigned — triage changed the assignee Webhook

Opt-in ticket event. Fires when triage changes a ticket's assignee, unassigning included. data.change.assignee carries from_user_id → to_user_id.

Authorizations:
bearerAuth
header Parameters
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. email.bounced).

X-Otok-Event-Id
required
string <uuid>

The event id (same as the payload id). Stable across retries and shared across your endpoints — use it as your dedup key.

X-Otok-Signature
required
string^t=\d+,v1=[0-9a-f]{64}$

t=<unix seconds>,v1=<hex HMAC>. The signed payload is the string "<t>" + "." + <raw request body> (the exact bytes on the wire). v1 is the lowercase hex HMAC-SHA256 of that string, keyed with the entire secret string including the whsec_ prefix. t is the unix-seconds timestamp of this delivery attempt — retries carry fresh timestamps, so you can enforce a replay window. Exactly one v1 value is sent per request. Compare with a timing-safe comparison.

Request Body schema: application/json
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 events, and only while the plan includes Customer tickets. They fire for every write source (help page, widget, the app, automations, the API). Quiet: a ticket in spam emits no created / message_created; internal notes never emit; reassignments that follow a contact's owner hand-over or a team member's offboarding are quiet; nothing is sent about an anonymised contact. Payload shape: TicketEventData.

Value: "ticket.assigned"
created_at
required
string <date-time>

When the event occurred.

required
object (Ticket event data)

ticket is exactly the /v1/tickets ticket object at the moment of the change; contact repeats its contact block.

Responses

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "type": "ticket.assigned",
  • "created_at": "2019-08-24T14:15:22Z",
  • "data": {
    }
}

List endpoints

No parameters (max 3 rows, unpaginated). Secrets are never included.

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Register an endpoint (secret returned once)

Maximum 3 endpoints per workspace (409 endpoint_limit_reached); the cap is enforced safely under concurrency.

The secret is 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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
url
required
string [ 1 .. 2048 ] characters

http:// or https:// only. URLs pointing at private, loopback, link-local, and other reserved IP ranges are rejected (400 unsafe_url) — re-checked on every delivery attempt. Prefer https://: plain http:// is accepted but the HMAC signature is then your only integrity protection.

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 (email.delivered, email.bounced, email.complained) — every other family is opt-in and must be listed explicitly: the engagement events email.opened/email.clicked, the five order.* lifecycle events, the four payment_request.* lifecycle events, the payment refund event (payment.refunded), the four contact lifecycle + consent events (contact.created, contact.updated, contact.deleted, contact.consent_changed), the inbound message event (message.received), the four deal.* lifecycle events, the four sale.* events, the four booking.* lifecycle events, the event-attendance event (event.attendance.changed), the form submission event (form.submitted), and the four ticket.* customer-ticket events. A pre-existing registration never starts receiving a new family unasked. email.failed is deprecated: still accepted when listed explicitly (the registration succeeds and echoes it in events), but it is never delivered.

Responses

Request samples

Content type
application/json
{
  • "url": "string",
  • "events": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "url": "string",
  • "events": [
    ],
  • "is_active": true,
  • "created_at": "2019-08-24T14:15:22Z",
  • "secret": "string"
}

Delete an endpoint

Deliveries stop immediately; anything still queued for the deleted endpoint is dropped.

Authorizations:
bearerAuth
path Parameters
id
required
string

Endpoint id. Unlike other routes, a malformed (non-UUID) id returns 404, not 400 — deliberately indistinguishable from an unknown id.

Responses

Response samples

Content type
application/json
{
  • "statusCode": 0,
  • "message": "string",
  • "error": "string",
  • "error_code": "string"
}

Meeting types

Read meeting types and query open slots.

List meeting types

Standard list conventions apply.

Authorizations:
bearerAuth
query Parameters
filter
string
Default: "{}"
Example: filter={"lifecycle_stage":"lead"}

String-encoded JSON object. Invalid JSON → 400 "Invalid filter: must be valid JSON"; a non-object → 400 "Invalid filter: must be a JSON object". Any workspace_id in the filter is ignored and replaced with your authenticated workspace.

Grammar: exact match ({"lifecycle_stage": "lead"}; array values match any member), $where (nested condition tree, max 100 nodes, max 200 items per array value), and $gt (the only top-level comparison shortcut). Contacts additionally support $jsonb_contains, $event_attendance, and virtual consent fields. Note: the full set of $where operators is not exhaustively enumerated in the public docs — see the Getting Started guide for the documented examples.

Filter values are type-checked against the target field before the query runs — a mistyped value returns 400 Invalid filter value for "<field>": … (per field kind: not a date / not a UUID / must be one of / not a number / not a boolean). Date-only strings ("2026-01-01"), numeric strings ("125"), and "true"/"false" strings are accepted and coerced. Substring operators (contains, not_contains, starts_with, ends_with) in $where trees are rejected on non-text fields (400 Invalid filter on "<field>": substring operators (contains, starts_with, ends_with) only apply to text fields).

sort
string
Default: "-created_at"

Field name; - prefix for descending.

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).

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Get one meeting type

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": {
    },
  • "hosts": [
    ],
  • "created_at": "2019-08-24T14:15:22Z"
}

Get open slots for a date range

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

query Parameters
from
required
string <date-time>

Range start (inclusive), ISO 8601.

to
required
string <date-time>

Range end (exclusive); must be after from; range may not exceed 62 days.

Responses

Response samples

Content type
application/json
{
  • "meeting_type_id": "45f6cfc0-931f-46e2-aa0a-27c7890f7f67",
  • "timezone": "string",
  • "duration_minutes": 0,
  • "slots": [
    ]
}

Get website-embed material

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".

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "workspace_ref": "string",
  • "slug": "string",
  • "embed_key": "string",
  • "page_url": "http://example.com",
  • "snippet_html": "string"
}

Bookings

Booking lifecycle — create, cancel, reschedule, reassign host.

List bookings

All filters optional and combined with AND. Malformed values → 400. A blank limit or offset (?limit=) means the default, not 0.

Authorizations:
bearerAuth
query Parameters
status
string
Enum: "confirmed" "cancelled" "completed" "no_show"
meeting_type_id
string <uuid>
from
string <date-time>

start_at >= from (ISO 8601).

to
string <date-time>

start_at <= to (ISO 8601).

sort
string
Default: "-start_at"

Default -start_at; - prefix for descending.

limit
integer [ 0 .. 500 ]
Default: 50

Hard cap 500. Non-integer or negative → 400.

offset
integer >= 0
Default: 0

Non-integer or negative → 400.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Create a booking

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
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. Europe/Berlin) — recorded as the invitee's timezone.

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.

Responses

Request samples

Content type
application/json
{
  • "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": {
    },
  • "notes": "string",
  • "host_user_id": "30451f6b-8801-4963-900e-e86af0f08607"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "duplicate": true
}

Get one booking

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Cancel a booking

API cancellations are attributed to the host side.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
optional
reason
string <= 1000 characters

Responses

Request samples

Content type
application/json
{
  • "reason": "string"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Move a booking to a new slot

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "start_at": "2019-08-24T14:15:22Z",
  • "timezone": "string"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Change the assigned host

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
optional
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 true.

Value: true

Responses

Request samples

Content type
application/json
{
  • "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
  • "reason": "string",
  • "force": true
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "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": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Tickets

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.

List tickets

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.

Authorizations:
bearerAuth
query Parameters
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 unassigned. Anything else → 400 invalid_assignee.

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"

portal = the hosted help page, widget = the website widget, agent = logged by a team member in the app, api = this 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 invalid_date.

created_to
string <date-time>

Created at/before this instant. A date that does not exist → 400 invalid_date.

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

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Open a ticket (idempotent via external_reference)

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
contact_id
string <uuid>

An existing contact. Mutually exclusive with contact.

object

Identity to upsert — resolved by phone, then email, then national ID, exactly like POST /v1/contacts. At least one valid identifier is required.

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"

customer behaves like a ticket opened on the help page: routing, the first-response target, the team's new-ticket alert, the inquiry queue (under an intake procedure that enables tickets), and a blocked contact's ticket filed into spam. team behaves like a ticket a team member logged: none of those.

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 assigned_user_id.

notify_customer
boolean
Default: true

false sends no creation email (for example when importing).

external_reference
string <= 200 characters

Your own key, unique per workspace. Sending it again answers the original ticket with duplicate: true and changes nothing.

Responses

Request samples

Content type
application/json
{
  • "contact_id": "f245e2a3-e141-4d41-912c-b1c1f5bd5598",
  • "contact": {
    },
  • "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"
}

Response samples

Content type
application/json
{
  • "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": {
    },
  • "assignee": {
    },
  • "first_response_at": "2019-08-24T14:15:22Z",
  • "response_target": {
    },
  • "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": [
    ],
  • "messages_has_more": true,
  • "messages_next_before": "621a0863-7ecf-4331-9b00-dd8e69408552",
  • "duplicate": true,
  • "customer_email_skipped": "no_verified_sender"
}

Get a ticket with a page of its conversation

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

query Parameters
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 messages_next_before. A message id outside this ticket's conversation → 400 invalid_cursor.

Responses

Response samples

Content type
application/json
{
  • "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": {
    },
  • "assignee": {
    },
  • "first_response_at": "2019-08-24T14:15:22Z",
  • "response_target": {
    },
  • "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": [
    ],
  • "messages_has_more": true,
  • "messages_next_before": "621a0863-7ecf-4331-9b00-dd8e69408552",
  • "last_customer_email": {
    }
}

Triage a ticket

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
status
string
Enum: "open" "resolved" "closed"
priority
string (TicketPriority)
Enum: "low" "normal" "high" "urgent"
category
string or null <= 80 characters

A ticket category, or null to clear it.

assigned_user_id
string or null <uuid>

Assign to this team member, or null to unassign.

assignee_email
string <email> <= 320 characters

Assign by login email. Mutually exclusive with assigned_user_id.

Responses

Request samples

Content type
application/json
{
  • "status": "open",
  • "priority": "low",
  • "category": "string",
  • "assigned_user_id": "fb44a81f-f77a-4323-8a34-49b1af008bf9",
  • "assignee_email": "user@example.com"
}

Response samples

Content type
application/json
{
  • "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": {
    },
  • "assignee": {
    },
  • "first_response_at": "2019-08-24T14:15:22Z",
  • "response_target": {
    },
  • "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"
}

Reply to the customer

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.

Authorizations:
bearerAuth
path Parameters
id
required
string <uuid>

Record UUID. A non-UUID value returns 400.

Request Body schema: application/json
required
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 author_user_id.

idempotency_key
string <= 200 characters

Makes retries safe — the same key answers the original reply. Scoped to your API key.

Responses

Request samples

Content type
application/json
{
  • "body": "string",
  • "author_user_id": "13209de9-6c50-42f7-a1fd-5bf87d7b9225",
  • "author_email": "user@example.com",
  • "idempotency_key": "string"
}

Response samples

Content type
application/json
{
  • "message": {
    },
  • "ticket": {
    },
  • "duplicate": true,
  • "customer_email_skipped": "no_verified_sender"
}