Skip to main content
REST API

Run PraxTalk from your own CRM.

Every conversation, lead, and brand is reachable via REST. Use it to power custom inboxes, sync to your data warehouse, or build automation on top of PraxTalk events.

Authentication

Every request needs a workspace API key in the Authorization header.

Generate a key in the dashboard at /app/integrations → API keys. Keys come in two scopes:

  • read — list and fetch endpoints only
  • write — list, fetch, and mutating endpoints (send message, change status, create lead)

Keys can be brand-scoped if you only want to expose one brand.

Authorization: Bearer ptk_live_<your-secret>

The full secret is shown once at create time and never again — copy it to your secret manager immediately. Compromised keys can be revoked from the same dashboard.

Base URL

Every endpoint lives under your workspace's Convex deployment host.

Your base URL is shown on the API keys page. It looks like:

https://<your-deployment>.convex.site

All endpoints below are relative to this base. CORS is open (Access-Control-Allow-Origin: *) so other backends in your stack can call it from anywhere.

⚠ Don't embed ptk_live_ keys in front-end JavaScript. Anyone who views your bundle (or scrapes it) can extract the key and read or mutate everything in your workspace until you notice + revoke. The wildcard CORS lets the API answer cross-origin requests from your backend code; it should not be read as a green-light for shipping the secret into a browser. For browser-driven flows (a visitor widget, an in-page chat), use PraxTalk's widget or mint short-lived tokens in your own backend and hand them to the page.

Rate limits

60 req/min per IP, plus 6,000/min per read-scope key and 600/min per write-scope key. Errors include Retry-After + X-RateLimit-* headers.

Two layered limits apply to every authenticated request:

  • Per-IP — 60 req/min. Catches malformed clients and brute-force attempts.
  • Per-key — 6,000 req/min for read-scope keys, 600 req/min for write-scope keys. Bounds the blast radius of a leaked key + prevents one tenant from starving another.

When you exceed either limit, the API returns 429 Too Many Requests with these headers:

Retry-After: 12
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0

Honour Retry-After — well-behaved clients back off automatically. Need a higher limit? Email hello@praxtalk.com.

Endpoints

All endpoints are JSON in / JSON out. Errors come back as { error: string }.

Brands

GET /api/v1/brands
→ { brands: [{ _id, slug, name, primaryColor, ... }] }

Conversations

GET /api/v1/conversations?status=open&brandId=...&limit=50
→ { conversations: [...] }

GET /api/v1/conversations/:id
→ { conversation: { ..., visitor, brand, messages: [...] } }

PATCH /api/v1/conversations/:id
Body: { status: "open" | "snoozed" | "resolved" | "closed" }
→ { ok: true }

POST /api/v1/conversations/:id/messages
Body: { body: "Reply text" }
→ { messageId: "..." }

Messages

GET /api/v1/messages?conversationId=...
→ { messages: [{ _id, role, body, channel, createdAt }] }

Leads

GET /api/v1/leads?status=new&brandId=...
→ { leads: [...] }

POST /api/v1/leads
Body: { name, email?, phone?, notes?, status?, brandId?, conversationId? }
→ { leadId: "..." }

PATCH /api/v1/leads/:id
Body: { status?, notes?, name? }
→ { ok: true }

Health

GET /api/v1/ping
→ { ok: true, workspaceId: "..." }

Webhooks

Subscribe to real-time events instead of polling. HMAC-signed, retried with exponential backoff.

Configure subscriptions in the dashboard at /app/integrations → Webhooks. Each subscription gets its own signing secret.

Events you can subscribe to:

  • conversation.created
  • conversation.status_changed
  • message.created
  • lead.created
  • lead.status_changed

Signature verification

Every POST includes an X-PraxTalk-Signature header in the format t=<unix-seconds>,v1=<hex> — the same scheme Stripe uses. Verify by computing HMAC-SHA256(secret, <t>.<rawBody>) and comparing to v1 in constant time. Reject anything with a timestamp older than 5 minutes to prevent replay.

// Node example
import crypto from "crypto";

function verify(rawBody, header, secret) {
  const [tPart, v1Part] = header.split(",");
  const t = tPart.split("=")[1];
  const sig = v1Part.split("=")[1];
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(sig, "hex"),
  );
}

Retries

Non-2xx responses are retried up to 6 times across roughly 7 hours. Failures are visible in the dashboard — you can manually replay them from the integrations page.

End-to-end example

Send a reply from your own CRM.

# 1. List open conversations
curl https://<your-deployment>.convex.site/api/v1/conversations?status=open \
  -H "Authorization: Bearer ptk_live_abc123..."

# 2. Pick one and send a reply
curl -X POST https://<your-deployment>.convex.site/api/v1/conversations/<id>/messages \
  -H "Authorization: Bearer ptk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "body": "Hi! How can I help?" }'

# 3. Mark resolved
curl -X PATCH https://<your-deployment>.convex.site/api/v1/conversations/<id> \
  -H "Authorization: Bearer ptk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "status": "resolved" }'

Error reference

Status codes you should expect and handle.

  • 401 — missing, malformed, or revoked API key
  • 403 — key has read-only scope, or is brand-scoped and you tried to act on another brand
  • 404 — resource doesn't exist or belongs to another workspace
  • 422 — request body failed validation; the error field explains
  • 429 — rate limited; honour Retry-After
  • 5xx — transient server issue; retry with backoff

Need something not covered here?

Email hello@praxtalk.com with your use case. Custom endpoints, higher rate limits, IP allowlist — all on the table for paying customers.