Acorn Reply API
Create support tickets in Acorn Reply from your product or AI agent: one endpoint, idempotent retries, business metadata, and end-user context your team sees in the inbox.
Authentication
Every request carries a workspace API key as a Bearer token. Create keys in Settings > API keys (admins only). Keys are server-side secrets — never ship one to a browser or mobile app.
Authorization: Bearer ak_YOUR_KEYCreate a ticket
One endpoint creates a conversation in your inbox from an end-user message. Only customer.email and body are required; everything else enriches what your team sees.
curl -X POST https://acornreply.com/api/v1/tickets \
-H "Authorization: Bearer ak_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer": { "email": "[email protected]", "external_id": "usr_42" },
"subject": "Export failing",
"body": "My export has been stuck for an hour.",
"priority": "normal",
"tags": ["billing"],
"metadata": { "plan": "trial", "user_id": 42 },
"context": { "locale": "en-US", "timezone": "America/Chicago", "country": "US" },
"external_id": "tkt_abc123"
}'Fill the context object from the browser
The context object is the end user's environment — it renders in the conversation sidebar so your team sees device, language, local time, and country next to the message. Collect it client-side, then create the ticket from your server where the API key lives.
Derive country yourself (for example from Cloudflare's CF-IPCountry header on your own request). Never send the user's IP — the API has no field for it and we never store one.
// 1) In the browser: collect the end user's environment with their message.
// No API key here — this is plain data collection.
const context = {
user_agent: navigator.userAgent,
locale: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
page_url: location.href,
referrer: document.referrer || undefined,
}
await fetch('/support/tickets', { // your own backend endpoint
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, context }),
})
// 2) On your server: create the ticket. The API key stays server-side.
const res = await fetch('https://acornreply.com/api/v1/tickets', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ACORN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer: { email: user.email, external_id: user.id },
body: message,
metadata: { plan: user.plan },
context: { ...context, country: req.headers['cf-ipcountry'] },
external_id: ticketId, // idempotency key
}),
})
const { ticket } = await res.json() // ticket.url links into your inboxIdempotency
external_id is the idempotency key. Reusing one returns the existing ticket with HTTP 200 (a fresh ticket returns 201) — retries and double-submits cannot create duplicates.
On a replay every field is ignored except context, which fills missing environment fields and never overwrites stored ones. A genuinely new ticket needs a new external_id.
metadata vs context
metadata is business data you tell us: plan, order id, feature flags — flat string/number/boolean values, max 50 keys. It shows in the sidebar's Details section and powers quick-link URL templates like {metadata.order_id}.
context is the observed environment: user agent, locale, timezone, country, page URL. It shows in the sidebar's Environment section. The two never mix.
Errors
Errors return { "error": { "message", "type", "code", "fields": [...] } }. 401 means a missing or invalid key; 422 lists the failing fields; 5xx are safe to retry with the same external_id.
Rate limits
30 requests per minute per key. Beyond that you get HTTP 429 with a retry-after header (seconds). Back off and retry; idempotency keys make retries safe.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
| customer | object | yes | The end user the ticket belongs to. Matched/created by email. |
| customer.email | string | yes | Customer's email (required). |
| customer.name | string | no | Display name; derived from the email when omitted. |
| customer.external_id | string | no | Your ID for this customer, stored for cross-referencing. |
| body | string | yes | The first message text (required). |
| subject | string | no | Synthesized from body when omitted. |
| priority | low | normal | high | urgent | no | Workflow priority shown in the inbox and sidebar. |
| tags | string[] | no | Up to 20 labels shown on the conversation. |
| metadata | object | no | Flat business data (string/number/boolean values, max 50 keys). Shown in the conversation sidebar Details section and usable in quick-link URL templates. |
| external_id | string | no | Idempotency key. Reuse returns the same ticket (HTTP 200) instead of creating a duplicate. |
| context | object | no | Observed end-user environment (not the calling server's). All fields optional; invalid values are dropped without failing the request. country is an ISO 3166-1 alpha-2 code derived by the caller — never send an IP. Shown to agents in the conversation sidebar's Environment section. |
| context.user_agent | string | no | End user's browser User-Agent. |
| context.locale | string | no | BCP 47 tag, e.g. en-US. |
| context.timezone | string | no | IANA zone, e.g. America/Chicago. |
| context.country | string | no | ISO 3166-1 alpha-2, e.g. US. |
| context.page_url | string | no | Page the user was on (https). |
| context.referrer | string | no | Referrer URL (https). |
Example request and response
{
"customer": {
"email": "[email protected]",
"name": "Jane Doe",
"external_id": "usr_42"
},
"subject": "Export failing",
"body": "My export has been stuck for an hour.",
"priority": "normal",
"tags": [
"billing"
],
"metadata": {
"plan": "trial",
"user_id": 42
},
"context": {
"locale": "en-US",
"timezone": "America/Chicago",
"country": "US"
},
"external_id": "tkt_abc123"
}{
"ticket": {
"id": "498b650b-c46c-4b66-a0f4-dbf5a8d8a803",
"external_id": "tkt_abc123",
"subject": "Export failing",
"status": "open",
"priority": "normal",
"tags": [
"billing"
],
"source": "api",
"url": "https://acornreply.com/conversations/498b650b-c46c-4b66-a0f4-dbf5a8d8a803",
"created_at": "2026-06-12T00:00:00.000Z",
"customer": {
"id": "166ac1e0-3013-4304-b4f6-a04c0374f8be",
"email": "[email protected]",
"external_id": "usr_42"
}
}
}Responses
- 200 — Idempotent replay (existing ticket returned)
- 201 — Ticket created
- 401 — Missing or invalid API key
- 422 — Validation error
- 429 — Rate limit exceeded; the retry-after header carries the wait in seconds.