Tidela API v1

Workspace-scoped REST API · Base URL https://app.tidela.ai/api/v1 · OpenAPI spec

Authentication

Create an API key in Admin → Developers → API & webhooks, then send it as a bearer token. A key belongs to one workspace and only ever sees that workspace's data. Keys carry scopes — read, write, export — and can be restricted by IP, rotated, and revoked.

curl https://app.tidela.ai/api/v1/ping/ \
  -H "Authorization: Bearer tgx_live_…"

Conventions

TopicHow it works
ErrorsNon-2xx responses return {"error": {"code", "message", "field", "request_id"}}. Every response carries X-Request-Id.
Rate limitsPer key, per minute (defaults: 120 reads, 60 writes). Watch X-RateLimit-Limit / -Remaining / -Reset; a 429 includes Retry-After.
IdempotencySend an Idempotency-Key header on writes. Retrying the same key + body returns the original response (Idempotency-Replayed: true) instead of double-processing.
PaginationLists return {"data": […], "has_more", "next_cursor"}. Pass cursor= to continue. limit= up to 200.
Sync?updated_since=ISO8601 returns changes oldest-first, including archived records ("archived": true) so you can mirror deletions.
Lean responses?fields=name,status selects fields; related data like custom_fields and affiliations only appear on lists when you ask via ?include=.
DeletesDELETE archives (soft-delete) — data is recoverable in the app, never destroyed.
TimestampsAll times are UTC ISO 8601.

Endpoints

Basics

GET /ping/ Verify a key: returns workspace, mode and scopes.

Verify a key: returns workspace, mode and scopes. (scope: read)

Accounts

GET /accounts/ List accounts.

List accounts. (scope: read)

Query parameterDescription
limitPage size (default 50, max 200).
cursorOpaque cursor from the previous page's next_cursor.
fieldsComma-separated field names to return (lean responses).
updated_sinceISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true).
sortfield or -field (whitelisted fields; uses offset paging via ?offset= instead of cursors; not combinable with updated_since).
<field>__<op>Operator filters on whitelisted fields: __gte __lte __gt __lt __contains __in (comma-separated). E.g. amount__gte=1000, created_at__lt=2026-01-01, status__in=open,won.
qName contains.
statusactive | inactive.
owner_idFilter by owner.
countryISO alpha-2.
include_archived1 to include archived records.
includecustom_fields
POST /accounts/ Create an account. Duplicate website domain returns 409 with th…

Create an account. Duplicate website domain returns 409 with the existing id (override with ?force=1). (scope: write)

Body fieldTypeNotes
namestringRequired on create.
websitestringDomain is derived from this for duplicate detection.
linkedin_urlstring
phonestring
streetstring
citystring
state_regionstring
postal_codestring
countrystringISO-3166 alpha-2, e.g. US.
statusstringactive | inactive.
customer_typestringAn Account Type key from this workspace's catalog (Admin → Account types), e.g. customer, prospect, vendor. Reads also include customer_type_name. Empty string clears it.
owner_iduuidMust be an active member of the workspace.
parent_iduuidParent account.
notesstring
custom_fieldsobject{key: value} — see GET /meta/custom-fields/.
POST /accounts/upsert/ Create-or-update matched by website domain. Response carries cr…

Create-or-update matched by website domain. Response carries created: true|false. (scope: write)

Body fieldTypeNotes
namestringRequired on create.
websitestringDomain is derived from this for duplicate detection.
linkedin_urlstring
phonestring
streetstring
citystring
state_regionstring
postal_codestring
countrystringISO-3166 alpha-2, e.g. US.
statusstringactive | inactive.
customer_typestringAn Account Type key from this workspace's catalog (Admin → Account types), e.g. customer, prospect, vendor. Reads also include customer_type_name. Empty string clears it.
owner_iduuidMust be an active member of the workspace.
parent_iduuidParent account.
notesstring
custom_fieldsobject{key: value} — see GET /meta/custom-fields/.
GET /accounts/{id}/ Fetch one account (custom fields included).

