Developers

Public API Beta

Build workspace integrations with secure, scoped API keys and a stable versioned JSON contract.

Open interactive API reference

Quickstart

  1. 01

    Open API keys

    Open Settings, select Developer, find API keys, then select Create key. Owners and admins can create keys.

  2. 02

    Create API key

    Enter a Name, choose Expires in days, choose Channel access as All channels or Selected channels, and choose the minimum scopes your integration needs under Scopes. A request succeeds only when the key has the exact resource action required by the endpoint. Select Create key.

  3. 03

    Save your API key

    The full key is shown once. Copy it into your secret manager, then select I saved it.

  4. 04

    Send the key

    Use a Bearer token, or send the same value in x-api-key. Never put keys in query strings or browser code.

cURL

curl https://www.usesonny.com/api/v1/contacts?limit=25 \
  --header "Authorization: Bearer sonny_your_key"

Keys stay secure

Key security
Keys use a sonny_ prefix, 64 random characters, one-way hashing at rest, configurable expiry, and a 1800 requests/minute per-key limit. A key is bound to one workspace.
To revoke one, select the trash button beside it. Confirm Revoke API key? by selecting Delete. This immediately stops the key.
Authorization on every call
Sonny verifies the hash and scope, then re-checks the creator's active workspace membership, role, and billing state. Removing or deactivating that user immediately disables their keys.
Selected channels are enforced for every REST API and MCP call. Contacts, properties, tags, and webhook management endpoints require access to all channels. Form creation can use contacts:write with conversations:write on selected channels; standalone contact endpoints remain workspace-wide.

Scopes

  • contacts:read
  • contacts:write
  • tags:read
  • tags:write
  • conversations:read
  • conversations:write
  • messages:read
  • messages:write
  • messages:send
  • webhooks:read
  • webhooks:write
  • kb:read
  • kb:write
  • csat:read
  • attachments:write
  • members:read
  • contact-memory:read
  • contact-memory:write
  • reporting:read

Resources

Contacts
Manage contacts, typed properties, exact-value filters, and customer memories. Archive a contact, or permanently erase it with all of its conversations for data-protection requests.
Sources
Discover configured sources and whether each supports chat, email, or both.
Conversations
Create enquiries from forms, filter attention, assign agents or teams, snooze, tag, and batch updates.
Sync
Replay durable workspace changes and deletion records with a saved cursor.
Members and teams
Discover assignable teammates, availability, and effective channel access.
Reporting
Read support, team, AI, knowledge, lead, and availability reports.
Messages
Read message history and attachment extraction, upload private files, add internal notes, and send or edit customer replies.
Webhooks
Manage endpoints, subscriptions, tests, deliveries, and retries.
Knowledge Base
Create, read, update, and delete help center articles and categories per source.
Customer Satisfaction
Read CSAT scores for the workspace, each channel, and each conversation, and list individual ratings filtered by rating value, comment, assignee, or time window.

Using Sonny from an AI tool

Sonny MCP exposes every public API operation as a tool. Clients connect by signing in with OAuth 2.1 — or with a scoped API key — and every call keeps the same scopes, workspace boundaries, and business rules.

Read the Sonny MCP guide

Responses and errors

Collection responses use data and include pagination where applicable. Every response includes x-request-id; you may supply a safe request ID and Sonny will echo it. Errors return a safe message without exposing sensitive implementation details.

{
  "error": {
    "type": "validation_error",
    "message": "Invalid email address",
    "requestId": "req_01J..."
  }
}
400
Invalid input, JSON, query, or cursor.
401
Missing, invalid, expired, or unscoped API key.
402
The workspace billing state blocks this request.
403
The key creator's workspace role is not allowed.
404
The resource does not exist in the key's workspace.
409
The request conflicts with an existing resource.
410
Sync cursor expired. Bootstrap current state and replay from a new cursor.
413
The upload exceeds the allowed size.
422
The source has no email channel or a submitted field value is invalid.
429
The per-key rate limit was exceeded.
500
An unexpected error occurred. Retry with the request ID.
503
Webhook delivery backlog is saturated. Retry later.

Rate limits

Each API key may make up to 1,800 requests per minute. Every response reports the current window through the standard RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers, so clients can self-throttle instead of guessing. When the limit is exceeded, the API returns 429 with a Retry-After header — wait that many seconds before retrying.

Versioning and deprecation

The API is versioned in the URL path (/api/v1). Within a version we only make additive changes — new endpoints, new optional fields, new enum values. Breaking changes ship as a new version, and before any v1 endpoint is retired we give at least six months' notice: on this page, by email to workspace owners with active API keys, and via Deprecation and Sunset headers on affected endpoints.

Edit a sent reply

  1. Give your API key or MCP connection the messages:read and messages:send scopes and access to the reply’s channel.
  2. Read the conversation’s messages and copy the ID of a human reply you sent. An API key acts as its creator; OAuth acts as the connected teammate.
  3. Send the replacement text using the request below, or call edit_message in MCP with conversationId, messageId, and body.
  4. Check the returned message. Its ID, attachments, read receipt, and original send time stay the same.
