Contacts & Deals API — Technical Documentation¶
Base URL: https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1
Authentication: X-API-Key: cg_your_api_key_here
Content-Type: application/json
API keys are scoped to a single organization. All requests are tenant-isolated; you cannot read or write data outside the organization that owns the key.
Table of Contents¶
- Overview
- Contacts API
- 2.1 Endpoints
- 2.2 Request Fields
- 2.3 Bulk Payload Structure
- Deals API
- 3.1 Endpoints
- 3.2 Request Fields
- 3.3 Bulk Payload Structure
- Custom Fields
- Validation Rules
- Best Practices
- Full Example Flows
- Error Reference
1. Overview¶
Purpose¶
The Contacts and Deals APIs let external systems (e‑commerce platforms, lead-gen forms, ERPs, scripts) push customer data and sales opportunities into the CRM. They are the primary integration surface for:
- Importing customers from a website, Shopify, or in-house systems.
- Streaming new orders / opportunities into the sales pipeline.
- Keeping CRM data in sync with an external source of truth (upsert pattern).
- Searching contacts before creating new ones to prevent duplicates.
Single vs Bulk endpoints¶
| Use case | Endpoint type | Typical batch |
|---|---|---|
| Real-time event (new signup, single order) | Single (/create-lead, rest-api-proxy/deals POST) |
1 record |
| Nightly sync, CSV upload, migration | Bulk (/import-contacts-bulk, /create-deals-bulk) |
up to 100 (deals) / 500 (contacts) per call |
Bulk endpoints return per-row results so partial success is observable. Single endpoints return either the created record or an error.
Typical use cases¶
- E-commerce checkout → create a contact + a deal per order.
- Lead-gen form → upsert contact and assign to a sales rep.
- CRM enrichment → search by phone/email, then PATCH the contact.
- Pipeline reporting → bulk-load historical orders as deals into a "closed-won" stage.
2. Contacts API¶
2.1 Endpoints¶
| Action | Method | Path |
|---|---|---|
| Create single contact | POST |
/rest-api-proxy/contacts |
| Create + auto-deal (lead) | POST |
/create-lead |
| Bulk create / upsert contacts | POST |
/import-contacts-bulk |
| Get contact by ID | GET |
/rest-api-proxy/contacts?id=eq.<uuid> |
| Search contacts (phone) | GET |
/rest-api-proxy/contacts?phones=cs.{"+201017177777"} |
| Search contacts (email) | GET |
/rest-api-proxy/contacts?emails=cs.{"user@example.com"} |
| Search contacts (free text) | POST |
/search-contacts |
| Update contact | PATCH |
/rest-api-proxy/contacts?id=eq.<uuid> |
| Delete contact | DELETE |
/rest-api-proxy/contacts?id=eq.<uuid> |
All endpoints require:
2.1.1 Create single contact¶
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/contacts' \
-H 'X-API-Key: cg_xxx' \
-H 'Content-Type: application/json' \
-H 'Prefer: return=representation' \
-d '{
"first_name": "Mahmoud",
"last_name": "Ali",
"phones": ["+201017177777"],
"emails": ["mahmoud@example.com"],
"source": "website",
"tags": ["vip", "newsletter"],
"custom_fields": {
"customer_type": "VIP",
"last_order_value": 1200
}
}'
Response 201 Created:
[{
"id": "f2b4f7ab-7033-47ce-be06-b30a47de020a",
"first_name": "Mahmoud",
"last_name": "Ali",
"phones": ["+201017177777"],
"emails": ["mahmoud@example.com"],
"tags": ["vip", "newsletter"],
"source": "website",
"custom_fields": { "customer_type": "VIP", "last_order_value": 1200 },
"created_at": "2026-04-23T10:12:34.567Z"
}]
Status codes: 201 created · 400 invalid payload · 401 bad key · 409 duplicate phone (if dedupe enabled) · 500 internal error.
2.1.2 Search contact by phone or email¶
The
cs(contains) operator is required for thephones[]andemails[]array columns. Always URL-encode+as%2B(or use--data-urlencode).
curl -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/contacts' \
-H 'X-API-Key: cg_xxx' \
--data-urlencode 'phones=cs.{"+201017177777"}' \
--data-urlencode 'select=id,first_name,last_name,phones,emails,custom_fields' \
--data-urlencode 'limit=1'
By email:
curl -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/contacts' \
-H 'X-API-Key: cg_xxx' \
--data-urlencode 'emails=cs.{"mahmoud@example.com"}' \
--data-urlencode 'limit=1'
By custom field (JSONB equality):
curl -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/contacts' \
-H 'X-API-Key: cg_xxx' \
--data-urlencode 'custom_fields->>customer_type=eq.VIP' \
--data-urlencode 'limit=50'
Response 200 OK: array of contact objects (empty [] if no match).
2.1.3 Update contact¶
curl -X PATCH 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/contacts?id=eq.f2b4f7ab-7033-47ce-be06-b30a47de020a' \
-H 'X-API-Key: cg_xxx' \
-H 'Content-Type: application/json' \
-H 'Prefer: return=representation' \
-d '{
"first_name": "Mahmoud Updated",
"tags": ["vip", "premium"],
"custom_fields": { "customer_type": "Platinum", "last_order_value": 2400 }
}'
You can also update by phone:
curl -X PATCH -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/contacts' \
-H 'X-API-Key: cg_xxx' \
-H 'Content-Type: application/json' \
--data-urlencode 'phones=cs.{"+201017177777"}' \
--data '{"first_name":"Mahmoud Updated"}'
⚠️
custom_fieldsis replaced, not merged. To preserve existing keys, GET → merge client-side → PATCH.
2.1.4 Bulk create / upsert contacts¶
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/import-contacts-bulk' \
-H 'X-API-Key: cg_xxx' \
-H 'Content-Type: application/json' \
-d '{
"contacts": [
{
"first_name": "Ahmed",
"phones": ["+201111111111"],
"emails": ["ahmed@example.com"],
"tags": ["import-2026-04"],
"custom_fields": { "city": "Cairo" }
},
{
"first_name": "Sara",
"phones": ["+201222222222"],
"source": "csv-import"
}
],
"skip_duplicates": false,
"default_tags": ["batch-2026-04"],
"default_source": "csv-import"
}'
Response 200 OK:
{
"success": true,
"summary": { "total": 2, "successful": 2, "failed": 0, "new_contacts": 1, "updated_contacts": 1 },
"results": [
{ "index": 0, "success": true, "contact_id": "...", "is_new": false },
{ "index": 1, "success": true, "contact_id": "...", "is_new": true }
]
}
2.2 Request Fields¶
| Field | Type | Required | Description | Example | Validation |
|---|---|---|---|---|---|
first_name |
string | optional | Given name | "Mahmoud" |
≤ 100 chars |
last_name |
string | optional | Family name | "Ali" |
≤ 100 chars |
phones |
string[] | optional* | E.164 phone numbers | ["+201017177777"] |
Must start with +, 8–15 digits |
emails |
string[] | optional* | RFC 5322 emails | ["a@b.com"] |
Standard email regex |
source |
string | optional | Origin label (free text) | "website", "shopify", "api" |
≤ 50 chars |
tags |
string[] | optional | Free-form labels | ["vip","newsletter"] |
each ≤ 50 chars |
assignee_id |
uuid | optional | User who owns this contact | "f2b4..." |
Must exist in org |
company_id |
uuid | optional | Linked company record | "a1b2..." |
Must exist in org |
description |
string | optional | Notes / bio | "VIP customer since 2023" |
≤ 5000 chars |
opt_in_status |
bool | optional | Marketing consent (default true) |
true |
— |
custom_fields |
object | optional | Arbitrary JSON metadata | {"city":"Cairo"} |
Valid JSON, ≤ 32 KB |
* At least one of phones or emails is recommended for deduplication and outbound messaging.
2.3 Bulk Payload Structure¶
{
"contacts": [ /* contact objects, max 500 */ ],
"skip_duplicates": false,
"default_source": "csv-import",
"default_tags": ["batch-2026-04"]
}
| Behavior | Detail |
|---|---|
| Max batch size | 500 contacts per request |
| Atomicity | Per-row; failure of one row does not roll back others |
| Duplicate handling | Contacts are matched on email first, then on any phone (normalized to E.164). When a match is found, skip_duplicates: true skips the row and skip_duplicates: false (default) merges the incoming data into the existing contact |
| Idempotency | Re-sending the same payload → no duplicates created (matched rows are merged or skipped) |
| Partial success | summary.failed > 0 while summary.new_contacts/summary.updated_contacts > 0; inspect results[] for per-row errors |
3. Deals API¶
3.1 Endpoints¶
| Action | Method | Path |
|---|---|---|
| Create single deal | POST |
/rest-api-proxy/deals |
| Create lead (contact + deal) | POST |
/create-lead |
| Bulk create deals | POST |
/create-deals-bulk |
| Bulk create leads (contact + deal) | POST |
/create-leads-bulk |
| Get deal by ID | GET |
/rest-api-proxy/deals?id=eq.<uuid> |
| Search deals by title | GET |
/rest-api-proxy/deals?title=ilike.*Order* |
| Search deals by contact | GET |
/rest-api-proxy/deals?contact_id=eq.<uuid> |
| Update deal | PATCH |
/rest-api-proxy/deals?id=eq.<uuid> |
| Delete deal | DELETE |
/rest-api-proxy/deals?id=eq.<uuid> |
3.1.1 Create single deal¶
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/deals' \
-H 'X-API-Key: cg_xxx' \
-H 'Content-Type: application/json' \
-H 'Prefer: return=representation' \
-d '{
"title": "Order #68297792",
"value": 11523,
"currency": "SAR",
"stage": "qualified",
"contact_id": "f2b4f7ab-7033-47ce-be06-b30a47de020a",
"source": "shopify",
"expected_close_date": "2026-05-15",
"probability": 60,
"tags": ["online-order"],
"custom_fields": {
"shopify_order_id": "68297792",
"payment_method": "mada"
}
}'
⚠️
stagemust be lowercase and must match a stage ID configured on the pipeline (e.g."new","qualified","won","lost"). Uppercase values like"QUALIFIED"are accepted but will not render in the dashboard.
3.1.2 Bulk create deals¶
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/create-deals-bulk' \
-H 'X-API-Key: cg_xxx' \
-H 'Content-Type: application/json' \
-d '{
"default_currency": "SAR",
"default_stage": "qualified",
"skip_if_exists": true,
"deals": [
{
"title": "Order #68297792",
"value": 11523,
"contact_phone": "+201017177777",
"contact_first_name": "Mahmoud",
"create_contact_if_missing": true,
"custom_fields": { "order_id": "68297792" }
},
{
"title": "Order #68297793",
"value": 540,
"contact_email": "noha@example.com",
"contact_first_name": "Noha",
"create_contact_if_missing": true
}
]
}'
Response 200 OK:
{
"success": true,
"summary": {
"total": 2,
"successful": 2,
"failed": 0,
"new_deals": 2,
"skipped_existing": 0,
"new_contacts_created": 1
},
"results": [
{ "index": 0, "success": true, "deal_id": "...", "contact_id": "f2b4f7ab-...", "is_new_contact": false },
{ "index": 1, "success": true, "deal_id": "...", "contact_id": "...", "is_new_contact": true }
]
}
3.1.3 Search deals by title (substring match)¶
curl -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/deals' \
-H 'X-API-Key: cg_xxx' \
--data-urlencode 'title=ilike.*68297792*' \
--data-urlencode 'select=id,title,value,stage,contact_id' \
--data-urlencode 'limit=10'
| Operator | Meaning |
|---|---|
ilike.*foo* |
Case-insensitive substring (PostgreSQL ILIKE %foo%) |
like.*Foo* |
Case-sensitive substring |
eq.foo |
Exact match |
3.1.4 Update deal (e.g. mark as won)¶
curl -X PATCH 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/deals?id=eq.<deal-id>' \
-H 'X-API-Key: cg_xxx' \
-H 'Content-Type: application/json' \
-d '{ "stage": "won", "probability": 100 }'
Bulk-fix wrong stage casing:
curl -X PATCH -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/deals' \
-H 'X-API-Key: cg_xxx' \
-H 'Content-Type: application/json' \
--data-urlencode 'stage=eq.QUALIFIED' \
--data '{ "stage": "qualified" }'
3.2 Request Fields¶
| Field | Type | Required | Description | Example | Validation |
|---|---|---|---|---|---|
title |
string | required | Human-readable deal name | "Order #12345" |
1–200 chars |
value |
number | optional | Monetary value (numeric, no symbols) | 11523 |
≥ 0 |
currency |
string | optional | ISO 4217 code (default USD) |
"SAR", "EUR" |
3-letter code |
stage |
string | optional | Pipeline stage ID (lowercase) | "qualified" |
Must exist in pipeline.stages |
pipeline_id |
uuid | optional | Specific pipeline; falls back to default | "a1b2..." |
Must exist in org |
contact_id |
uuid | required* | Existing contact UUID | "f2b4..." |
Must exist in org |
contact_email |
string | required* | Lookup contact by email | "a@b.com" |
Bulk only |
contact_phone |
string | required* | Lookup contact by phone | "+201017177777" |
Bulk only, E.164 |
contact_first_name |
string | optional | Used when creating new contact | "Mahmoud" |
Bulk only |
contact_last_name |
string | optional | Used when creating new contact | "Ali" |
Bulk only |
create_contact_if_missing |
bool | optional | Auto-provision contact (default false) |
true |
Bulk only |
description |
string | optional | Notes | — | ≤ 5000 chars |
expected_close_date |
date | optional | ISO date | "2026-05-15" |
YYYY-MM-DD |
probability |
number | optional | Win probability 0–100 | 60 |
0–100 |
source |
string | optional | Origin label | "shopify", "api" |
≤ 50 chars |
tags |
string[] | optional | Free-form labels | ["online-order"] |
each ≤ 50 chars |
owner_id |
uuid | optional | Sales rep UUID | "u1..." |
Must exist in org |
company_id |
uuid | optional | Linked company | "c1..." |
Must exist in org |
custom_fields |
object | optional | Arbitrary JSON metadata | {"order_id":"123"} |
Valid JSON, ≤ 32 KB |
products |
object[] | optional | Line items | [{"name":"Sku1","quantity":2,"unit_price":50}] |
Optional schema |
* For /create-deals-bulk: provide at least one of contact_id, contact_email, or contact_phone.
3.3 Bulk Payload Structure¶
{
"deals": [ /* deal objects, max 100 */ ],
"default_pipeline_id": "uuid (optional)",
"default_stage": "qualified",
"default_currency": "SAR",
"skip_if_exists": true
}
| Behavior | Detail |
|---|---|
| Max batch size | 100 deals per request (HTTP 400 above this) |
| Contact resolution order | contact_id → contact_email → contact_phone |
| Linking vs creating | Match found → deal linked, contact NOT updated. No match + create_contact_if_missing: true → new contact created |
skip_if_exists |
Skips when a deal with the same title already exists for the resolved contact (returns existing deal_id, counted in skipped_existing) |
| Atomicity | Per-row; failures isolated, no rollback |
| Idempotency | Re-running with skip_if_exists: true is safe |
| Partial success | Always inspect summary.failed and per-row results[].error |
4. Custom Fields¶
custom_fields is a free-form JSONB object on both contacts and deals. It is the recommended way to attach business-specific data without modifying the core schema.
Where to send them¶
- Single create:
body.custom_fields - Bulk create:
body.deals[i].custom_fieldsorbody.contacts[i].custom_fields - Update: PATCH the whole
custom_fieldsobject (replaces existing — merge client-side first)
Naming rules¶
- Keys:
snake_case, ASCII alphanumerics + underscore, ≤ 64 chars. - Avoid keys starting with
_(reserved). - Total payload ≤ 32 KB.
Supported value types¶
| Type | Example |
|---|---|
| string | {"customer_type":"VIP"} |
| number | {"last_order_value":1200} |
| boolean | {"is_returning":true} |
| date (ISO string) | {"signup_date":"2026-04-22"} |
| array | {"interests":["sports","music"]} |
| nested object | {"address":{"city":"Cairo","zip":"11511"}} |
Pre-creation in dashboard¶
Custom fields do not need to be pre-defined to be stored. However, to make them visible as columns/filters in the CRM dashboard, an admin should register them in Settings → Custom Fields.
Example¶
{
"title": "Order #68297792",
"value": 11523,
"contact_id": "f2b4f7ab-...",
"custom_fields": {
"shopify_order_id": "68297792",
"payment_method": "mada",
"is_gift": true,
"delivered_at": "2026-04-25",
"shipping_address": { "city": "Riyadh", "zip": "12345" }
}
}
Querying by custom field¶
# String equality
?custom_fields->>customer_type=eq.VIP
# Numeric comparison (cast required)
?custom_fields->>last_order_value=gt.1000
# Existence
?custom_fields=cs.{"is_gift":true}
5. Validation Rules¶
Required fields¶
| Entity | Required |
|---|---|
| Contact | none strictly required, but at least one of phones[] / emails[] recommended |
| Deal (single) | title, contact_id |
| Deal (bulk) | title + one of contact_id / contact_email / contact_phone |
Phone format (E.164)¶
- Normalized server-side via the platform.
- Must contain only digits and an optional leading
+. - After normalization: 8–15 digits.
- Whitespace, dashes, parentheses, dots are stripped.
- Numbers without
+and ≥ 8 digits get+prepended. - Stored example:
"+201017177777".
| Input | Stored as |
|---|---|
"+20 101 717 7777" |
"+201017177777" |
"201017177777" |
"+201017177777" |
"abc" |
rejected |
⚠️ Provide numbers in proper E.164 with a leading
+and country code. A00international prefix is not auto-converted to+: an input like"00201017177777"simply gets a+prepended (yielding"+00201017177777"), which is not a valid E.164 number. Send"+201017177777"instead.
Email validation¶
- Regex:
^[^\s@]+@[^\s@]+\.[^\s@]{2,}$ - Lowercased before storage.
Duplicate detection¶
| Endpoint | Logic |
|---|---|
/import-contacts-bulk |
Matches on email first, then on any phone (normalized to E.164). skip_duplicates: true skips matched rows; otherwise matched rows are merged |
/create-deals-bulk |
When skip_if_exists: true, matches on (contact_id, title) |
rest-api-proxy/contacts POST |
No automatic dedupe; caller responsible (search → create) |
Linking rules¶
- A deal must link to exactly one
contact_id. Deals without a contact are not visible in the dashboard pipeline. - Multiple contacts on a deal: use the
deal_contactsrelation table (separate endpoint). pipeline_iddefaults to the org'sis_default = truepipeline; specifying an invalid pipeline returns400.
6. Best Practices¶
Upsert pattern (recommended)¶
1. SEARCH contact by phone or email
GET /rest-api-proxy/contacts?phones=cs.{"+201017177777"}&limit=1
2. If found → reuse contact_id
If not found → POST /rest-api-proxy/contacts
3. POST /rest-api-proxy/deals with the contact_id
For high-volume sources, use /create-deals-bulk with create_contact_if_missing: true and skip_if_exists: true — it does the upsert atomically per row.
Bulk processing¶
- Cap each request to the documented max (500 contacts / 100 deals).
- Send batches sequentially or with controlled concurrency (≤ 5 in flight) to avoid rate limits.
- Always inspect
results[]— never trust a200response alone.
Retry strategy¶
- Retry on
5xxand network errors with exponential backoff (1s, 2s, 4s, 8s, max 30s). - Do not retry on
4xx(bad payload) — fix and resend. - Use idempotency keys at the row level:
custom_fields.external_id+skip_if_exists: true.
Rate limits¶
- Soft guideline: 120 requests/minute per API key. Heavier loads may be throttled with HTTP
429. - Bulk endpoints count as one request regardless of batch size — prefer them for migrations.
Logging¶
- Log the full
resultsarray from bulk responses so you can replay failed rows. - Persist your own external ID → CRM ID mapping after each successful create.
Webhook integrations¶
- For real-time syncing, register a webhook on the source system (Shopify, Stripe, your form) → forward payload to a small adapter → call the CRM API.
- Include the original event ID in
custom_fields.external_idfor traceability.
7. Full Example Flows¶
7.1 Create a contact¶
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/contacts' \
-H 'X-API-Key: cg_xxx' -H 'Content-Type: application/json' \
-H 'Prefer: return=representation' \
-d '{
"first_name": "Mahmoud", "last_name": "Ali",
"phones": ["+201017177777"],
"emails": ["mahmoud@example.com"],
"source": "website",
"custom_fields": { "signup_campaign": "spring2026" }
}'
7.2 Update a contact (merge custom_fields safely)¶
# 1. Read current
curl -G '.../rest-api-proxy/contacts?id=eq.f2b4f7ab-...' \
-H 'X-API-Key: cg_xxx' --data-urlencode 'select=custom_fields'
# 2. Merge in your code, then PATCH
curl -X PATCH '.../rest-api-proxy/contacts?id=eq.f2b4f7ab-...' \
-H 'X-API-Key: cg_xxx' -H 'Content-Type: application/json' \
-d '{ "custom_fields": { "signup_campaign":"spring2026", "last_login":"2026-04-23" } }'
7.3 Create a deal linked by contact_id¶
curl -X POST '.../rest-api-proxy/deals' \
-H 'X-API-Key: cg_xxx' -H 'Content-Type: application/json' \
-H 'Prefer: return=representation' \
-d '{
"title": "Order #68297792",
"value": 11523, "currency": "SAR",
"stage": "qualified",
"contact_id": "f2b4f7ab-7033-47ce-be06-b30a47de020a",
"source": "shopify",
"custom_fields": { "shopify_order_id": "68297792" }
}'
7.4 Bulk create deals (with auto-contact creation)¶
curl -X POST '.../create-deals-bulk' \
-H 'X-API-Key: cg_xxx' -H 'Content-Type: application/json' \
-d '{
"default_currency": "SAR",
"default_stage": "qualified",
"skip_if_exists": true,
"deals": [
{
"title": "Order #68297792", "value": 11523,
"contact_phone": "+201017177777",
"contact_first_name": "Mahmoud",
"create_contact_if_missing": true,
"custom_fields": { "external_id": "68297792" }
},
{
"title": "Order #68297793", "value": 540,
"contact_email": "noha@example.com",
"contact_first_name": "Noha",
"create_contact_if_missing": true,
"custom_fields": { "external_id": "68297793" }
}
]
}'
7.5 Find a deal by external ID and mark it won¶
# 1. Find
curl -G '.../rest-api-proxy/deals' \
-H 'X-API-Key: cg_xxx' \
--data-urlencode 'custom_fields->>external_id=eq.68297792' \
--data-urlencode 'select=id,title,stage' --data-urlencode 'limit=1'
# 2. Update
curl -X PATCH '.../rest-api-proxy/deals?id=eq.<deal-id>' \
-H 'X-API-Key: cg_xxx' -H 'Content-Type: application/json' \
-d '{ "stage": "won", "probability": 100 }'
8. Error Reference¶
| HTTP | Body example | Cause | Fix |
|---|---|---|---|
400 |
{"error":"title is required"} |
Missing required field | Validate payload before send |
400 |
{"error":"Maximum batch size is 100 deals"} |
Bulk size exceeded | Chunk batches |
400 |
{"error":"No pipeline found..."} |
Org has no pipelines | Create one in dashboard or pass pipeline_id |
401 |
{"error":"Unauthorized"} |
Missing/invalid X-API-Key |
Re-issue key from Settings → API Keys |
404 |
[] empty array on GET |
No matching record | Verify filters (especially cs.{} syntax for arrays) |
409 |
{"error":"duplicate key..."} |
Unique constraint hit | Use upsert / skip_if_exists |
429 |
{"error":"rate limit"} |
Too many requests | Backoff and reduce concurrency |
500 |
{"error":"Internal server error","details":"..."} |
Server-side issue | Retry with backoff; report details |
Common gotchas¶
| Symptom | Likely cause |
|---|---|
| Deal created but invisible in dashboard | stage was uppercase ("QUALIFIED") — must be lowercase |
malformed array literal: "{ +201..." |
+ not URL-encoded — use --data-urlencode or %2B |
is_new_contact: true for known phone |
The phone in storage uses a different format — normalize to E.164 with + |
custom_fields got wiped after PATCH |
PATCH replaces the object — read-merge-write |
Bulk response 200 but rows missing |
Check summary.failed and results[].error |