Fetch one account (custom fields included). (scope: read)

PATCH /accounts/{id}/ Update fields you send; everything else is untouched.

Update fields you send; everything else is untouched. (scope: write)

Body fieldTypeNotes
namestringRequired on create.
websitestringDomain is derived from this for duplicate detection.
linkedin_urlstring
phonestring
streetstring
citystring
state_regionstring
postal_codestring
countrystringISO-3166 alpha-2, e.g. US.
statusstringactive | inactive.
customer_typestringAn Account Type key from this workspace's catalog (Admin → Account types), e.g. customer, prospect, vendor. Reads also include customer_type_name. Empty string clears it.
owner_iduuidMust be an active member of the workspace.
parent_iduuidParent account.
notesstring
custom_fieldsobject{key: value} — see GET /meta/custom-fields/.
DELETE /accounts/{id}/ Archive (soft-delete) — the record is recoverable in the app, n…

Archive (soft-delete) — the record is recoverable in the app, never destroyed. (scope: write)

Contacts

GET /contacts/ List contacts.

List contacts. (scope: read)

Query parameterDescription
limitPage size (default 50, max 200).
cursorOpaque cursor from the previous page's next_cursor.
fieldsComma-separated field names to return (lean responses).
updated_sinceISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true).
sortfield or -field (whitelisted fields; uses offset paging via ?offset= instead of cursors; not combinable with updated_since).
<field>__<op>Operator filters on whitelisted fields: __gte __lte __gt __lt __contains __in (comma-separated). E.g. amount__gte=1000, created_at__lt=2026-01-01, status__in=open,won.
qName or email contains.
emailExact email.
owner_id
account_idLinked to this account.
include_archived1 to include archived.
includecustom_fields,affiliations
POST /contacts/ Create a contact. Duplicate email returns 409 with the existing…

Create a contact. Duplicate email returns 409 with the existing id. (scope: write)

Body fieldTypeNotes
first_namestring
last_namestring
titlestringThe person's title, e.g. Operations Manager (standard field; distinct from the per-company job_title on create).
emailstringUnique per workspace; duplicate returns 409.
phonestring
mobilestring
linkedin_urlstring
owner_iduuid
notesstring
account_iduuidCreate only: links the contact to an account.
job_titlestringCreate only: title at that account.
custom_fieldsobject{key: value}.
POST /contacts/upsert/ Create-or-update matched by email. Response carries created: tr…

Create-or-update matched by email. Response carries created: true|false. (scope: write)

Body fieldTypeNotes
first_namestring
last_namestring
titlestringThe person's title, e.g. Operations Manager (standard field; distinct from the per-company job_title on create).
emailstringUnique per workspace; duplicate returns 409.
phonestring
mobilestring
linkedin_urlstring
owner_iduuid
notesstring
account_iduuidCreate only: links the contact to an account.
job_titlestringCreate only: title at that account.
custom_fieldsobject{key: value}.
GET /contacts/{id}/ Fetch one contact (custom fields + affiliations included).

Fetch one contact (custom fields + affiliations included). (scope: read)

PATCH /contacts/{id}/ Update a contact.

Update a contact. (scope: write)

Body fieldTypeNotes
first_namestring
last_namestring
titlestringThe person's title, e.g. Operations Manager (standard field; distinct from the per-company job_title on create).
emailstringUnique per workspace; duplicate returns 409.
phonestring
mobilestring
linkedin_urlstring
owner_iduuid
notesstring
account_iduuidCreate only: links the contact to an account.
job_titlestringCreate only: title at that account.
custom_fieldsobject{key: value}.
DELETE /contacts/{id}/ Archive (soft-delete).

Archive (soft-delete). (scope: write)

Deals

GET /deals/ List deals.

List deals. (scope: read)

