Gmail integration (API v1)
Gmail Integration API (v1)
Guide for CLI tools and external integrators syncing Gmail with CustomDesigner / MioTees.
There are two related resources:
| Resource | Table | Purpose |
|---|---|---|
| Thread context | gmail_thread_contexts |
CRM cache: linked orders, clients, participant metadata for inbox UI |
| Email thread | email_threads |
Full thread payload: subject, labels, participants, and all messages from Gmail |
Use thread context when you need order/client matching. Use email threads when you are pushing mail content from the CLI into the database.
Prerequisites
- Developer API token — Settings → Developer → Generate (64 hex characters, shown once).
- Migration applied in Supabase:
migrations/20260604_gmail_thread_contexts.sql(thread context reads)migrations/20260605_email_threads.sql(email thread upserts)
- Base URL
- Production:
https://<your-site> - Local:
http://localhost:3000
- Production:
All paths below are under /api/v1.
Authentication
Every request:
Authorization: Bearer <64_character_hex_token>
Content-Type: application/json
- The token is scoped to one company (set when the token was created).
- Do not send
company_idin the body; the server assigns it from the token. - Errors use:
{
"error": {
"code": "unauthenticated",
"message": "…",
"details": null
}
}
Common codes: unauthenticated, forbidden, invalid_request, not_found, rate_limited, internal_error.
1. Gmail thread context (read CRM cache)
GET /api/v1/gmail/threads/{legacyThreadId}
Returns a row from gmail_thread_contexts for a Gmail legacy thread id (the id your CLI/extension already uses).
Request
curl -sS "https://YOUR_SITE/api/v1/gmail/threads/LEGACY_THREAD_ID" \
-H "Authorization: Bearer YOUR_TOKEN"
| Parameter | Location | Required | Description |
|---|---|---|---|
legacyThreadId |
path | yes | Gmail thread id |
No query parameters.
Response 200
{
"thread_id": "…",
"legacy_thread_id": "…",
"sender_email": "customer@example.com",
"client_id": "42",
"client_data": { },
"clients_data": [ ],
"orders_data": [ ],
"selected_order_id": "101",
"participant_emails": [ "a@example.com", "b@example.com" ],
"participants_data": [ ],
"fetched_at": "2026-06-04T12:00:00.000Z",
"updated_at": "2026-06-04T12:00:00.000Z"
}
Fields are stored as JSON in Supabase; shapes depend on what wrote the cache (extension, internal jobs, etc.).
Access control
Returns 404 if:
- No row exists for that
legacy_thread_id, or - The row is not tied to your company (via
client_id, order ids inorders_data, orcompany_idinside cached JSON).
This prevents cross-tenant reads when the table has no company_id column.
Typical CLI flow
- User opens a Gmail thread in the CLI.
- CLI calls this endpoint with the thread’s legacy id.
- CLI uses
orders_data/client_data/participants_datato show linked CRM records or suggest matches.
2. Email threads (upsert full thread + messages)
PUT /api/v1/gmail/email-threads/{legacyThreadId}
Creates or updates a row in email_threads for the authenticated company and mailbox.
Conflict key: (company_id, account_email, legacy_thread_id) — same legacyThreadId with a different account_email is a separate row.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
account_email |
string | yes | Gmail mailbox that was synced (normalized to lowercase) |
messages |
array | yes | Message objects from Gmail (use [] if empty) |
participants |
array | no | Default [] — people on the thread |
labels |
array | no | Default [] — Gmail label ids/names |
subject |
string | no | Thread subject |
snippet |
string | no | Short preview for lists |
gmail_thread_id |
string | no | Canonical Gmail thread id if different from legacy |
first_message_at |
ISO string | no | Earliest message time |
last_message_at |
ISO string | no | Latest message time |
gmail_history_id |
string | no | For incremental sync |
last_synced_at |
ISO string | no | Defaults to server now() |
Example: upsert from CLI
curl -sS -X PUT "https://YOUR_SITE/api/v1/gmail/email-threads/LEGACY_THREAD_ID" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"account_email": "shop@yourcompany.com",
"subject": "Re: Quote #4421",
"snippet": "Thanks, can we add 12 more shirts?",
"participants": [
{ "email": "customer@example.com", "name": "Jane Customer" },
{ "email": "shop@yourcompany.com", "name": "Your Shop" }
],
"labels": ["INBOX", "IMPORTANT"],
"messages": [
{
"id": "MSG_ID_1",
"internal_date": "2026-06-01T14:22:00.000Z",
"from": "customer@example.com",
"to": ["shop@yourcompany.com"],
"subject": "Quote #4421",
"snippet": "Hi, we need 48 tees…",
"body_plain": "Hi, we need 48 tees…",
"body_html": "<p>Hi, we need 48 tees…</p>"
},
{
"id": "MSG_ID_2",
"internal_date": "2026-06-04T09:15:00.000Z",
"from": "customer@example.com",
"to": ["shop@yourcompany.com"],
"subject": "Re: Quote #4421",
"snippet": "Thanks, can we add 12 more shirts?",
"body_plain": "Thanks, can we add 12 more shirts?",
"body_html": null
}
],
"first_message_at": "2026-06-01T14:22:00.000Z",
"last_message_at": "2026-06-04T09:15:00.000Z",
"gmail_history_id": "987654",
"gmail_thread_id": "LEGACY_THREAD_ID"
}'
Response 200
{
"email_thread": {
"id": "uuid",
"account_email": "shop@yourcompany.com",
"legacy_thread_id": "LEGACY_THREAD_ID",
"gmail_thread_id": "LEGACY_THREAD_ID",
"subject": "Re: Quote #4421",
"snippet": "Thanks, can we add 12 more shirts?",
"participants": [ ],
"labels": [ "INBOX", "IMPORTANT" ],
"messages": [ ],
"message_count": 2,
"first_message_at": "2026-06-01T14:22:00.000Z",
"last_message_at": "2026-06-04T09:15:00.000Z",
"gmail_history_id": "987654",
"last_synced_at": "2026-06-04T10:00:00.000Z",
"created_at": "2026-06-04T09:00:00.000Z",
"updated_at": "2026-06-04T10:00:00.000Z"
}
}
message_count is derived from messages.length in the database trigger.
Upsert semantics
- First call for
(company, account_email, legacy_thread_id)→ insert. - Repeat calls → update the same row (full replace of JSON arrays and scalar fields you send).
deletedis set back tofalseon upsert (restore after soft-delete, if you add that later in-app).- Send the complete
messagesarray each sync; omitted keys are not merged field-by-field.
Read back (optional)
GET /api/v1/gmail/email-threads/{legacyThreadId}?account_email=shop@yourcompany.com
curl -sS -G "https://YOUR_SITE/api/v1/gmail/email-threads/LEGACY_THREAD_ID" \
-H "Authorization: Bearer YOUR_TOKEN" \
--data-urlencode "account_email=shop@yourcompany.com"
Returns the same { "email_thread": { … } } wrapper. 404 if not found for that company + mailbox + thread id.
Suggested message object shape (CLI)
The API does not validate per-message schema; store consistent objects for your own tooling.
| Field | Type | Notes |
|---|---|---|
id |
string | Gmail message id |
internal_date |
ISO string | Message time |
from |
string | Sender email or "Name <email>" |
to |
string[] | Recipients |
cc |
string[] | Optional |
subject |
string | Optional per message |
snippet |
string | Short preview |
body_plain |
string | Plain body |
body_html |
string | HTML body, nullable |
End-to-end CLI workflow
- Pull CRM context —
GET …/gmail/threads/{legacyThreadId}to show linked orders/clients. - Push mail snapshot —
PUT …/gmail/email-threads/{legacyThreadId}after fetching thread/messages from Gmail API. - Verify (optional) —
GET …/gmail/email-threads/{legacyThreadId}?account_email=….
Rate limits and docs
- ~300 requests/minute per API key.
- Interactive OpenAPI: /docs/api/explorer
- OpenAPI YAML:
/openapi.yaml
Related files in this repo
| File | Role |
|---|---|
server/api/v1/gmail/threads/[legacyThreadId].get.ts |
Thread context read |
server/api/v1/gmail/email-threads/[legacyThreadId].put.ts |
Email thread upsert |
server/api/v1/gmail/email-threads/[legacyThreadId].get.ts |
Email thread read |
migrations/20260604_gmail_thread_contexts.sql |
Context table |
migrations/20260605_email_threads.sql |
Email threads table |