REST API and keys

Create API keys and use the PepoChat REST API to read conversations, reply as your team, sync contacts, add snippets and talk to the agent.

The PepoChat REST API lets your own systems read and act on a workspace: list and update conversations, reply to visitors as your team, sync contacts, add knowledge snippets and send messages to the agent. Every request carries an API key created on the Developers page, and the API is available on every plan, including Free.

Create an API key

  1. Open Configure → Developers

    The API keys card is at the top of the page. Only workspace owners and admins can create and revoke keys.

  2. Name the key and click Create key

    Type a New key name of up to 60 characters, usually the system that will use it, and click Create key.

  3. Copy the key

    The Your new API key dialog shows the full key once; copy it and click Done. Only a hash is stored, so a lost key cannot be shown again: revoke it and create another.

Each key in the list shows its name, its first characters, when it was created and when it was last used (refreshed at most every five minutes). Revoke warns "Anything using this key stops working at once."; click Revoke key to confirm.

Base URL and authentication

Copy the base URL from the API keys card, where it appears in the Try it sample and the reference link:

https://flippant-cheetah-84.convex.site/api/v1

Send the key as a bearer token in the Authorization header of every request:

curl "https://flippant-cheetah-84.convex.site/api/v1/conversations?limit=5" \
  -H "Authorization: Bearer pc_live_..."

A key is pc_live_ followed by 32 letters and digits, and every call reads and changes only the workspace it belongs to. The OpenAPI 3.1 description is served at /api/v1/openapi.json without a key. Responses carry no CORS headers, so call the API from your server and keep keys out of browser code.

Endpoints

Paths are relative to the base URL. Bodies are JSON, and every time is a Unix timestamp in milliseconds.

Method and pathWhat it does
GET /meThe workspace the key belongs to: workspaceId and plan.
GET /conversationsConversations, newest first. Optional status: unresolved, escalated or resolved.
GET /conversations/{id}One conversation with its transcript.
PATCH /conversations/{id}Change status, priority or tags.
POST /conversations/{id}/messagesReply to the visitor as your team.
POST /chatSend a visitor message to the agent and wait for the answer.
GET /contactsContacts, most recently seen first.
POST /contactsCreate or update up to 500 contacts by email.
GET /contacts/{id}One contact.
PATCH /contacts/{id}Update a contact.
GET /knowledge/snippetsText snippets and Q&A pairs, most recently updated first.
POST /knowledge/snippetsAdd a text snippet or a Q&A pair.
GET /openapi.jsonThe OpenAPI reference. No key needed.

Lists and pagination

The four list endpoints take limit (1 to 100, default 25; other values are clamped) and cursor, and return { "items": [...], "cursor": "..." }. Pass cursor back for the next page and stop when it is null. Conversation lists leave out chats from Test the agent, so a page can hold fewer items than limit while more follow.

Conversations