Query parameterDescription
limitPage size (default 50, max 200).
cursorOpaque cursor from the previous page's next_cursor.
fieldsComma-separated field names to return (lean responses).
updated_sinceISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true).
sortfield or -field (whitelisted fields; uses offset paging via ?offset= instead of cursors; not combinable with updated_since).
<field>__<op>Operator filters on whitelisted fields: __gte __lte __gt __lt __contains __in (comma-separated). E.g. amount__gte=1000, created_at__lt=2026-01-01, status__in=open,won.
qName contains.
statusopen | won | lost.
pipeline_id
stage_id
owner_id
account_idPrimary account.
include_archived1 to include archived.
includecustom_fields
POST /deals/ Create a deal (fires deal.created).

Create a deal (fires deal.created). (scope: write)

Body fieldTypeNotes
namestringRequired on create.
primary_account_iduuidRequired on create.
pipeline_iduuidDefaults to the workspace's first pipeline.
stage_iduuidMust belong to the pipeline. Changing it moves the deal (status/closed_at follow the stage type, automations fire).
amountstring|numberDecimal.
currencystring3-letter code.
owner_iduuid
lead_source_iduuid
source_detailstring
expected_close_datestringYYYY-MM-DD.
custom_fieldsobject{key: value}.
POST /deals/upsert/ Create-or-update matched by name + primary_account_id. Stage ch…

Create-or-update matched by name + primary_account_id. Stage changes go through PATCH /deals/{id}/ so full move semantics apply. (scope: write)

Body fieldTypeNotes
namestringRequired on create.
primary_account_iduuidRequired on create.
pipeline_iduuidDefaults to the workspace's first pipeline.
stage_iduuidMust belong to the pipeline. Changing it moves the deal (status/closed_at follow the stage type, automations fire).
amountstring|numberDecimal.
currencystring3-letter code.
owner_iduuid
lead_source_iduuid
source_detailstring
expected_close_datestringYYYY-MM-DD.
custom_fieldsobject{key: value}.
GET /deals/{id}/ Fetch one deal.

Fetch one deal. (scope: read)

PATCH /deals/{id}/ Update a deal. Sending a new stage_id moves the deal exactly li…

Update a deal. Sending a new stage_id moves the deal exactly like dragging it on the board (status, closed_at, commissions, automations). (scope: write)

Body fieldTypeNotes
namestringRequired on create.
primary_account_iduuidRequired on create.
pipeline_iduuidDefaults to the workspace's first pipeline.
stage_iduuidMust belong to the pipeline. Changing it moves the deal (status/closed_at follow the stage type, automations fire).
amountstring|numberDecimal.
currencystring3-letter code.
owner_iduuid
lead_source_iduuid
source_detailstring
expected_close_datestringYYYY-MM-DD.
custom_fieldsobject{key: value}.
DELETE /deals/{id}/ Archive (soft-delete).

Archive (soft-delete). (scope: write)

Meetings

GET /meetings/ List meetings (activities with the 'meeting' type).

List meetings (activities with the 'meeting' type). (scope: read)

Query parameterDescription
limitPage size (default 50, max 200).
cursorOpaque cursor from the previous page's next_cursor.
fieldsComma-separated field names to return (lean responses).
updated_sinceISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true).
sortfield or -field (whitelisted fields; uses offset paging via ?offset= instead of cursors; not combinable with updated_since).
<field>__<op>Operator filters on whitelisted fields: __gte __lte __gt __lt __contains __in (comma-separated). E.g. amount__gte=1000, created_at__lt=2026-01-01, status__in=open,won.
statusopen | completed | cancelled.
assigned_to_id
starts_afterISO 8601 — scheduled_at lower bound.
starts_beforeISO 8601 — scheduled_at upper bound.
account_id
contact_id
deal_id
project_id
POST /meetings/ Create a meeting (subject + starts_at required; duration_min, a…

Create a meeting (subject + starts_at required; duration_min, assigned_to_id, account_ids/contact_ids/deal_ids/project_ids optional). Read/update items via /activities/{id}/. For a Microsoft-connected assignee, the meeting appears on their Outlook calendar on the next sync. (scope: write)

Activities

GET /activities/ List activities (calls, tasks, meetings, notes…).

List activities (calls, tasks, meetings, notes…). (scope: read)

Query parameterDescription
limitPage size (default 50, max 200).
cursorOpaque cursor from the previous page's next_cursor.
fieldsComma-separated field names to return (lean responses).
updated_sinceISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true).
sortfield or -field (whitelisted fields; uses offset paging via ?offset= instead of cursors; not combinable with updated_since).
<field>__<op>Operator filters on whitelisted fields: __gte __lte __gt __lt __contains __in (comma-separated). E.g. amount__gte=1000, created_at__lt=2026-01-01, status__in=open,won.
statusopen | completed | cancelled.
assigned_to_id
type_id
account_id
contact_id
deal_id
project_id
due_beforeISO 8601.
due_afterISO 8601.
POST /activities/ Log an activity / create a task.

