REST API
Read and update Pengo conversations, send messages and list visitors from your own code with the REST API and an API key.
On this page
The Pengo REST API lets your own systems work with your inbox: list and read conversations, change their status, assignee, priority or tags, read and send messages, and list visitors. Use it to sync conversations into a CRM or data warehouse, build reports, or reply from another tool.
#Create an API key
-
Go to Settings Workspace API keys and click Generate key.
-
Enter a Key name that tells you where the key is used, such as “Production server”, and click Create key.
-
Copy the key straight away.
Pengo shows the full key only once. Click the copy icon and store it somewhere safe, such as your server’s environment variables or a secrets manager. Then click Dismiss.
API keys start with pgk_live_. In the list, Pengo shows each key’s name, the start of the key, when it was created and when it was last used.
Treat keys like passwords. A key has full read and write access to the websites of the person who created it, so never put one in page code, a mobile app or a public repository. To revoke a key, click its delete icon and confirm. Anything using it stops working immediately. You can have up to 10 active keys.
#Authentication
Send your key as a Bearer token in the Authorization header of every request:
curl https://app.pengo.chat/api/v1/conversations \
-H "Authorization: Bearer pgk_live_xxxxxxxxxxxxxxxx" The base URL for every endpoint is:
https://app.pengo.chat/api/v1 #Choose a website
Every request works on one website. If you have more than one, add property_id to the query string of every request, including POST and PATCH requests. It’s your website ID, the propertyId value in your install snippet. Without it, Pengo picks one of the websites the key can access, so always pass it when you run several sites.
curl "https://app.pengo.chat/api/v1/conversations?property_id=YOUR_PROPERTY_ID&status=open" \
-H "Authorization: Bearer $PENGO_API_KEY" #Endpoints
| Method | Path | What it does |
|---|---|---|
GET | /conversations | Lists conversations, newest first. |
GET | /conversations/{id} | Gets one conversation. |
PATCH | /conversations/{id} | Updates a conversation’s status, assignee, priority or tags. |
GET | /conversations/{id}/messages | Lists a conversation’s messages, oldest first. |
POST | /conversations/{id}/messages | Sends a message in a conversation. |
GET | /visitors | Lists visitors, most recently seen first. |
The full, interactive reference with every field is at app.pengo.chat/api-docs.
#List conversations
GET /conversations
| Query parameter | Description |
|---|---|
status | Only conversations with this status: open, pending or resolved. |
assigned_to | Only conversations assigned to this teammate’s user ID. |
limit | How many to return. Default 50, maximum 100. |
offset | How many to skip, for paging. Default 0. |
Each conversation has id, visitor_id, visitor_name, visitor_email, status, priority, assigned_to, created_at, updated_at, last_message, last_message_at and tags.
{
"data": [
{
"id": "3f6d1c2e-9a8b-4c7d-a6e5-0f1e2d3c4b5a",
"visitor_name": "Ada",
"visitor_email": "ada@example.com",
"status": "open",
"priority": "normal",
"last_message": "Do you ship to Canada?",
"tags": ["shipping"]
}
],
"pagination": { "total": 128, "limit": 50, "offset": 0, "has_more": true }
} #Get a conversation
GET /conversations/{id} returns the full conversation as data. You get a 404 if it doesn’t exist or belongs to another website.
#Update a conversation
PATCH /conversations/{id} with a JSON body. Send only the fields you want to change:
| Field | Description |
|---|---|
status | open, pending or resolved. |
assigned_to | The user ID of the teammate to assign. |
priority | urgent, high, normal or low. |
tags | The full list of tags. It replaces the current tags. |
curl -X PATCH "https://app.pengo.chat/api/v1/conversations/CONVERSATION_ID?property_id=YOUR_PROPERTY_ID" \
-H "Authorization: Bearer $PENGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "resolved", "tags": ["billing", "refund"]}' The response is the updated conversation as data.
#List messages
GET /conversations/{id}/messages returns the conversation’s messages, oldest first, with the same limit, offset and pagination as conversations.
#Send a message
POST /conversations/{id}/messages with a JSON body:
| Field | Required | Description |
|---|---|---|
content | Yes | The message text, 1 to 10,000 characters. |
sender_id | Yes | The user ID (a UUID) of the teammate sending it, for example a conversation’s assigned_to. |
sender_type | No | agent (the default) or bot. |
client_message_id | No | Your own ID for this message, up to 64 characters of letters, numbers and . _ : -. Makes retries safe. |
curl -X POST "https://app.pengo.chat/api/v1/conversations/CONVERSATION_ID/messages?property_id=YOUR_PROPERTY_ID" \
-H "Authorization: Bearer $PENGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Thanks, your refund is on its way.", "sender_id": "AGENT_USER_ID", "client_message_id": "refund-4821"}' A new message returns 201 with the message as data. If you retry with the same client_message_id, Pengo doesn’t create a second message. It returns 200 with the original message and "deduplicated": true. Sending a message triggers the message.sent webhook.
#List visitors
GET /visitors
| Query parameter | Description |
|---|---|
is_online | true or false. |
has_active_chat | true or false. |
limit | Default 50, maximum 100. |
offset | Default 0. |
Each visitor has id, email, name, phone, is_online, has_active_chat, last_seen_at, created_at, country_code and tags.
#Paging
List endpoints return a pagination object with total, limit, offset and has_more. To get every record, keep adding limit to offset until has_more is false:
const base = 'https://app.pengo.chat/api/v1/conversations';
const headers = { Authorization: `Bearer ${process.env.PENGO_API_KEY}` };
let offset = 0;
const all = [];
while (true) {
const res = await fetch(`${base}?property_id=${process.env.PENGO_PROPERTY_ID}&limit=100&offset=${offset}`, { headers });
if (!res.ok) throw new Error(`Pengo API error ${res.status}`);
const page = await res.json();
all.push(...page.data);
if (!page.pagination.has_more) break;
offset += page.pagination.limit;
} #Rate limits
Each endpoint allows 200 requests per minute from the same IP address. Over the limit, Pengo answers 429 Too Many Requests with these headers:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait before trying again. |
X-RateLimit-Limit | The number of requests allowed per minute. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | When the window resets, in milliseconds since 1970. |
Wait for Retry-After seconds, then retry.
#Errors
Errors use standard HTTP status codes and a JSON body.
| Status | When |
|---|---|
400 | The request body is invalid, for example a missing content or sender_id. |
401 | The key is missing, wrong or revoked, or property_id isn’t a website the key can access. |
404 | The conversation doesn’t exist, or belongs to another website. |
429 | You hit the rate limit. |
500 | Something went wrong on Pengo’s side. Retry later. |
Authentication errors look like this:
{ "error": "Unauthorized", "message": "Invalid API key" } Other errors include a code and a requestId. The request ID is also in the X-Request-ID header. Include it if you contact support@pengo.chat about a failed request.
{
"error": true,
"message": "Conversation not found",
"code": "NOT_FOUND",
"requestId": "mgx3k2a1-4f9c2e"
} #Get notified instead of polling
To react to new conversations and messages as they happen, use webhooks instead of calling the API on a timer. Many integrations use both: webhooks to hear about changes, and the API to read details or reply.
Common questions
I lost my API key. Can I see it again?
No. Pengo only shows a key once, when you create it. Revoke the lost key and create a new one.
How many API keys can I have?
Up to 10 active keys. Revoke keys you no longer use to make room for new ones.
Can I use the API from the browser?
Don't. An API key has full read and write access, so keep it on your server and call the API from there.
Was this guide helpful?
Your answer helps us improve it.
Sorry about that. or contact support, and we'll fix the guide.