FieldContents
id, createdAtThe conversation id and when it started.
statusunresolved, escalated or resolved.
channelwidget, hosted, api, whatsapp, messenger, instagram, voice or phone.
prioritylow, normal, high or urgent.
tags, topic, assignedToInbox tags, the topic label and the user id of the assigned teammate.
ratingThe visitor's feedback: up, down or null.
escalatedAt, resolvedAtThe first time each happened, or null.
messageCount, lastMessageThe message count and the latest message (text, role, createdAt).
visitorsessionId, name, email, emailVerified, country, pageUrl.
messages, messagesTruncatedOnly on GET /conversations/{id}: up to 200 messages with role (visitor, assistant or operator), author (the teammate's name), text and createdAt, without tool calls. messagesTruncated is true when there are more.

PATCH /conversations/{id} takes any of three fields:

{ "status": "resolved", "priority": "high", "tags": ["billing", "refund"] }
  • A status change records escalatedAt or resolvedAt the first time and sends the matching webhook event. Setting unresolved reopens a conversation.
  • tags replaces the whole list. Tags are stored in lowercase and cut to 30 characters; a conversation carries at most 10, and a workspace at most 200 different tags.

POST /conversations/{id}/messages takes text (1 to 4,000 characters) and an optional author, the name shown on the reply (up to 80 characters, API when left out). The visitor sees a reply from your team. An unresolved conversation becomes escalated and the agent stops answering it. A resolved conversation has to be reopened first. These replies reach the web chat and the inbox, not WhatsApp, Messenger or Instagram.

Talk to the agent

POST /chat sends a message as a visitor and waits up to 60 seconds for the agent's answer.

curl -X POST "https://flippant-cheetah-84.convex.site/api/v1/chat" \
  -H "Authorization: Bearer pc_live_..." \
  -H "Content-Type: application/json" \
  -d '{"message": "Do you ship to Canada?", "visitor": {"name": "Ada", "email": "[email protected]", "externalId": "user_123", "attributes": {"plan": "pro"}}}'
{ "conversationId": "k17...", "status": "unresolved", "reply": "Yes, we ship to Canada...", "pending": false }
  • message is 1 to 4,000 characters. Leave out conversationId to start a conversation (channel api, opening with your greeting); send it back to continue one.
  • visitor (optional, read only when a conversation starts) is trusted because the caller holds your key: an email counts as verified and files the contact without a new-lead notification, and an externalId marks the visitor as identified, as an identity token would. Unnamed visitors show as "API visitor".
  • If the agent has not answered within the wait, the response is 202 with "reply": null and "pending": true. Read the answer later with GET /conversations/{id}.
  • Once a conversation is escalated to your team the agent stops answering it, so the call ends with 202 unless a teammate replies within the wait.
  • Replies count toward the monthly AI replies like widget replies (Plans and billing). On Free, once they are used up, the conversation goes to your team and reply holds the hand-off notice. The hourly ceiling on anonymous web chat does not apply.
  • The visitor session behind an API conversation lasts 24 hours and is extended while messages arrive. After it expires, that conversation answers 403 "Invalid session"; start a new one.

Contacts

POST /contacts creates or updates up to 500 contacts by email in one call:

{
  "contacts": [
    {
      "email": "[email protected]",
      "name": "Ada Lovelace",
      "company": "Analytical Engines",
      "externalId": "user_123",
      "attributes": { "plan": "pro", "seats": 5 }
    }
  ]
}

The response gives created, updated, skipped (emails that are not valid addresses) and ids in request order. Like a CSV import, these writes never trigger a new-lead notification or the lead.created webhook, and new contacts show the source API on the Contacts page.

Fields you send replace the stored values; attributes merge key by key. PATCH /contacts/{id} takes the same fields except email. Longer text is cut: name to 100 characters, phone to 50, company to 100, externalId to 200. Attributes follow the rules in Visitor identity, and entries that break them are dropped without an error.

Knowledge snippets

POST /knowledge/snippets adds a text snippet ("kind": "text" with title and body) or a Q&A pair ("kind": "qa" with question and answer). It follows the rules and plan limits of Text snippets and Q&A pairs and returns 201 with the new id; the snippet reaches the knowledge base in the background.

Limits

  • 600 requests a minute per key, in bursts of up to 100. Past that the API answers 429 with a Retry-After header in seconds.
  • POST /chat also has its own limit of 60 calls a minute per key, in bursts of up to 20.
  • 10 active keys per workspace, and up to 20 new keys an hour.

Common errors

Every error has the same shape:

{ "error": { "code": "bad_request", "message": "status must be unresolved, escalated or resolved" } }
StatusMessageMeaning
401Send an API key as Authorization: Bearer pc_live_…The header is missing, or the key is not in the pc_live_ format.
401This API key is not validThe key was revoked or never existed.
403Invalid sessionThe visitor session of an API conversation expired. Start a new conversation.
404Conversation not foundThe id is wrong or belongs to another workspace. Contact not found is the same for contacts.
404No such routeThe path is not part of the API.
400text must be 1 to 4000 charactersThe reply is empty or too long. /chat says message must be 1 to 4000 characters.
400The conversation is resolved; reopen it firstSet status to unresolved before replying.
400The conversation is resolved; start a new oneCall /chat without conversationId.
400A conversation can carry at most 10 tagsSend fewer tags.
400contacts must hold 1 to 500 entriesSplit the import into batches.
400cursor is not valid: pass the cursor returned with the previous page of the same list, or leave it out for the first pageUse the cursor from the previous page of the same list.
429Too many requestsWait the number of seconds in Retry-After. The /chat limit answers Too many chat requests, without that header.
500Something went wrong on our sideRetry later.

Something missing or wrong on this page? Tell us and we will fix it.