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

  1. Developer API token — Settings → Developer → Generate (64 hex characters, shown once).
  2. Migration applied in Supabase:
    • migrations/20260604_gmail_thread_contexts.sql (thread context reads)
    • migrations/20260605_email_threads.sql (email thread upserts)
  3. Base URL
    • Production: https://<your-site>
    • Local: http://localhost:3000

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_id in 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 in orders_data, or company_id inside cached JSON).

This prevents cross-tenant reads when the table has no company_id column.

Typical CLI flow

  1. User opens a Gmail thread in the CLI.
  2. CLI calls this endpoint with the thread’s legacy id.
  3. CLI uses orders_data / client_data / participants_data to 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).
  • deleted is set back to false on upsert (restore after soft-delete, if you add that later in-app).
  • Send the complete messages array 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

  1. Pull CRM contextGET …/gmail/threads/{legacyThreadId} to show linked orders/clients.
  2. Push mail snapshotPUT …/gmail/email-threads/{legacyThreadId} after fetching thread/messages from Gmail API.
  3. 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

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