escalio Docs

CUSTOMER API · V1

Your requests, in your tools.

Read, create and reply to support requests from your own backend or MCP client. A company administrator can create a key directly in the support portal.

Choose the right API

Customer keys use esc_customer_ and act as a customer. They expose public conversations only. For an internal cron job that creates tickets in your support team's queue, use the separate Organization API and an esc_org_ key.

Create a key in the portal

  1. Sign in to your ordinary support portal using an enabled sign-in method.
  2. Open API keys beside My requests (in the navigation menu on the chat layout).
  3. Choose Create API key, enter a name, select permissions and a lifetime of 1–30 days.
  4. Copy the key once into your server's secret store. The full key cannot be retrieved later.

The key acts as the signed-in company administrator. It stops working if that administrator loses company access, moves company, or its sign-in configuration changes. An administrator can inspect and revoke the company's keys. Ordinary company users cannot create keys.

Cannot see API keys?

Ask your support provider to open Settings → Tickets → Companies → your company → Users → your user and assign Company admin. This is a customer-company role, separate from staff owner/admin roles. Matching an email domain alone does not grant it. JWT SSO and the embedded panel do not need to be enabled.

Keys issued through the existing embedded panel can act as a selected company member; member keys see only that member's own requests. They also depend on the signed membership and embedded-support settings. Keys cannot create other keys.

For staff entering the customer portal from the workspace, keys retain that sign-in's staff-session binding. Use a direct customer sign-in for an integration that must outlive a staff sign-out.

Authentication and permissions

https://YOUR-PORTAL/api/portal/customer/v1

Use the exact portal origin where you created the key, including a custom domain when applicable. Do not switch between the custom domain and the Escalio subdomain. Send Authorization: Bearer <customer key>. Browser cookies and organization keys are not substitutes.

  • read: list and read accessible requests.
  • create: create a public request as the key's customer account.
  • reply: reply publicly to an accessible request.

Company admins can use the company view; ordinary member keys are limited to their own requests. Scopes restrict access further. Internal notes, other companies, staff Settings and arbitrary requester impersonation are unavailable.

Set ESCALIO_PORTAL to the portal origin without a trailing slash, ESCALIO_CUSTOMER_KEY from your secret store, and ESCALIO_TICKET_ID to a returned ticket UUID. For each new write, persist a unique ESCALIO_REQUEST_ID and its exact payload. The cURL examples use a POSIX shell; Python uses the standard library.

Methods and examples

Download the OpenAPI 3.1 reference. All responses are JSON. Unknown JSON body fields are rejected. Subject: 1–240 characters, no control characters. Body: 1–20,000 characters, not blank. Format: markdown (default) or plain. Markdown can include code blocks for logs; file upload is not part of this API.

Current customer and scopes

GET/api/portal/customer/v1/me

No ticket scope is required. Returns contact and company IDs, role, kind and scopes.

cURL (POSIX shell)

curl --fail-with-body --silent --show-error --request GET \
  "$ESCALIO_PORTAL/api/portal/customer/v1/me" \
  --header "Authorization: Bearer $ESCALIO_CUSTOMER_KEY"

List requests

GET/api/portal/customer/v1/tickets

Requires read. mine is the default; company requires a company admin. Returns page, page_size (50) and tickets; no total. Request another page while it contains 50 items.

Parameters
NameWhereUsage
viewquerymine (default) returns your requests; company returns all accessible company requests and requires a company admin. Example: ?view=company&page=1.
pagequeryOptional page number, 1–1,000,000; defaults to 1. Use ?page=2 to read the next page.

cURL (POSIX shell)

curl --fail-with-body --silent --show-error --request GET \
  "$ESCALIO_PORTAL/api/portal/customer/v1/tickets" \
  --header "Authorization: Bearer $ESCALIO_CUSTOMER_KEY"

Create a request

POST/api/portal/customer/v1/tickets

Requires create. Creates a public customer request as the key’s contact. Returns 201 and an id, including on an identical retry. Customer identity comes from the key, not the payload.

Parameters
NameWhereUsage
Idempotency-KeyheaderPersist a unique ID for this operation; reuse it with the exact same payload when retrying.

ticket.json

{
  "subject": "API connection issue",
  "body": "## What happened\nThe nightly job returned an error.\n\n```text\nrequest_id: demo-42\nstatus: 503\n```",
  "format": "markdown"
}

cURL (POSIX shell)

curl --fail-with-body --silent --show-error --request POST \
  "$ESCALIO_PORTAL/api/portal/customer/v1/tickets" \
  --header "Authorization: Bearer $ESCALIO_CUSTOMER_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $ESCALIO_REQUEST_ID" \
  --data-binary @ticket.json

Python 3

import json, os
from urllib.request import Request, urlopen
from urllib.error import HTTPError

