API reference
Overview
A JSON API over a workspace: contacts, lists, prospects, campaigns, sequences, the inbox, senders, signals and the blocklist. Every endpoint is also an MCP tool with the same arguments and the same rules, so what an assistant can do, your code can do.
Base URL
https://app.trygtm.com/api/v1The version is in the path. A breaking change ships as /v2; /v1 keeps its contract. Additive changes — a new endpoint, a new optional parameter, a new response field — ship in place, so ignore fields you do not recognise. An OpenAPI 3.1 document for everything below is served at https://app.trygtm.com/api/v1/openapi.json, without a key.
Authentication
An API key in an Authorization header. Mint one in the app under Settings → API; it starts with tg_live_ and is shown once, because only its hash is stored.
curl https://app.trygtm.com/api/v1/workspace \
-H "Authorization: Bearer $TRYGTM_API_KEY"A key is the organization's credential, not a person's: it keeps working when whoever minted it leaves, and revoking it takes effect on the next request. Three things about a key are frozen when it is created, because a key already in somebody's CI cannot be re-consented:
- Scopes.
readcovers everyGET.writecovers everything that changes data or spends credits. Each endpoint page says which it needs; a key without it gets403 insufficient_scope. - Workspace. Agency-wide, or pinned to one client workspace. See below.
- Expiry. Optional. An expired key answers
401like a revoked one.
The API requires an active subscription. Without one every call answers 402 subscription_required.
Workspaces
A workspace is the isolation boundary the whole data model is built on: contacts, campaigns, senders, conversations and the blocklist all belong to exactly one. An organization on the Agency plan has its own agency workspace plus one per client.
- A pinned key acts inside one client workspace and can never read another. This is the key to hand a client, or a script that serves one.
- An agency key acts in the agency workspace by default, and inside a client workspace when the request carries
X-Workspace-Id. List the ids with List client workspaces.
curl https://app.trygtm.com/api/v1/contacts \
-H "Authorization: Bearer $TRYGTM_API_KEY" \
-H "X-Workspace-Id: 7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f"Requests and responses
Bodies are JSON with Content-Type: application/json, and every field is snake_case. A success is wrapped in data; a paged list carries next_cursor beside it. Every response has an X-Request-Id header — quote it when you write to support.
Parameters are validated before anything runs. A parameter the endpoint does not take is refused rather than ignored, so a typo in a filter cannot quietly return the unfiltered list.
Errors
Errors return the HTTP status plus a stable machine-readable code. Branch on code, never on the message — messages are written for people and get rewritten.
| Code | Status | Meaning |
|---|---|---|
invalid_request | 400 | A parameter is missing, malformed, or not one this endpoint takes. field names it. |
unauthorized | 401 | No API key, or the key is revoked, expired or not valid. |
subscription_required | 402 | The organization has no active subscription, so the API is closed until billing is resolved. |
quota_exhausted | 402 | The credit balance, or the client workspace's credit limit, cannot cover this call. |
insufficient_scope | 403 | The key is valid but was minted without the scope this endpoint needs. required_scope names it. |
forbidden | 403 | The key may not do this here: an organization-level endpoint called from a client workspace, or a row that cannot be changed programmatically. |
workspace_forbidden | 403 | The key is pinned to one client workspace and X-Workspace-Id named another. |
not_found | 404 | No such resource in this workspace, or no such endpoint. |
workspace_not_found | 404 | X-Workspace-Id does not name an active client workspace of this organization. |
method_not_allowed | 405 | The path exists but not with this method. Allow lists the ones it takes. |
conflict | 409 | The request is valid but the resource is not in a state that allows it: a duplicate, a message already sent, a campaign missing what activation needs. |
suppressed | 409 | The address or its domain is on a suppression list, so the contact was not written. |
idempotency_in_progress | 409 | A request with this Idempotency-Key started and has not finished, or never reported back. It is not run again. |
idempotency_key_reused | 422 | This Idempotency-Key was already used with a different method, path or body. |
rate_limited | 429 | Too many requests. Wait the number of seconds Retry-After says. |
internal_error | 500 | Something failed on our side. Safe to retry a read; retry a write with its Idempotency-Key. |
unavailable | 503 | A vendor the call depends on did not answer. Retry later. |
Error shape
{
"error": {
"code": "invalid_request",
"message": "min_heat must be at most 5.",
"field": "min_heat",
"issues": [{ "field": "min_heat", "message": "must be at most 5." }]
}
}field and issues appear on validation errors. Some conflicts carry the id you were about to duplicate — creating a contact that already exists answers 409 conflict with its contact_id.
Pagination
Cursor-based, newest first. A paged response carries next_cursor, which is opaque and either a string or null. Pass it back as cursor with every other parameter unchanged; null means you have reached the end. Do not construct one, and do not assume offsets — contacts and conversations are written every minute by the runtime, so an offset walk would skip and repeat rows. limit is 1 to 100 and defaults to 25.
let cursor: string | null = null;
do {
const params = new URLSearchParams({ limit: "100", ...(cursor ? { cursor } : {}) });
const response = await fetch(`https://app.trygtm.com/api/v1/contacts?${params}`, {
headers: { Authorization: `Bearer ${process.env.TRYGTM_API_KEY}` },
});
const page = await response.json();
for (const contact of page.data) handle(contact);
cursor = page.next_cursor;
} while (cursor);Idempotency
Send an Idempotency-Key header on any write. A retry carrying the same key returns the first response, with Idempotent-Replayed: true, instead of running again. This matters most where a write spends credits — importing or enriching prospects — and a timeout would otherwise leave you choosing between paying twice and not knowing.
- Keys are scoped to the API key and workspace, and kept for 24 hours.
- The same key with a different method, path or body answers
422 idempotency_key_reused. - A key whose first request started and never finished answers
409 idempotency_in_progress. It is not run again, because the write may already have happened: check, then retry with a new key. - A request refused by validation does not consume its key.
Rate limits
120 requests a minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window closes). A 429 carries Retry-After in seconds; honour it rather than backing off on a schedule of your own.
Credits
A write through the API spends the same credits the same action spends in the app, from the same balance, and is refused with 402 quota_exhausted when the balance or a client workspace's limit cannot cover it. Endpoints that spend say so at the top of their page. Most do not: reading, creating contacts, editing campaigns and managing the blocklist are free. List credit costs returns the current price list.
What the API deliberately does not do
The API covers what an operator does in the app day to day. A few things are left out on purpose, and knowing which saves looking for them.
- Sending a message
- Every outbound message goes through the launcher: the send window, the sender's ramp, the suppression check and the sender's limits. The API can write a reply and queue it, approve a step, or activate a campaign. The launcher does the sending.
- Starting a conversation
- A thread opens when a campaign step leaves or the contact writes in. A first message with no campaign behind it would be cold outreach with nothing deciding when, from whom or how often.
- Billing, plans and credit purchases
- Checkout and the customer portal need a person in a browser. Balance and usage are readable.
- Members, invites and roles
- Who may act as the organization is decided by a signed-in owner or admin, not by a credential that acts as the organization.
- API keys and webhook secrets
- A key that could mint keys could outlive its own revocation. Secrets are shown once, in the app.
- Connecting a LinkedIn account or a mailbox
- Those are OAuth and hosted-auth flows in a browser. Attaching and verifying a sending domain is available.
- Buying a domain
- It charges a card. Search and purchase stay in Settings → Senders.