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
| Topic | How it works |
|---|---|
| Errors | Non-2xx responses return {"error": {"code", "message", "field", "request_id"}}. Every response carries X-Request-Id. |
| Rate limits | Per key, per minute (defaults: 120 reads, 60 writes). Watch X-RateLimit-Limit / -Remaining / -Reset; a 429 includes Retry-After. |
| Idempotency | Send an Idempotency-Key header on writes. Retrying the same key + body returns the original response (Idempotency-Replayed: true) instead of double-processing. |
| Pagination | Lists 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=. |
| Deletes | DELETE archives (soft-delete) — data is recoverable in the app, never destroyed. |
| Timestamps | All 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 parameter | Description |
|---|---|
limit | Page size (default 50, max 200). |
cursor | Opaque cursor from the previous page's next_cursor. |
fields | Comma-separated field names to return (lean responses). |
updated_since | ISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true). |
sort | field 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. |
q | Name contains. |
status | active | inactive. |
owner_id | Filter by owner. |
country | ISO alpha-2. |
include_archived | 1 to include archived records. |
include | custom_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 field | Type | Notes |
|---|---|---|
name | string | Required on create. |
website | string | Domain is derived from this for duplicate detection. |
linkedin_url | string | |
phone | string | |
street | string | |
city | string | |
state_region | string | |
postal_code | string | |
country | string | ISO-3166 alpha-2, e.g. US. |
status | string | active | inactive. |
customer_type | string | An 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_id | uuid | Must be an active member of the workspace. |
parent_id | uuid | Parent account. |
notes | string | |
custom_fields | object | {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 field | Type | Notes |
|---|---|---|
name | string | Required on create. |
website | string | Domain is derived from this for duplicate detection. |
linkedin_url | string | |
phone | string | |
street | string | |
city | string | |
state_region | string | |
postal_code | string | |
country | string | ISO-3166 alpha-2, e.g. US. |
status | string | active | inactive. |
customer_type | string | An 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_id | uuid | Must be an active member of the workspace. |
parent_id | uuid | Parent account. |
notes | string | |
custom_fields | object | {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 field | Type | Notes |
|---|---|---|
name | string | Required on create. |
website | string | Domain is derived from this for duplicate detection. |
linkedin_url | string | |
phone | string | |
street | string | |
city | string | |
state_region | string | |
postal_code | string | |
country | string | ISO-3166 alpha-2, e.g. US. |
status | string | active | inactive. |
customer_type | string | An 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_id | uuid | Must be an active member of the workspace. |
parent_id | uuid | Parent account. |
notes | string | |
custom_fields | object | {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 parameter | Description |
|---|---|
limit | Page size (default 50, max 200). |
cursor | Opaque cursor from the previous page's next_cursor. |
fields | Comma-separated field names to return (lean responses). |
updated_since | ISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true). |
sort | field 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. |
q | Name or email contains. |
email | Exact email. |
owner_id | |
account_id | Linked to this account. |
include_archived | 1 to include archived. |
include | custom_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 field | Type | Notes |
|---|---|---|
first_name | string | |
last_name | string | |
title | string | The person's title, e.g. Operations Manager (standard field; distinct from the per-company job_title on create). |
email | string | Unique per workspace; duplicate returns 409. |
phone | string | |
mobile | string | |
linkedin_url | string | |
owner_id | uuid | |
notes | string | |
account_id | uuid | Create only: links the contact to an account. |
job_title | string | Create only: title at that account. |
custom_fields | object | {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 field | Type | Notes |
|---|---|---|
first_name | string | |
last_name | string | |
title | string | The person's title, e.g. Operations Manager (standard field; distinct from the per-company job_title on create). |
email | string | Unique per workspace; duplicate returns 409. |
phone | string | |
mobile | string | |
linkedin_url | string | |
owner_id | uuid | |
notes | string | |
account_id | uuid | Create only: links the contact to an account. |
job_title | string | Create only: title at that account. |
custom_fields | object | {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 field | Type | Notes |
|---|---|---|
first_name | string | |
last_name | string | |
title | string | The person's title, e.g. Operations Manager (standard field; distinct from the per-company job_title on create). |
email | string | Unique per workspace; duplicate returns 409. |
phone | string | |
mobile | string | |
linkedin_url | string | |
owner_id | uuid | |
notes | string | |
account_id | uuid | Create only: links the contact to an account. |
job_title | string | Create only: title at that account. |
custom_fields | object | {key: value}. |
DELETE /contacts/{id}/ Archive (soft-delete).
Archive (soft-delete). (scope: write)
Deals
GET /deals/ List deals.
List deals. (scope: read)
| Query parameter | Description |
|---|---|
limit | Page size (default 50, max 200). |
cursor | Opaque cursor from the previous page's next_cursor. |
fields | Comma-separated field names to return (lean responses). |
updated_since | ISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true). |
sort | field 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. |
q | Name contains. |
status | open | won | lost. |
pipeline_id | |
stage_id | |
owner_id | |
account_id | Primary account. |
include_archived | 1 to include archived. |
include | custom_fields |
POST /deals/ Create a deal (fires deal.created).
Create a deal (fires deal.created). (scope: write)
| Body field | Type | Notes |
|---|---|---|
name | string | Required on create. |
primary_account_id | uuid | Required on create. |
pipeline_id | uuid | Defaults to the workspace's first pipeline. |
stage_id | uuid | Must belong to the pipeline. Changing it moves the deal (status/closed_at follow the stage type, automations fire). |
amount | string|number | Decimal. |
currency | string | 3-letter code. |
owner_id | uuid | |
lead_source_id | uuid | |
source_detail | string | |
expected_close_date | string | YYYY-MM-DD. |
custom_fields | object | {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 field | Type | Notes |
|---|---|---|
name | string | Required on create. |
primary_account_id | uuid | Required on create. |
pipeline_id | uuid | Defaults to the workspace's first pipeline. |
stage_id | uuid | Must belong to the pipeline. Changing it moves the deal (status/closed_at follow the stage type, automations fire). |
amount | string|number | Decimal. |
currency | string | 3-letter code. |
owner_id | uuid | |
lead_source_id | uuid | |
source_detail | string | |
expected_close_date | string | YYYY-MM-DD. |
custom_fields | object | {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 field | Type | Notes |
|---|---|---|
name | string | Required on create. |
primary_account_id | uuid | Required on create. |
pipeline_id | uuid | Defaults to the workspace's first pipeline. |
stage_id | uuid | Must belong to the pipeline. Changing it moves the deal (status/closed_at follow the stage type, automations fire). |
amount | string|number | Decimal. |
currency | string | 3-letter code. |
owner_id | uuid | |
lead_source_id | uuid | |
source_detail | string | |
expected_close_date | string | YYYY-MM-DD. |
custom_fields | object | {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 parameter | Description |
|---|---|
limit | Page size (default 50, max 200). |
cursor | Opaque cursor from the previous page's next_cursor. |
fields | Comma-separated field names to return (lean responses). |
updated_since | ISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true). |
sort | field 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. |
status | open | completed | cancelled. |
assigned_to_id | |
starts_after | ISO 8601 — scheduled_at lower bound. |
starts_before | ISO 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 parameter | Description |
|---|---|
limit | Page size (default 50, max 200). |
cursor | Opaque cursor from the previous page's next_cursor. |
fields | Comma-separated field names to return (lean responses). |
updated_since | ISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true). |
sort | field 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. |
status | open | completed | cancelled. |
assigned_to_id | |
type_id | |
account_id | |
contact_id | |
deal_id | |
project_id | |
due_before | ISO 8601. |
due_after | ISO 8601. |
POST /activities/ Log an activity / create a task.
Log an activity / create a task. (scope: write)
| Body field | Type | Notes |
|---|---|---|
subject | string | Required on create. |
body | string | |
type_id | uuid | An activity type id (see /meta/options/); or pass `type` with the type key. Defaults to the first type. |
direction | string | inbound | outbound | internal. |
status | string | open | completed | cancelled. |
scheduled_at | string | ISO 8601. |
due_at | string | ISO 8601. |
completed_at | string | ISO 8601. |
duration_min | integer | |
assigned_to_id | uuid | |
account_ids | uuid[] | Linked accounts. |
contact_ids | uuid[] | Linked contacts. |
deal_ids | uuid[] | Linked deals. |
project_ids | uuid[] | 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 field | Type | Notes |
|---|---|---|
subject | string | Required on create. |
body | string | |
type_id | uuid | An activity type id (see /meta/options/); or pass `type` with the type key. Defaults to the first type. |
direction | string | inbound | outbound | internal. |
status | string | open | completed | cancelled. |
scheduled_at | string | ISO 8601. |
due_at | string | ISO 8601. |
completed_at | string | ISO 8601. |
duration_min | integer | |
assigned_to_id | uuid | |
account_ids | uuid[] | Linked accounts. |
contact_ids | uuid[] | Linked contacts. |
deal_ids | uuid[] | Linked deals. |
project_ids | uuid[] | Linked projects. |
Projects
GET /projects/ List projects.
List projects. (scope: read)
| Query parameter | Description |
|---|---|
limit | Page size (default 50, max 200). |
cursor | Opaque cursor from the previous page's next_cursor. |
fields | Comma-separated field names to return (lean responses). |
updated_since | ISO 8601 — incremental sync mode: returns records changed since then, oldest first, INCLUDING archived ones (archived: true). |
sort | field 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. |
q | Name contains. |
stage_id | |
owner_id | |
health | on_track | at_risk | off_track. |
priority | low | medium | high. |
include | custom_fields |
POST /projects/ Create a project (fires project.created).
Create a project (fires project.created). (scope: write)
| Body field | Type | Notes |
|---|---|---|
name | string | Required on create. |
stage_id | uuid | Project stage (see /meta/options/). Changing it fires project.stage_changed. |
owner_id | uuid | |
health | string | on_track | at_risk | off_track. |
priority | string | low | medium | high. |
start_date | string | YYYY-MM-DD. |
target_end_date | string | YYYY-MM-DD. |
actual_end_date | string | YYYY-MM-DD. |
account_ids | uuid[] | Create only: linked accounts (first becomes primary). |
custom_fields | object | {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 field | Type | Notes |
|---|---|---|
name | string | Required on create. |
stage_id | uuid | Project stage (see /meta/options/). Changing it fires project.stage_changed. |
owner_id | uuid | |
health | string | on_track | at_risk | off_track. |
priority | string | low | medium | high. |
start_date | string | YYYY-MM-DD. |
target_end_date | string | YYYY-MM-DD. |
actual_end_date | string | YYYY-MM-DD. |
account_ids | uuid[] | Create only: linked accounts (first becomes primary). |
custom_fields | object | {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 field | Type | Notes |
|---|---|---|
entity | string | account | contact | opportunity | project (plural slugs accepted). |
label | string | Required. |
type | string | One of the 20 field types (text, number, select, multiselect, date, lookup, formula, rollup, file, …). |
options | string[] | 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 field | Type | Notes |
|---|---|---|
label | string | |
options | string[] | Revalidated per type. |
sort_order | integer | |
is_active | boolean |
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 parameter | Description |
|---|---|
since | ISO 8601 — events created after this. |
event | Filter 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 parameter | Description |
|---|---|
q | Search text (min 2 characters). |
limit | Results 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 field | Type | Notes |
|---|---|---|
mode | string | create | upsert. |
records | object[] | 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 parameter | Description |
|---|---|
limit | Up to 1000 (default 500). |
cursor | |
updated_since | Sync mode, as on lists. |
<field>__<op> | Operator filters, as on lists. |
include | custom_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 field | Type | Notes |
|---|---|---|
name | string | |
target_url | string | Your https endpoint. |
events | string[] | 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 field | Type | Notes |
|---|---|---|
name | string | |
target_url | string | |
events | string[] | |
is_active | boolean |
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 parameter | Description |
|---|---|
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 field | Type | Notes |
|---|---|---|
content | string | Required. The note text. |
subject | string | Optional; defaults to the first line of content. |
account_id | uuid | Link to an account (or account_ids list). |
contact_id | uuid | Link to a contact (or contact_ids list). |
deal_id | uuid | Link to a deal (or deal_ids list). |
project_id | uuid | Link to a project (or project_ids list). |
Quotes & partners (read-only)
GET /quotes/ List quotes.
List quotes. (scope: read)
| Query parameter | Description |
|---|---|
status | draft | 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 parameter | Description |
|---|---|
status | pending | 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 parameter | Description |
|---|---|
status | pending | 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 parameter | Description |
|---|---|
limit | Default 50, max 200. |
offset | Skip N rows. |
GET /comments/ List a record's comments (oldest first).
List a record's comments (oldest first). (scope: read)
| Query parameter | Description |
|---|---|
entity | account | contact | deal | project. |
record_id | The 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 field | Type | Notes |
|---|---|---|
entity | string | account | contact | deal | project. |
record_id | uuid | |
body | string | The comment text (max 5000 chars). |
PATCH /comments/{id}/ Edit a comment's body.
Edit a comment's body. (scope: write)
| Body field | Type | Notes |
|---|---|---|
body | string |
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):
| Endpoint | Purpose |
|---|---|
/.well-known/oauth-authorization-server | Server metadata (RFC 8414). |
/.well-known/oauth-protected-resource | Resource 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.