payload = open("ticket.json", "rb").read()
request = Request(os.environ["ESCALIO_PORTAL"].rstrip("/") + "/api/portal/customer/v1/tickets",
    method="POST", data=payload, headers={
        "Authorization": "Bearer " + os.environ["ESCALIO_CUSTOMER_KEY"],
        "Content-Type": "application/json",
        "Idempotency-Key": os.environ["ESCALIO_REQUEST_ID"],
    })
try:
    with urlopen(request, timeout=30) as response:
        print(json.loads(response.read()))
except HTTPError as error:
    raise SystemExit(f"HTTP {error.code}: {error.read().decode()}")

Read a request

GET/api/portal/customer/v1/tickets/{ticket_id}

Requires read. Public messages only, oldest first. Returns ticket, messages, page, page_size (100), total, follow_up_of and follow_ups. Each message includes body, format, author, own, created_at and attachment metadata. Internal notes are never returned.

Parameters
NameWhereUsage
ticket_idpathRequired ticket UUID returned by a create or list operation.
pagequeryOptional page number, 1–1,000,000; defaults to 1. Use ?page=2 to read the next page.

cURL (POSIX shell)

curl --fail-with-body --silent --show-error --request GET \
  "$ESCALIO_PORTAL/api/portal/customer/v1/tickets/$ESCALIO_TICKET_ID" \
  --header "Authorization: Bearer $ESCALIO_CUSTOMER_KEY"

Reply to a request

POST/api/portal/customer/v1/tickets/{ticket_id}/messages

Requires reply. Sends a public reply. A closed request may create a follow-up: use the returned id, which can differ from ticket_id. Identical retries return 201 with the same id.

Parameters
NameWhereUsage
ticket_idpathRequired ticket UUID returned by a create or list operation.
Idempotency-KeyheaderPersist a unique ID for this operation; reuse it with the exact same payload when retrying.

reply.json

{
  "body": "The issue is resolved, thank you.",
  "format": "plain"
}

cURL (POSIX shell)

curl --fail-with-body --silent --show-error --request POST \
  "$ESCALIO_PORTAL/api/portal/customer/v1/tickets/$ESCALIO_TICKET_ID/messages" \
  --header "Authorization: Bearer $ESCALIO_CUSTOMER_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $ESCALIO_REQUEST_ID" \
  --data-binary @reply.json

Revoke this key

POST/api/portal/customer/v1/logout

No ticket scope is required. Immediately revokes this credential. Returns {"revoked": true}; further calls with this key return 401.

cURL (POSIX shell)

curl --fail-with-body --silent --show-error --request POST \
  "$ESCALIO_PORTAL/api/portal/customer/v1/logout" \
  --header "Authorization: Bearer $ESCALIO_CUSTOMER_KEY"

Retry safely

Ticket creation and replies require Idempotency-Key, 8–100 characters. Reuse it only for the same operation, ticket and payload. After a timeout, retry with backoff and the same key: the message may already have been saved. Both first success and identical retries return 201 with {"id": "ticket UUID"}.

Receipts are shared across keys for the same customer account in the same support organization and guaranteed for seven days. A new customer account has a different retry scope. Different operations or payloads under the same key return 409. Do not bypass a conflict with a new ID. A reply to a closed request may return a new follow-up ID; save the returned ID before continuing.

Errors and limits

Up to 20 active keys per company, expiring in 1–30 days. Up to 120 successful API operations per key per minute. Lists contain 50 tickets per page; conversations contain 100 public messages per page. Revoked/expired keys remain in the portal's recent history.

HTTP errors
StatusWhat to do
400Malformed request. Fix the request before retrying.
401The key is invalid, expired, revoked or its sign-in or company access changed. Sign in to the portal and replace it.
403The key lacks the required scope, the customer role does not allow this action, or portal terms must be accepted.
404This ticket is not available to this customer in this portal.
409This Idempotency-Key already belongs to a different operation or payload. Check your saved request.
422Invalid fields, UUID, page, or missing Idempotency-Key. Correct the request.
429Too many requests. Wait at least a minute and retry with backoff.
503Support is temporarily unavailable. Retry with backoff and the same idempotency key for writes.

Errors include code, detail and request_id. Save the request ID for troubleshooting. Never log the key. If your support portal requires acceptance of terms, sign in and accept them before retrying a blocked write.

Customer MCP

Configure a Streamable HTTP MCP client with the same customer key as a Bearer token and the endpoint https://YOUR-PORTAL/api/portal/customer/mcp. Customer MCP does not use the staff OAuth connection.

Available tools: list_requests, get_request, create_request and reply_to_request. The key's scopes determine which tools are offered. Write tools use request_key in their arguments instead of the REST idempotency header. Replies are public; review the content before sending.