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

  1. 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.
  2. Request your contacts:
Request
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.

NameTypeDescription
contacts:readRead contactsGET /contacts, GET /contacts/{id}, GET /fields
contacts:writeRead and write contactsEverything 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.

Contact
{
  "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"
}
NameTypeDescription
idstring (uuid)The contact's id.
displayNamestringThe name to show people: their given name, with the profile name beside it.
name, email, phonestring | nullBuilt-in details. Phone numbers keep the country code when given.
profileNamestring | nullTheir Facebook or Instagram name.
sourcestringWhere the contact came from, e.g. "Messenger", "Instagram DM", "Telegram".
platform"messenger" | "instagram" | nullThe channel the conversation happened on.
statusstring"New lead", "Qualified", or "Needs follow-up".
scorenumber | nullThe assistant's 0–100 qualification score.
fieldsarrayEvery captured detail beyond name, email, and phone: key, label, type, value.
valuesobjectEverything as one flat object keyed by field key. The easiest thing to parse.
conversationIdstring | nullThe conversation the details came from.
lastCapturedAtstring | nullWhen a detail last changed.
createdAt, updatedAtstringISO 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.

NameTypeDescription
textstringAnything, trimmed.
numberstringDigits only when it is a plain number, e.g. "25000".
datestringYYYY-MM-DD when the lead gave a full date.
addressstringA full address, as the lead wrote it.
vinstringUppercase, spaces and dashes removed, e.g. 1HGCM82633A004352.
emailstringLowercased.
phonestringAs given, with the country code when provided.
urlstringA 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.

NameTypeDescription
limitinteger1–100. Default 50.
updated_sinceISO 8601 date-timeOnly contacts created or changed after this time.
cursorstringThe nextCursor from the previous page.
emailstringOnly the contact with this email.
phonestringOnly the contact with this phone number.
Response
{
  "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.

Request
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.

NameTypeDescription
namestringTheir name.
emailstringA valid email address.
phonestringInclude the country code when you have it.
sourcestringShown on new contacts, e.g. "WhatsApp". Default "API".
fieldsobjectField keys to values: { "vin": "1HG…" }, or { "vin": { "value": "1HG…", "label": "VIN", "type": "vin" } }. Known keys use the assistant's label and type.
Request
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.

Response
{
  "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." } }
NameTypeDescription
400 invalid_requestA parameter or body field is missing or malformed. The message says which.
401 invalid_api_keyThe key is missing, mistyped, or revoked.
402 plan_inactiveThe workspace's trial or plan has ended.
403 insufficient_scopeThe key can't do this — writing needs contacts:write.
404 not_foundNo contact with that id in this workspace.
503 server_errorSomething 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.

telegram_leads.py
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.

discord-leads.mjs
// 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.

JavaScript
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.