> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tied.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API channel

> Build the channel Tied doesn't ship — your webhook carries the outbound leg, three endpoints carry the inbound one.

Not every surface a customer messages you from is one Tied has a native connector for — an in-app chat you already built, a kiosk, a proprietary device, a national messenger Tied hasn't integrated yet. The API channel is how you wire any of those into the same inbox, routing and AI agent as every native channel, using the same building blocks Instagram or Telegram use internally: a channel record, a webhook, and a set of endpoints for pushing messages in.

## What it actually is

A `Channel::Api` is deliberately thin. Creating one gives you:

| Field | What it's for |
| - | - |
| **`identifier`** | A long random token Tied generates. It's the URL segment that says which channel a request is for — treat it like a secret, not a slug you pick. |
| **`hmac_token`** | A second random secret, used only to sign identity proofs (below). |
| **`webhook_url`** | Where Tied sends you everything that happens on this inbox. |
| **`hmac_mandatory`** | Off by default. Flip it once you're ready to require identity verification on every request. |

There's no transport built in, on purpose — you supply both directions.

<img src="https://mintcdn.com/tied/EKg57bTFCkV_E_Je/images/diagrams/api-channel-legs-light.svg?fit=max&auto=format&n=EKg57bTFCkV_E_Je&q=85&s=020627162e03a89eab121621dab492f4" alt="The API channel's two legs: three inbound endpoints your server calls, and an outbound webhook Tied calls." className="block dark:hidden w-full my-8" noZoom width="900" height="380" data-path="images/diagrams/api-channel-legs-light.svg" />

<img src="https://mintcdn.com/tied/EKg57bTFCkV_E_Je/images/diagrams/api-channel-legs-dark.svg?fit=max&auto=format&n=EKg57bTFCkV_E_Je&q=85&s=dfb31069aba09f3a68f413d23bfcf5e1" alt="The API channel's two legs: three inbound endpoints your server calls, and an outbound webhook Tied calls." className="hidden dark:block w-full my-8" noZoom width="900" height="380" data-path="images/diagrams/api-channel-legs-dark.svg" />

## The inbound leg: three endpoints

Your server calls these to represent, inside Tied, whatever arrived on your real surface. No `api_access_token` header here — the URL itself carries the identity:

```bash theme={null}
POST /public/api/v1/inboxes/{inbox_identifier}/contacts
POST /public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations
POST /public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversations/{conversation_id}/messages
```

`inbox_identifier` is your channel's `identifier`. `contact_identifier` is whatever ID you assign that person on your own surface — a device ID, an internal user ID, anything stable — scoped to this one channel. A minimal round trip:

```bash theme={null}
# 1. Create the contact
curl -X POST https://app.tied.com/public/api/v1/inboxes/$IDENTIFIER/contacts \
  -H "Content-Type: application/json" \
  -d '{"identifier": "user_42", "name": "Alice", "email": "alice@acme.inc"}'

# 2. Start a conversation for them
curl -X POST https://app.tied.com/public/api/v1/inboxes/$IDENTIFIER/contacts/user_42/conversations \
  -H "Content-Type: application/json" -d '{}'

# 3. Post their message into it
curl -X POST https://app.tied.com/public/api/v1/inboxes/$IDENTIFIER/contacts/user_42/conversations/$CONVERSATION_ID/messages \
  -H "Content-Type: application/json" \
  -d '{"content": "Where is my order?"}'
```

Every message created this way is incoming, attributed to that contact. There's also a matching read path (`GET` the same routes) and three conversation actions — `toggle_status` (resolve, reopen or mark pending), `toggle_typing` (start or stop a typing indicator), and `update_last_seen` — for keeping Tied's view of the conversation accurate on your terms, not just delivering text.

<Info>
  This is the **Client** family of the API — one of three. See [API reference](/api-reference) for how it
  relates to the Application and Platform families you'd use to manage inboxes and accounts themselves.
</Info>

## Proving identity — optional, until it isn't

Nothing above proves that `user_42` is really who they claim to be — the identifier is just a string you send. For anything where that matters, add an `identifier_hash`:

```
identifier_hash = HMAC-SHA256(hmac_token, identifier)
```

Compute it server-side, where `hmac_token` never leaves your backend, and send it alongside `identifier`. Tied verifies it the same way the web widget verifies a logged-in visitor. Send it and it's checked; turn on `hmac_mandatory` and Tied rejects any request that omits it.

## The outbound leg: your webhook

Eight event types are POSTed as JSON to `webhook_url`: `conversation_created`, `conversation_updated`, `conversation_status_changed`, `message_created`, `message_updated`, `webwidget_triggered`, `conversation_typing_on` and `conversation_typing_off`. Activity messages — the system-generated "conversation was assigned" style entries — are not included. This is how a reply typed in the Tied dashboard reaches whatever surface your contact is actually using: your server receives the event and pushes it out over your own transport.

<Warning>
  **Private internal notes are posted to this webhook too.** A note is a `message_created` event with
  `message_type: "outgoing"`, identical in shape to a real reply — there's no `is_private` guard
  filtering it out before it reaches you. If your integration echoes `message_created` events onto a
  customer-facing surface, filter out private notes yourself, or your team's internal notes will leak
  to the customer.
</Warning>

<Warning>
  **This webhook isn't signature-verified.** It's a plain JSON POST — there's no signing header the way
  account-level webhooks may carry. Treat the URL itself as the secret, and validate payloads by shape
  (matching an `id` against a conversation or message you already track) rather than trusting the network
  alone. Tied also gives your endpoint **5 seconds** to respond; if it doesn't respond successfully in
  that window, Tied marks the outgoing message failed rather than retrying forever — surface that
  failure state on your side too.
</Warning>

## Modeling a reply window

If your real channel has a session-expiry concept — the way WhatsApp closes the door 24 hours after a customer's last message — you can mirror it. Set `agent_reply_time_window` (in hours) in the channel's `additional_attributes`, and Tied treats a reply as out-of-window once that many hours have passed since the contact's last incoming message. Leave it unset and there's no restriction at all.

## No channel fee

There's no carrier leg for Tied to bill here — you're already paying for whatever transport you built. Resolutions are the only cost. See [Channel fees](/pricing/channel-fees).

## Next

<CardGroup cols={2}>
  <Card title="API reference" icon="terminal" href="/api-reference">
    The three credential families, and where the Client API fits among them.
  </Card>

  <Card title="Authentication" icon="lock" href="/api-reference/authentication">
    How the Application and Platform families authenticate, if you need those too.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/api-reference/webhooks/events">
    The account-level webhook system, and how it differs from this channel's.
  </Card>

  <Card title="Channel fees" icon="radio" href="/pricing/channel-fees">
    What every native channel costs to deliver on, and what doesn't.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.