curl --request PATCH \
  https://www.usesonny.com/api/v1/conversations/conversation_1/messages/message_1 \
  --header "Authorization: Bearer sonny_your_key" \
  --header "Content-Type: application/json" \
  --data '{"body":"The corrected reply"}'

Editing corrects the transcript and connected chat clients. It does not send another email or notification, and emails already delivered stay unchanged. Only your own human customer replies can be edited; incoming messages, bot replies, internal notes, and other teammates’ replies cannot. Wait for pending email delivery to finish before editing. Replacement text must contain 1–50,000 characters; previous HTML and link previews are cleared. Integrations receive a message.updated webhook and sync event. Polling only for newer message IDs will not find edits; use the durable sync feed.

Create a conversation from a form

Send a form submission to POST /api/v1/conversations. Sonny finds or creates the contact by email, stores the answers, and opens an incoming conversation on your chosen source’s email channel. The channel’s team, assignment rules, and notifications apply. Agents reply by email.

  1. Create a key with conversations:write and contacts:write. You can restrict it to the client’s selected channels. Find the source ID with GET /api/v1/sources, which also needs conversations:read.
  2. In n8n, add an HTTP Request node: method POST, URL https://www.usesonny.com/api/v1/conversations. Store the key in a Header Auth credential: Authorization with value Bearer sonny_your_key.
  3. Enable Send Body, choose JSON and Using JSON, then switch the whole JSON field to Expression and paste this example. Replace the source ID and map the input fields to your form. Use the form’s stable, unique submission ID so a retry uses the same value.
{{ {
  sourceId: "YOUR_CLIENT_SOURCE_ID",
  contact: { email: $json.email, name: $json.name },
  subject: "Website enquiry",
  message: $json.message,
  fields: {
    Company: String($json.company ?? ""),
    Budget: String($json.budget ?? ""),
    Service: String($json.service ?? "")
  },
  externalId: "website-form-" + $json.submissionId
} }}

For more node options, see the n8n HTTP Request guide. The equivalent MCP tool is create_conversation, with the same payload and permissions.

New field names become text properties. Existing number, URL, date and select fields must receive valid string values; a bad value returns 422 naming the field and saves nothing. Dates accept ISO dates or timestamps; select values must match an option. Up to 50 fields are accepted, with names up to 100 characters and values up to 5,000. Properties appear on the contact and in the conversation sidebar. Both message bodies keep a copy of the answers, including when you supply optional htmlMessage.

Optional tags accepts up to 20 existing workspace tag IDs. The response contains conversation, a contact summary, message, and deduplicated. New submissions return 201. Reusing externalId on the same source returns 200 with the original IDs and deduplicated: true; changed content is ignored. Without an external ID, every call creates a new conversation. Attachments on creation and AI auto-responses are not supported.

Agent workflows

Triage, act, and stay in sync

REST and MCP share the same permissions and workflows. Credentials can only narrow your current channel access; removing a channel from your membership also removes it from your integrations.

Find conversations that need attention

Filter by awaitingReply, unread, unassigned, assigneeId, agentGroupId, snoozed, priority, or tagIds. Awaiting reply ignores internal notes; unread is specific to the authenticated teammate. Unassigned means no individual assignee, even if a team is assigned. Tags match any supplied ID.

GET /api/v1/conversations?status=open&awaitingReply=true&compact=true&sort=waitingSince&direction=asc

Sort by waitingSince, lastMessageAt, createdAt, or business priority with direction asc or desc. IDs break ties. Omitted filters retain compact=false and snoozed=any. REST tagIds accepts comma-separated IDs; MCP also accepts an array. waitingSince starts at the first incoming message after the last reply; follow-ups and internal notes do not reset it. Run this waiting-queue scan on a schedule even when no webhook arrives, so old threads resurface.

Catch up without rereading the inbox

Use GET /api/v1/sync with conversations:read. The durable feed includes conversation changes, tags, messages, contact properties, memories, and deletion records. Each payload also requires its own read scope: message bodies need messages:read and memories need contact-memory:read. Restricted credentials receive only permitted channels.

  1. Start without a cursor, follow nextCursor until hasMore is false, and save that cursor.
  2. Read the current conversation/contact lists and any history you need for your initial state.
  3. Replay from the saved cursor to catch changes during that read. Process each page, then save nextCursor.

Deduplicate by event id: replay is at least once, so downstream actions need their own duplicate protection. A request without a cursor covers the last hour, not a complete snapshot. Events are retained for 30 days; HTTP 410 resync_required means bootstrap again. Also bootstrap after widening scopes or channel access. Use limit up to 100 and maxBodyChars up to 10,000 (default 500). Set compact=true for message textPreview/textTruncated fields capped at 200 characters instead of body/bodyTruncated. Webhooks can wake an agent; the feed remains available without webhook subscriptions or when webhook delivery is backlogged.

Record checks once and share the result

