# Authentication (https://inboxtender.com/docs/authentication)
## API keys [#api-keys]
Keys are minted by a workspace **owner** in the dashboard: **Assistant →
API access**. A key looks like `it_live_` followed by 48 hex characters,
and it is shown **once** — we store only a hash, so copy it when you
create it.
* Up to **5 active keys** per workspace. Name them after what uses them
("Website form", "Zapier") so revoking is painless.
* Revoke instantly from the same card; requests with a revoked key start
failing with `401` immediately.
* API access is part of the **Responder** and **Front Desk** plans.
## Permissions [#permissions]
Choose permissions when creating a key. Existing keys retain contacts-only access.
Read, draft, and send permissions are separate; see [messaging scopes](/docs/messaging#permissions).
Name each agent key so internal operation records identify the operator.
## Sending the key [#sending-the-key]
Pass it in the `Authorization` header on every request:
```bash
curl https://api.inboxtender.com/api/v1/contacts \
-H "Authorization: Bearer it_live_…"
```
Treat keys like passwords: server-side only, never in a browser bundle or a
public repo. If a key leaks, revoke it and mint a new one — the swap takes
seconds.
## Rate limits [#rate-limits]
Each key may make **120 requests per minute**. Past that, requests answer
`429` with a `Retry-After` header (in seconds) — wait that long and retry.
# Contacts (https://inboxtender.com/docs/contacts)
A contact is identified by its **addresses** — a phone number, an email, or
both. That's what makes pushing idempotent: send the same person twice and
you update one record instead of minting a twin.
## The contact object [#the-contact-object]
```json
{
"id": "k97c2fq0v6bz8kj3n1xw5d4rhm",
"name": "Tyler West",
"phone": "+13232500210",
"email": "tyler@example.com",
"notes": "Wants the XLT",
"tags": ["hot lead", "f-150"],
"createdAt": 1756640000000
}
```
Phones are normalized to E.164 (`+1…`); emails are lowercased; tags are
lowercased and capped at 16 per contact.
## Create or merge a contact [#create-or-merge-a-contact]
```
POST /api/v1/contacts
```
```bash
curl https://api.inboxtender.com/api/v1/contacts \
-H "Authorization: Bearer it_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Tyler West",
"phone": "(323) 250-0210",
"email": "tyler@example.com",
"tags": ["hot lead"],
"notes": "Wants the XLT"
}'
```
| Field | Type | Notes |
| ------- | ---------- | -------------------------------------------------------------------------- |
| `phone` | `string` | 10 US digits or full E.164. At least one of `phone` / `email` is required. |
| `email` | `string` | Lowercased on save. |
| `name` | `string` | Up to 120 characters. |
| `notes` | `string` | Up to 500 characters. |
| `tags` | `string[]` | Up to 16, each up to 30 characters. |
Answers `201` when a contact was created, `200` when it merged into an
existing one; both return `{ "contact": …, "created": true|false }`.
### Merge rules [#merge-rules]
* A matching phone or email updates the existing contact and adds the other
address when it is unclaimed. Email-only contacts are supported.
* If the phone and email belong to **different contacts**, the request returns
`409` with error code `identifier_conflict`. Neither contact is changed.
* Each normalized email and phone belongs to one contact per workspace.
* Merges **fill blanks only**: a `name` or `notes` your team already wrote is
never overwritten. Tags are added, never removed.
* Re-running the same request is always safe.
## List contacts [#list-contacts]
```
GET /api/v1/contacts?limit=50&cursor=…&tag=hot%20lead
```
Newest first. `limit` is 1–100 (default 50). Page with the returned
`nextCursor` until it comes back `null`. `tag` filters within each page, so
a filtered page can come back short while `nextCursor` still advances.
```json
{ "contacts": [ … ], "nextCursor": "…" }
```
## Get one contact [#get-one-contact]
```
GET /api/v1/contacts/:id
```
Answers `{ "contact": … }`, or `404` if the id is unknown — including ids
that belong to a different workspace.
# InboxTender API (https://inboxtender.com/docs)
The InboxTender API lets your other tools write straight into your
workspace. Create or merge **contacts**, and manage **email and SMS**
conversations through the [messaging API](/docs/messaging).
```
Base URL https://api.inboxtender.com
```
Every request is authenticated with a workspace [API key](/docs/authentication)
and every response body is JSON.
**Using an AI agent?** These docs are agent-readable: point it at
[`/llms.txt`](https://inboxtender.com/llms.txt) for the index or
[`/llms-full.txt`](https://inboxtender.com/llms-full.txt) for everything on
one page. Install the [InboxTender skill](/skills/inboxtender/SKILL.md)
for the conversation-management workflow.
## What it's for [#what-its-for]
* **Push leads from anywhere** — a website form, a marketplace scraper, a
Zapier step, your existing CRM's automation.
* **Safe to re-send** — `POST /api/v1/contacts` merges by phone or email instead
of duplicating, and it never overwrites a name or note your team wrote.
* **The assistant picks them up** — pushed contacts appear on the Contacts
screen with their tags, ready for broadcasts and follow-ups.
## Errors [#errors]
Errors always share one shape:
```json
{ "error": { "code": "unauthorized", "message": "That API key is unknown or revoked." } }
```
| Status | Code | Meaning |
| ------ | ----------------------------------------------- | ---------------------------------------------- |
| 400 | `invalid_json`, `invalid_body`, `invalid_query` | The request doesn't parse or validate. |
| 401 | `unauthorized` | Missing, unknown, or revoked API key. |
| 403 | `plan_required` | The workspace's plan doesn't include the API. |
| 404 | `not_found` | Unknown endpoint or contact id. |
| 422 | `invalid_identifier` | Neither phone nor email is a usable address. |
| 429 | `rate_limited` | Over the request budget — honor `Retry-After`. |
# Lead emails (https://inboxtender.com/docs/lead-emails)
Marketplaces and dealer systems announce a new lead by email — but that email
comes *from a robot*, not from the buyer. Reply to it and you're talking to
yourself. InboxTender reads the notification, pulls the buyer out, and treats
the lead as if the buyer had written in directly: the contact is the buyer,
the conversation is with the buyer, and the drafted first reply goes to the
buyer's own inbox.
## Your leads address [#your-leads-address]
Take your workspace email address from the Channels page and add `+leads`
before the `@` — signed in, this shows your workspace's actual address:
Anything sent there is treated as lead-source traffic: recognized
notifications become buyer conversations, and nothing on this address ever
gets an automatic reply to the robot that sent it.
## Where to plug it in [#where-to-plug-it-in]
Anything that can send an email works. These are the usual suspects:
* **Your website** — send your form-fill notification emails to it.
* **Or just forward** — a forwarding rule from the mailbox that already
receives your lead alerts works too. Recognition also runs on your plain
address, so existing forwarding keeps working without the `+leads` suffix.
Use one route per source: if your dealer system emails us directly, don't
*also* forward the same alert from your mailbox, or the same lead can show
up twice.
## What happens to a recognized lead [#what-happens-to-a-recognized-lead]
1. **The buyer becomes a contact** — name, email, and phone, merged into an
existing contact when one already matches, tagged `lead` plus the source
(for example `cycletrader`), with the vehicle of interest noted.
2. **A conversation opens from the buyer** — what they wrote ("I would like
to make an offer…") plus the vehicle, price, location, and trade-in
details underneath.
3. **A first reply is drafted** — the assistant answers the buyer's actual
message. Email replies always wait for your approval before sending.
## Recognized formats [#recognized-formats]
| Format | What it is |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Lightspeed EVO system alerts | `ONLINE_LEAD_RECEIVED` notification emails, including embedded marketplace lead details |
| ADF/XML | The automotive lead standard emitted by marketplaces, OEM sites, and dealer website providers |
| RELS lead metadata | The real-estate standard ([leadmetadata.org](https://leadmetadata.org)) that portals and agent CRMs embed in their notification emails |
A message on the leads address that doesn't match a known format still lands
in your inbox — visible, untouched, and never auto-replied to. If a lead
arrives without any email address for the buyer, the contact (with phone,
tags, and notes) is still captured; only the drafted reply is skipped.
Prefer pushing leads from code or an automation tool instead? Use the
[contacts API](/docs/contacts).
# Email and SMS (https://inboxtender.com/docs/messaging)
Use the same email address and SMS number configured in your workspace. Messages
use the normal InboxTender delivery path, including SMS consent, STOP handling,
carrier readiness, sender identity, and sending limits.
API activity is recorded internally with the key's name and ID. InboxTender does
not add an agent label to message bodies or change the customer-facing sender.
The caller supplies the message text. Existing business identification and SMS
opt-out instructions still apply.
## Set up an agent [#set-up-an-agent]
Create a named key in **Assistant → API access**. Choose **Read messages**,
**Draft replies for review**, or **Manage and send messages**. Existing keys keep
their contacts-only permissions. Revoke a key there to stop new API operations.
Install the [InboxTender skill](/skills/inboxtender/SKILL.md) in your agent's skill
folder as `inboxtender/SKILL.md`. Set `INBOXTENDER_API_KEY` in the agent's environment.
No CLI, npm package, or MCP server is required.
The skill describes a workflow; your agent must be running to execute it. This
version supports polling, with no outgoing event subscriptions.
## Permissions [#permissions]
| Scope | Allows |
| ---------------------- | ---------------------------------------------------------- |
| `contacts:read` | Read contacts |
| `contacts:write` | Create or merge contacts |
| `messages:read` | Read email/SMS conversations, messages, and pending drafts |
| `messages:draft` | Create reply drafts and dismiss drafts |
| `messages:send` | Start messages, send replies, and approve drafts |
| `conversations:manage` | Claim/release conversations and mark them handled |
Drafting, sending, and draft decisions also require `conversations:manage`.
A key without `messages:send` cannot approve its own drafts. A workspace member
can review and approve them in the existing dashboard.
`GET /api/v1/me` returns the authenticated `workspaceId`, `keyId`, and `scopes`.
All data belongs to that workspace; never supply a workspace ID in the body.
## Read conversations [#read-conversations]
```
GET /api/v1/conversations?channel=sms&limit=50&cursor=…
GET /api/v1/conversations?channel=email&limit=50&cursor=…
GET /api/v1/conversations/:id
GET /api/v1/conversations/:id/messages?limit=50&cursor=…
GET /api/v1/drafts?limit=50&cursor=…
```
Conversation IDs are opaque strings such as `sms:+15555550101` or `email:…`.
**URL-encode the entire ID** when inserting it into a path.
Lists return `conversations`, `messages`, or `drafts`, plus `nextCursor`.
Pass that cursor unchanged until it is `null`. Limits are integers from 1 to 100,
defaulting to 50. A page can be empty while its cursor still advances.
Conversation lists use stable conversation-key order, not last-activity order.
Each conversation includes `id`, `channel`, `contactId`, `name`, `peer`,
`aiPaused`, `managedByApiKeyId`, `optedOut`, `system`, and `lastMessageAt`.
The ownership fields are internal operator information.
Messages are newest first within each source. SMS includes both directions.
For email, fetch **both** `direction=inbound` and `direction=outbound`, keeping
a separate cursor for each, then merge by `at`. Email defaults to inbound.
Messages include `id`, `conversationId`, `direction`, `text`, `at`, and optional
`subject` and `status`. These APIs handle text; they do not upload attachments.
For polling, start a fresh conversation listing on each sweep, compare activity,
and read new messages until you reach IDs already processed. Persist message IDs
in your agent's own storage. Pagination cursors are for finishing a sweep, not
subscriptions. Gmail access covers mail routed into InboxTender and its configured
sending account; it does not synchronize the entire Gmail mailbox.
## Claim a conversation [#claim-a-conversation]
```
POST /api/v1/conversations/:id/takeover
{ "paused": true }
```
This pauses the built-in assistant, stops scheduled follow-ups, and assigns the
conversation to the calling API key. Another agent or a human takeover causes a
`409 conflict`. A workspace member can take control using the dashboard.
Claim before writing a reply, creating a draft, deciding a draft, or marking a
conversation handled. A new outbound message claims its conversation automatically.
API writes check ownership again inside the transaction.
To hand control back to the built-in assistant, use the same endpoint with
`{"paused": false}`. Resolve pending drafts and queued or uncertain sends first.
Releasing control does not recreate cancelled follow-ups.
## Draft or send a reply [#draft-or-send-a-reply]
```
POST /api/v1/conversations/:id/drafts
{ "text": "The blue model is available. Would you like to see it tomorrow?" }
POST /api/v1/conversations/:id/replies
{ "text": "The blue model is available. Would you like to see it tomorrow?" }
```
Draft creation leaves a pending reply for review. Sending queues it immediately.
Both supersede older pending replies in the conversation and stop its follow-ups.
SMS replies allow up to 1,600 characters; email replies allow up to 10,000.
Replying to an outbound-only email thread requires the recipient's first inbound
reply; use a new email when starting another message before that happens.
```
POST /api/v1/drafts/:id/approve
{ "text": "Optional edited reply" }
POST /api/v1/drafts/:id/dismiss
{}
POST /api/v1/conversations/:id/handled
{}
```
Approve with `{}` to keep the draft text. Approval preserves existing freshness,
appointment, consent, and delivery checks. A superseded draft can return an
operation with `status: "dismissed"` without sending anything. Marking handled
clears pending replies and follow-ups while keeping your claim.
## Start a message [#start-a-message]
```
POST /api/v1/messages
{ "channel": "sms", "to": "+15555550101", "text": "Your order is ready.", "purpose": "service" }
```
SMS requires existing consent evidence or an eligible inbound conversation.
`purpose` defaults to `service`; use `marketing` for promotional messages.
The API cannot grant consent. The workspace's existing consent workflow handles it.
First-contact and marketing texts retain business identification and STOP instructions.
```
POST /api/v1/messages
{ "channel": "email", "to": "customer@example.com", "subject": "Your order", "text": "Your order is ready." }
```
Starting a message sends it. These endpoints do not expose bulk campaigns.
## Retry and verify delivery [#retry-and-verify-delivery]
**Every messaging POST requires an `Idempotency-Key` header** containing 1–100
characters. Generate one unique value for each intended action, store it before
sending, and reuse it with the identical request after a timeout. Reusing a key
for another action or body returns `409 conflict`. Receipts are scoped to the API
key and retained with the workspace.
```bash
curl 'https://api.inboxtender.com/api/v1/messages' \
-H "Authorization: Bearer $INBOXTENDER_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-123-ready-v1' \
--data '{"channel":"sms","to":"+15555550101","text":"Your order is ready."}'
```
Writes return `{ "operation": … }`. The operation has `id`, `operation`,
`createdAt`, `actor`, `conversationId`, `status`, and, when applicable, `draftId`
or `outboundId`. `actor` records the calling agent's API key internally.
```
GET /api/v1/operations/:id
```
Only the key that created the operation can fetch it. A retry of the original
POST also returns that operation with its current status.
| Status | Meaning |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| `pending` | A draft awaits review |
| `queued`, `sending` | Sending is in progress; a 202 response is not proof of delivery |
| `sent` | The sending provider accepted the message; this does not prove it was read or reached the recipient |
| `failed` | Sending failed or the draft was reopened; inspect the dashboard before approving another attempt |
| `unknown` | The provider may have accepted it; reconcile in the dashboard before attempting another send |
| `dismissed` | The draft was dismissed or superseded |
| `completed` | A management action completed |
Follow `Retry-After` on `429`. On `401`, stop and replace or restore the key.
On `403 insufficient_scope`, obtain an appropriately scoped key from the owner.
On ownership conflicts, stop changing that conversation until an operator
releases it. Invalid message content and existing channel restrictions return
`422` with the underlying error code and message.