Log an activity / create a task. (scope: write)

Body fieldTypeNotes
subjectstringRequired on create.
bodystring
type_iduuidAn activity type id (see /meta/options/); or pass `type` with the type key. Defaults to the first type.
directionstringinbound | outbound | internal.
statusstringopen | completed | cancelled.
scheduled_atstringISO 8601.
due_atstringISO 8601.
completed_atstringISO 8601.
duration_mininteger
assigned_to_iduuid
account_idsuuid[]Linked accounts.
contact_idsuuid[]Linked contacts.
deal_idsuuid[]Linked deals.
project_idsuuid[]Linked projects.
GET /activities/{id}/ Fetch one activity.

Fetch one activity. (scope: read)

PATCH /activities/{id}/ Update an activity (marking status completed stamps completed_a…

Update an activity (marking status completed stamps completed_at). (scope: write)

Body fieldTypeNotes
subjectstringRequired on create.
bodystring
type_iduuidAn activity type id (see /meta/options/); or pass `type` with the type key. Defaults to the first type.
directionstringinbound | outbound | internal.
statusstringopen | completed | cancelled.
scheduled_atstringISO 8601.
due_atstringISO 8601.
completed_atstringISO 8601.
duration_mininteger
assigned_to_iduuid
account_idsuuid[]Linked accounts.
contact_idsuuid[]Linked contacts.
deal_idsuuid[]Linked deals.
project_idsuuid[]Linked projects.

Projects

GET /projects/ List projects.

List projects. (scope: read)

Query parameterDescription
limitPage size (default 50, max 200).
cursorOpaque cursor from the previous page's next_cursor.
fieldsComma-separated field names to return (lean responses).
updated_sinceISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true).
sortfield or -field (whitelisted fields; uses offset paging via ?offset= instead of cursors; not combinable with updated_since).
<field>__<op>Operator filters on whitelisted fields: __gte __lte __gt __lt __contains __in (comma-separated). E.g. amount__gte=1000, created_at__lt=2026-01-01, status__in=open,won.
qName contains.
stage_id
owner_id
healthon_track | at_risk | off_track.
prioritylow | medium | high.
includecustom_fields
POST /projects/ Create a project (fires project.created).

Create a project (fires project.created). (scope: write)

Body fieldTypeNotes
namestringRequired on create.
stage_iduuidProject stage (see /meta/options/). Changing it fires project.stage_changed.
owner_iduuid
healthstringon_track | at_risk | off_track.
prioritystringlow | medium | high.
start_datestringYYYY-MM-DD.
target_end_datestringYYYY-MM-DD.
actual_end_datestringYYYY-MM-DD.
account_idsuuid[]Create only: linked accounts (first becomes primary).
custom_fieldsobject{key: value}.
GET /projects/{id}/ Fetch one project.

Fetch one project. (scope: read)

PATCH /projects/{id}/ Update a project.

Update a project. (scope: write)

