Skip to content

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

  1. Overview
  2. Contacts API
  3. 2.1 Endpoints
  4. 2.2 Request Fields
  5. 2.3 Bulk Payload Structure
  6. Deals API
  7. 3.1 Endpoints
  8. 3.2 Request Fields
  9. 3.3 Bulk Payload Structure
  10. Custom Fields
  11. Validation Rules
  12. Best Practices
  13. Full Example Flows
  14. 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:

X-API-Key: cg_your_api_key_here
Content-Type: application/json

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 the phones[] and emails[] 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_fields is 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"
 }
 }'

⚠️ stage must 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_idcontact_emailcontact_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_fields or body.contacts[i].custom_fields
  • Update: PATCH the whole custom_fields object (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. A 00 international 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_contacts relation table (separate endpoint).
  • pipeline_id defaults to the org's is_default = true pipeline; specifying an invalid pipeline returns 400.

6. Best Practices

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 a 200 response alone.

Retry strategy

  • Retry on 5xx and 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 results array 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_id for 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