Developers
Triton Relay API
Give your other tools — Telegram bots, Discord servers, WhatsApp flows, CRMs, spreadsheets — the leads your assistants capture. Every detail comes back keyed, labelled, and typed, so it parses the same way every time.
Base URL: https://www.tritonrelay.app/api/v1
Quick start
- In your dashboard, open Settings → Developer API and generate a key. Choose Read contacts, or Read and write contacts if your app will add details too. The key is shown once — store it as a secret.
- Request your contacts:
curl "https://www.tritonrelay.app/api/v1/contacts?limit=10" \
-H "Authorization: Bearer rly_your_key"Each contact has a flat values object — values.vin, values.car — and a fields array with each detail's label and type.
Authentication
Send your key in the Authorization header as a bearer token. X-API-Key: rly_… also works. Keys belong to one workspace and only reach that workspace's contacts. Use them from servers, never from a browser or mobile app where people can read them.
| Name | Type | Description |
|---|---|---|
| contacts:read | Read contacts | GET /contacts, GET /contacts/{id}, GET /fields |
| contacts:write | Read and write contacts | Everything above, plus POST /contacts and PATCH /contacts/{id} |
Requests only succeed while the workspace's trial or plan is active. Revoke a key in Settings the moment it may have leaked; it stops working immediately.
The contact object
A contact is built up as a lead shares details. The first detail creates it under their Facebook or Instagram name; a real name, email, and custom fields are added as they arrive.
{
"object": "contact",
"id": "5b1f0c7e-2d4a-4f7e-9c1a-8e3b6d2f4a10",
"displayName": "Kofi Mensah (Kofi M)",
"name": "Kofi Mensah",
"profileName": "Kofi M",
"email": "kofi@example.com",
"phone": "+233245550199",
"source": "Messenger",
"platform": "messenger",
"status": "Qualified",
"score": 82,
"fields": [
{ "key": "address", "label": "Address", "type": "address", "value": "12 Oxford St, Osu, Accra" },
{ "key": "vin", "label": "VIN", "type": "vin", "value": "1HGCM82633A004352" },
{ "key": "car", "label": "Car", "type": "text", "value": "Honda Accord 2019" },
{ "key": "color", "label": "Color", "type": "text", "value": "Silver" }
],
"values": {
"name": "Kofi Mensah",
"email": "kofi@example.com",
"phone": "+233245550199",
"address": "12 Oxford St, Osu, Accra",
"vin": "1HGCM82633A004352",
"car": "Honda Accord 2019",
"color": "Silver"
},
"conversationId": "c2a7e0d4-91b3-4c55-8f0e-3a6d1b9e7f22",
"lastCapturedAt": "2026-09-14T18:22:05.114Z",
"createdAt": "2026-09-14T18:20:41.902311+00:00",
"updatedAt": "2026-09-14T18:22:05.130244+00:00"
}| Name | Type | Description |
|---|---|---|
| id | string (uuid) | The contact's id. |
| displayName | string | The name to show people: their given name, with the profile name beside it. |
| name, email, phone | string | null | Built-in details. Phone numbers keep the country code when given. |
| profileName | string | null | Their Facebook or Instagram name. |
| source | string | Where the contact came from, e.g. "Messenger", "Instagram DM", "Telegram". |
| platform | "messenger" | "instagram" | null | The channel the conversation happened on. |
| status | string | "New lead", "Qualified", or "Needs follow-up". |
| score | number | null | The assistant's 0–100 qualification score. |
| fields | array | Every captured detail beyond name, email, and phone: key, label, type, value. |
| values | object | Everything as one flat object keyed by field key. The easiest thing to parse. |
| conversationId | string | null | The conversation the details came from. |
| lastCapturedAt | string | null | When a detail last changed. |
| createdAt, updatedAt | string | ISO 8601 timestamps. |
Fields and types
Fields are the details an assistant collects. In the assistant editor you create them — or have Triton Relay find them in instructions like “ask for the customer's name, address, VIN, car and color” — and reference them as placeholders like {{vin}}. A field's key never changes after it's created, even if its label is renamed, so your integration can rely on it.
| Name | Type | Description |
|---|---|---|
| text | string | Anything, trimmed. |
| number | string | Digits only when it is a plain number, e.g. "25000". |
| date | string | YYYY-MM-DD when the lead gave a full date. |
| address | string | A full address, as the lead wrote it. |
| vin | string | Uppercase, spaces and dashes removed, e.g. 1HGCM82633A004352. |
| string | Lowercased. | |
| phone | string | As given, with the country code when provided. |
| url | string | A web link. |
Values are always strings, so parse numbers and dates on your side.
List contacts
GET/api/v1/contacts
Returns contacts newest first. With updated_since, returns contacts changed after that time, oldest first — ideal for syncing.
| Name | Type | Description |
|---|---|---|
| limit | integer | 1–100. Default 50. |
| updated_since | ISO 8601 date-time | Only contacts created or changed after this time. |
| cursor | string | The nextCursor from the previous page. |
| string | Only the contact with this email. | |
| phone | string | Only the contact with this phone number. |
{
"object": "list",
"data": [ { "object": "contact", "id": "…", "values": { "name": "Kofi Mensah", "vin": "1HGCM82633A004352" } } ],
"hasMore": true,
"nextCursor": "WyIyMDI2LTA5LTE0VDE4OjIyOjA1…"
}Get a contact
GET/api/v1/contacts/{id}
Add ?include=history to see every change to the contact — which field, old and new values, where it came from, and when.
curl "https://www.tritonrelay.app/api/v1/contacts/5b1f0c7e-2d4a-4f7e-9c1a-8e3b6d2f4a10?include=history" \
-H "Authorization: Bearer rly_your_key"Create or update a contact
POST/api/v1/contacts
Requires contacts:write. If a contact with the same email or phone exists, it's updated (200); otherwise a new contact is created (201). Send details from your own Telegram, WhatsApp, or Discord bot so every lead lands in one place. Changes appear in the contact's history.
| Name | Type | Description |
|---|---|---|
| name | string | Their name. |
| string | A valid email address. | |
| phone | string | Include the country code when you have it. |
| source | string | Shown on new contacts, e.g. "WhatsApp". Default "API". |
| fields | object | Field keys to values: { "vin": "1HG…" }, or { "vin": { "value": "1HG…", "label": "VIN", "type": "vin" } }. Known keys use the assistant's label and type. |
curl -X POST "https://www.tritonrelay.app/api/v1/contacts" \
-H "Authorization: Bearer rly_your_write_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Ama Owusu",
"phone": "+233201234567",
"source": "WhatsApp",
"fields": { "vin": "1hgcm82633a004352", "car": "Toyota Corolla", "color": "Blue" }
}'The response is the saved contact. Values are tidied by field type: when one of your assistants defines vin as a VIN field, values.vin comes back as 1HGCM82633A004352. Keys no assistant defines are saved as text — send { "value": "…", "type": "vin" } to set the type yourself.
Update a contact
PATCH/api/v1/contacts/{id}
Requires contacts:write. Takes the same body as creating a contact and only changes what you send. Details can't be erased through the API — send a new value to replace one.
List fields
GET/api/v1/fields
Every field your assistants collect, so your app can build forms or column mappings automatically.
{
"object": "list",
"data": [
{ "key": "name", "label": "Name", "type": "text", "builtIn": true, "assistants": ["Car rentals"] },
{ "key": "vin", "label": "VIN", "type": "vin", "builtIn": false, "assistants": ["Car rentals"] }
]
}Syncing new leads
Poll GET /contacts?updated_since=… every minute or so. Page through with nextCursor while hasMore is true, keeping updated_since the same, then save the updatedAt of the last contact you processed and use it as the next updated_since. A contact that gains a new detail shows up again with its latest values.
Errors
Errors return a non-2xx status and a JSON body you can show or log:
{ "error": { "code": "insufficient_scope", "message": "This API key can only read contacts. Create a key with read and write access." } }| Name | Type | Description |
|---|---|---|
| 400 invalid_request | A parameter or body field is missing or malformed. The message says which. | |
| 401 invalid_api_key | The key is missing, mistyped, or revoked. | |
| 402 plan_inactive | The workspace's trial or plan has ended. | |
| 403 insufficient_scope | The key can't do this — writing needs contacts:write. | |
| 404 not_found | No contact with that id in this workspace. | |
| 503 server_error | Something failed on our side. Retry with a short delay. |
Telegram, Discord, WhatsApp
Send new leads to a Telegram chat (Python)
Create a bot with @BotFather, add it to your group, and run this with TRITON_API_KEY, TELEGRAM_BOT_TOKEN, and TELEGRAM_CHAT_ID set.
import os, time, requests
API = "https://www.tritonrelay.app/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TRITON_API_KEY']}"}
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"] # from @BotFather
CHAT_ID = os.environ["TELEGRAM_CHAT_ID"] # the chat that gets new leads
since = "2026-01-01T00:00:00Z" # store this somewhere durable in production
while True:
cursor, newest = None, since
while True:
params = {"updated_since": since, "limit": 100}
if cursor:
params["cursor"] = cursor
page = requests.get(f"{API}/contacts", headers=HEADERS, params=params, timeout=30).json()
for contact in page["data"]:
lines = [f"Lead: {contact['displayName']}"]
lines += [f"{label}: {value}" for label, value in
(("Phone", contact["phone"]), ("Email", contact["email"])) if value]
lines += [f"{field['label']}: {field['value']}" for field in contact["fields"]]
requests.post(f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
json={"chat_id": CHAT_ID, "text": "\n".join(lines)}, timeout=30)
newest = contact["updatedAt"]
if not page["hasMore"]:
break
cursor = page["nextCursor"]
since = newest
time.sleep(60)Post leads to a Discord channel (Node.js)
In Discord, open the channel's Integrations → Webhooks, copy the URL, and set it as DISCORD_WEBHOOK_URL.
// Node 18+. Posts each new or updated lead to a Discord channel webhook.
const API = "https://www.tritonrelay.app/api/v1";
const headers = { Authorization: `Bearer ${process.env.TRITON_API_KEY}` };
let since = new Date().toISOString();
async function sync() {
let cursor = null;
let newest = since;
do {
const url = new URL(`${API}/contacts`);
url.searchParams.set("updated_since", since);
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const page = await (await fetch(url, { headers })).json();
for (const contact of page.data) {
await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
embeds: [{
title: contact.displayName,
fields: Object.entries(contact.values).map(([key, value]) => ({
name: contact.fields.find((f) => f.key === key)?.label ?? key,
value,
inline: true,
})),
}],
}),
});
newest = contact.updatedAt;
}
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);
since = newest;
}
setInterval(sync, 60_000);Save details your WhatsApp bot collects
When your WhatsApp (or Telegram, or Discord) bot collects a customer's details, send them with a contacts:write key. Matching on phone number keeps one contact per person, so details from Messenger and WhatsApp end up together.
await fetch("https://www.tritonrelay.app/api/v1/contacts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TRITON_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
phone: message.from, // e.g. "+233201234567"
name: profile.name,
source: "WhatsApp",
fields: { address: answers.address, vin: answers.vin, car: answers.car, color: answers.color },
}),
});Need a custom integration built for you? Open Full customization in your dashboard, or email support@tritonrelay.app.