Body fieldTypeNotes
namestringRequired on create.
stage_iduuidProject stage (see /meta/options/). Changing it fires project.stage_changed.
owner_iduuid
healthstringon_track | at_risk | off_track.
prioritystringlow | medium | high.
start_datestringYYYY-MM-DD.
target_end_datestringYYYY-MM-DD.
actual_end_datestringYYYY-MM-DD.
account_idsuuid[]Create only: linked accounts (first becomes primary).
custom_fieldsobject{key: value}.

Workspace

GET /users/ List the workspace's active members (read-only).

List the workspace's active members (read-only). (scope: read)

GET /meta/custom-fields/ Custom field definitions per entity: id, key, label, type, opti…

Custom field definitions per entity: id, key, label, type, options, read_only (formula/rollup/file values are read-only via API). (scope: read)

POST /meta/custom-fields/ Create a custom field definition (key auto-generated from the l…

Create a custom field definition (key auto-generated from the label; per-type options validation matches the in-app builder). (scope: write)

Body fieldTypeNotes
entitystringaccount | contact | opportunity | project (plural slugs accepted).
labelstringRequired.
typestringOne of the 20 field types (text, number, select, multiselect, date, lookup, formula, rollup, file, …).
optionsstring[]Select/radio/multiselect choices; lookup target entity; formula expression; rollup config.
GET /meta/custom-fields/{id}/ Fetch one custom field definition.

Fetch one custom field definition. (scope: read)

PATCH /meta/custom-fields/{id}/ Update label, options, sort_order, or is_active. A field's TYPE…

Update label, options, sort_order, or is_active. A field's TYPE cannot change — create a new field instead. (scope: write)

Body fieldTypeNotes
labelstring
optionsstring[]Revalidated per type.
sort_orderinteger
is_activeboolean
GET /meta/options/ Pipelines + stages, project stages, activity types, lead source…

Pipelines + stages, project stages, activity types, lead sources, and the available webhook event names. (scope: read)

GET /events/ The workspace's event stream — polling fallback for webhooks.

The workspace's event stream — polling fallback for webhooks. (scope: read)

Query parameterDescription
sinceISO 8601 — events created after this.
eventFilter by event name.
limit
cursor

Search

GET /search/ Cross-object substring search: account names/domains/phones, co…

Cross-object substring search: account names/domains/phones, contact names/emails/phones, deal and project names. Grouped lean results. (scope: read)

Query parameterDescription
qSearch text (min 2 characters).
limitResults per type (default 10, max 25).

Batch & export

POST /batch/{entity}/ Bulk ingest for accounts | contacts | deals: up to 100 records …

Bulk ingest for accounts | contacts | deals: up to 100 records per request, mode create or upsert, per-row results — one bad row never aborts the rest. (scope: write)

Body fieldTypeNotes
modestringcreate | upsert.
recordsobject[]Same shapes as the entity's POST body.
GET /export/{entity}/ Bulk pull for accounts | contacts | deals | activities | projec…

Bulk pull for accounts | contacts | deals | activities | projects. Requires the export scope; pages up to 1000; lower rate limit; every request audited as an export. (scope: export)

Query parameterDescription
limitUp to 1000 (default 500).
cursor
updated_sinceSync mode, as on lists.
<field>__<op>Operator filters, as on lists.
includecustom_fields

Webhook management

GET /webhooks/ List this workspace's webhook subscriptions (no secrets).

List this workspace's webhook subscriptions (no secrets). (scope: read)

POST /webhooks/ Create a subscription. The signing secret and the loop-skip tok…

Create a subscription. The signing secret and the loop-skip token are returned ONCE, in this response only. (scope: write)

Body fieldTypeNotes
namestring
target_urlstringYour https endpoint.
eventsstring[]Event names; empty list = all events.
GET /webhooks/{id}/ Fetch one subscription.

Fetch one subscription. (scope: read)

PATCH /webhooks/{id}/ Update name, target_url, events, or is_active (re-activating cl…

Update name, target_url, events, or is_active (re-activating clears an auto-disable). (scope: write)

Body fieldTypeNotes
namestring
target_urlstring
eventsstring[]
is_activeboolean
DELETE /webhooks/{id}/ Delete a subscription.

Delete a subscription. (scope: write)

GET /webhooks/{id}/deliveries/ The subscription's 50 most recent deliveries.

The subscription's 50 most recent deliveries. (scope: read)

Files & notes

GET /files/{entity}/{record_id}/ List a record's attachments (file-type custom fields): field ke…

List a record's attachments (file-type custom fields): field key, filename, size, download URL. Entities: accounts | contacts | deals | projects. (scope: read)

GET /files/{entity}/{record_id}/{field_key}/ Download the attached file (Content-Disposition: attachment).

Download the attached file (Content-Disposition: attachment). (scope: read)

POST /files/{entity}/{record_id}/{field_key}/ Attach or replace: multipart/form-data with a 'file' field, 25 …

Attach or replace: multipart/form-data with a 'file' field, 25 MB max, one file per field per record (mirrors the app). (scope: write)

DELETE /files/{entity}/{record_id}/{field_key}/ Remove the attached file.

Remove the attached file. (scope: write)

GET /notes/ List notes (the Note activity type, notes-shaped).

List notes (the Note activity type, notes-shaped). (scope: read)

Query parameterDescription
account_id
contact_id
deal_id
project_id
updated_since
limit
cursor
POST /notes/ Attach a note to records without learning the activity model.

Attach a note to records without learning the activity model. (scope: write)

Body fieldTypeNotes
contentstringRequired. The note text.
subjectstringOptional; defaults to the first line of content.
account_iduuidLink to an account (or account_ids list).
contact_iduuidLink to a contact (or contact_ids list).
deal_iduuidLink to a deal (or deal_ids list).
project_iduuidLink to a project (or project_ids list).

Quotes & partners (read-only)

GET /quotes/ List quotes.

List quotes. (scope: read)

Query parameterDescription
statusdraft | sent | accepted | declined.
deal_id
updated_since
limit
cursor
GET /quotes/{id}/ Fetch one quote incl. line items and totals.

Fetch one quote incl. line items and totals. (scope: read)

GET /partners/ List partners.

List partners. (scope: read)

Query parameterDescription
statuspending | active | …
updated_since
GET /partners/{id}/ Fetch one partner.

Fetch one partner. (scope: read)

GET /partner-leads/ List partner-registered leads.

List partner-registered leads. (scope: read)

Query parameterDescription
statuspending | approved | rejected.
partner_id

History & comments

GET /history/{entity}/{id}/ A record's field-level change history, newest first: who change…

A record's field-level change history, newest first: who changed what, old → new, and how (app, API, AI assistant, or automation). Entities: account, contact, deal, project. (scope: read)

Query parameterDescription
limitDefault 50, max 200.
offsetSkip N rows.
GET /comments/ List a record's comments (oldest first).

List a record's comments (oldest first). (scope: read)

Query parameterDescription
entityaccount | contact | deal | project.
record_idThe record's UUID.
POST /comments/ Add a comment. @username mentions notify the mentioned members;…

Add a comment. @username mentions notify the mentioned members; fires comment.created. (scope: write)

Body fieldTypeNotes
entitystringaccount | contact | deal | project.
record_iduuid
bodystringThe comment text (max 5000 chars).
PATCH /comments/{id}/ Edit a comment's body.

Edit a comment's body. (scope: write)

Body fieldTypeNotes
bodystring
DELETE /comments/{id}/ Soft-delete a comment. (Refused for OAuth connections, like eve…

Soft-delete a comment. (Refused for OAuth connections, like every delete.) (scope: write)

Webhooks

Subscribe an HTTPS endpoint in Admin → Developers → API & webhooks — or programmatically via POST /webhooks/. We POST a lean JSON payload when things happen:

account.archivedaccount.createdaccount.mergedaccount.updatedactivity.completedcomment.createdcontact.archivedcontact.createdcontact.mergedcontact.updatedcustom_record.createdcustom_record.updateddeal.archiveddeal.createddeal.stage_changeddeal.updatedpartner.appliedproject.createdproject.stage_changedproject.updatedquote.accepted

{
  "id": "…event id…",
  "event": "deal.stage_changed",
  "created_at": "2026-06-10T18:04:11Z",
  "data": {"id": "…deal id…", "object": "deal", "name": "Crane deal",
           "status": "won", "stage_id": "…", "old_stage_id": "…"}
}

Event semantics. *.updated fires on field changes (including custom-field changes); *.archived when a record is soft-deleted; *.merged once per merge (payload carries the surviving id and the merged_id). To keep your endpoint sane: changes made by automation rules, bulk imports, and demo data emit no events, and at most one event of a given type fires per record per request. Deliveries are at-least-once — dedupe on the event id.

Verify signatures. Each delivery carries X-Tidela-Signature: t=<unix ts>,v1=<hex> where v1 is HMAC-SHA256 of "{t}.{raw body}" with your subscription's signing secret. Compare with a constant-time function and reject timestamps older than 5 minutes.

# Python receiver
import hashlib, hmac, time

def verify(header, body, secret, tolerance=300):
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False
    expected = hmac.new(secret.encode(),
                        f"{parts['t']}.{body}".encode(),
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Respond with any 2xx. Failures retry on a backoff schedule for about three days, then appear in the delivery log for manual redelivery. If your endpoint was down, poll GET /events/ to catch up.

Avoiding loops. If your integration writes back to the API in response to a webhook, send the subscription's skip token on those writes — X-Tidela-Webhook-Skip: <token> (shown in the admin) — and the events your write generates won't be delivered back to that same subscription.

Connect AI assistants (MCP)

Tidela is an MCP server: connect Claude, ChatGPT, or any MCP-capable assistant directly to your workspace and ask it things like "prep me for tomorrow's meeting with Acme" or "log a note on the Crane deal and move it to Proposal."

Add this URL as a custom connector / remote MCP server in your assistant:

https://app.tidela.ai/mcp/

Your assistant registers itself, then sends you to the Tidela consent screen: sign in, pick the workspace, approve. Workspace owners and admins can always connect; regular members need the workspace's "members may connect AI assistants" setting turned on (Admin → Developers → API & webhooks). The connection appears under Connected apps with a one-click disconnect.

What a connected assistant can do: search; read accounts, contacts, deals, activities and projects; create and update single records; move deals by stage name; log notes. What it can never do: bulk-export your data, delete or archive records, or use the batch endpoints — those require an admin API key. Every tool call is rate-limited and audited like a normal API call.

OAuth 2.0 for app developers

Building an integration your customers install themselves? Use the authorization-code flow instead of asking them to paste API keys. Discovery, registration, and token endpoints (PKCE S256 required for public clients; read and write scopes only — export is never grantable to apps):

EndpointPurpose
/.well-known/oauth-authorization-serverServer metadata (RFC 8414).
/.well-known/oauth-protected-resourceResource metadata for MCP clients (RFC 9728).
POST /oauth/register/Dynamic Client Registration (RFC 7591). Vetted partner apps can instead be registered for you — contact us.
GET /oauth/authorize/Consent screen (response_type=code, exact-match redirect_uri, PKCE).
POST /oauth/token/Code exchange + refresh (rotating refresh tokens). Access tokens are sent as normal bearers on /api/v1/… and /mcp/.
POST /oauth/introspect/ · POST /oauth/revoke/RFC 7662 / RFC 7009 (client-authenticated, own tokens only).

OAuth connections carry the same hard limits as AI assistants: no exports, no deletes, no batch — one record at a time, fully audited, revocable per-connection in the admin.

Fair use

API access is included with your workspace's plan as a flat entitlement — there is no per-call charge. Rate limits and quotas are fair-use guardrails; sustained bulk extraction is throttled and flagged. Every export is audited.