Before repeating an investigation, read list_conversation_checks (GET /api/v1/conversations/{conversationId}/checks). Save a result with record_conversation_check (PUT to the same path), supplying key, result, checkedBy and an optional reference for the card ID or URL. For example: key=product-version, result=Shopstar Go verified, checkedBy=Engineer, reference=card-X. Sonny records the authenticated actorId and checkedAt timestamp. Reusing a key replaces only that check; this is current state, not a history log. Reads need conversations:read and writes need conversations:write, both scoped to the conversation’s channel. These calls do not send a reply or mark the seller answered.

Assign and update work in batches

Discover active, assignable teammates and teams with GET /api/v1/members and /teams (members:read). Responses include availability and effective channel access, without email addresses. Use conversations:write to change status, priority, assigneeId, agentGroupId, snoozedUntil, snoozedUntilReply, addTagIds, or removeTagIds.

POST /api/v1/conversations/batch
{
  "conversationIds": ["conversation_1", "conversation_2"],
  "updates": { "agentGroupId": "team_1", "assignment": "round_robin" }
}

Round-robin chooses an available team member who can access the conversation's channel. With no eligible member, a single update returns 409 and a batch item fails. Each batch accepts up to 100 unique IDs and applies one patch atomically per conversation. Inspect every { id, ok, error? } result; some items may fail. Batches use the current request-based rate limit, without weighted charging by item count.

Share customer context with Sonny AI

List, save, and remove contact memories with contact-memory:read/write. Supply both contactId and an accessible sourceId; the contact must have a conversation on that source. Saved facts use the same validation, duplicate handling, and limits as the app and are recorded as manual memories.

Read property definitions at /api/v1/properties and read or set values at /api/v1/contacts/{contactId}/properties. MCP exposes list_properties, get_contact_properties, and set_contact_property. These require contacts:read/write and access to all channels. Set exactly one propertyId or propertyName, with a string value or null to clear it. Text, number, URL, date, and select values are validated against the field definition.

PUT /api/v1/contacts/contact_1/properties
{ "propertyName": "Plan", "value": "Pro" }

GET /api/v1/contacts?property[Plan]=Pro

Property filters match exact stored strings; multiple properties must all match. API-authored values are labelled as API data in Co-Pilot and the auto-responder. Existing verified-customer and source rules still apply. Ordinary manual internal field values remain excluded from this AI context.

Read the same reports as your team

With reporting:read, call GET /api/v1/reporting/{kind}. Kinds are overview, volume, response-times, agents, sources, tags, csat, ai, knowledge, leads, and online-hours. Overview combines counts, response times, and CSAT. Filters are period (1–365 days, default 30), paired from/to dates (to is exclusive), granularity (day/month), source, assigneeId, and tagId. Source accepts all, all-chat, all-email, website:{id}, or email:{id}.

Reports use the dashboard queries and current access checks. Knowledge uses dates and permitted channels, independently of the selected source, assignee, or tag. Online hours measures accessible teammates' workspace presence; its conversation counts remain channel-scoped. Existing /csat endpoints keep their separate csat:read scope and may describe a different population.

Send files with replies or internal notes

Grant attachments:write and upload a file up to 25 MB for a conversation. POST /api/v1/attachments takes multipart file and conversationId fields. The response contains an id and expiresAt. Pass up to 10 attachmentIds to a reply or note within one hour. Each upload is bound to your user and conversation and can be used only once. Unused uploads are cleaned up after expiry.

POST /api/v1/conversations/conversation_1/reply
{ "attachmentIds": ["upload_1"] }

Replies require messages:send; internal notes require messages:write and never reach customers. Attachment-only messages are supported. Email replies include files within the email size budget and download links for the rest; offline chat emails include file links. Emailed links remain usable while the attachment exists, so recipients can open them later. Forwarding the email shares access to those files. Check emailDeliveryStatus for delivery failures.

Authorized message reads include downloadUrl, downloadExpiresAt, aiStatus, aiDescription, and aiExtractedText, plus videoTranscripts for linked videos (Loom, Vimeo and others). Download links last 15 minutes; reread the message for fresh links. New API/MCP uploads have private storage. Older attachments remain public and are marked access=legacy_public: their original URL does not expire. Reading a message starts reading its images when the workspace has Sonny AI. Image readings and video transcripts finish shortly after a message arrives: while any are still running, the response starts with readsInProgress, whose nextStep gives the exact call to make. Pass waitSeconds (up to 30) to wait for them in one request. Customer-facing widget and realtime messages never include extraction or transcripts.

Explore the full request and response contracts

Knowledge base

Sync from an external source

Sync articles and categories using stable external IDs, import Markdown or HTML, upload images, set ordering, and manage reader audiences. Repeating an upsert updates the existing content.

  1. Choose your channel’s sourceId and grant kb:read and kb:write. Keep the API key on your server and limit its Channel access.
  2. Create the category, then sync an article as a draft. Review its formatting and access before publishing.
  3. Configure help-center access, publish, and test the reader view. Use pagination and stable external IDs for later updates.
Follow the complete REST sync walkthrough

Manage customer audiences

Create groups from verified customer traits and apply them to a help center, category, or article. All inherited restrictions must match. API and MCP credentials act as staff within their scopes; preview and test a real customer session to check reader access.

Set up and test help-center audiences

Related docs