AcornReply
Developer API

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.

OpenAPI specllms.txtGet an API key

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_KEY

Create 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 inbox

Idempotency

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

FieldTypeRequiredDescription
customerobjectyesThe end user the ticket belongs to. Matched/created by email.
customer.emailstringyesCustomer's email (required).
customer.namestringnoDisplay name; derived from the email when omitted.
customer.external_idstringnoYour ID for this customer, stored for cross-referencing.
bodystringyesThe first message text (required).
subjectstringnoSynthesized from body when omitted.
prioritylow | normal | high | urgentnoWorkflow priority shown in the inbox and sidebar.
tagsstring[]noUp to 20 labels shown on the conversation.
metadataobjectnoFlat business data (string/number/boolean values, max 50 keys). Shown in the conversation sidebar Details section and usable in quick-link URL templates.
external_idstringnoIdempotency key. Reuse returns the same ticket (HTTP 200) instead of creating a duplicate.
contextobjectnoObserved 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_agentstringnoEnd user's browser User-Agent.
context.localestringnoBCP 47 tag, e.g. en-US.
context.timezonestringnoIANA zone, e.g. America/Chicago.
context.countrystringnoISO 3166-1 alpha-2, e.g. US.
context.page_urlstringnoPage the user was on (https).
context.referrerstringnoReferrer 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

  • 200Idempotent replay (existing ticket returned)
  • 201Ticket created
  • 401Missing or invalid API key
  • 422Validation error
  • 429Rate limit exceeded; the retry-after header carries the wait in seconds.