Developers

API reference

A REST API over HTTPS with JSON in and JSON — or audio — out. Every route below is mounted and auth-gated.

Base URL

Every route on this page lives under https://api.arapulse.com/api/v1. Requests are JSON over HTTPS; responses are JSON, or the audio file itself on the synthesis endpoints. The one exception to the prefix is GET /health, the bare liveness probe.

Authentication

Two credential types. A sk_live_ or sk_test_ secret key authenticates server-to-server calls and must never reach a browser. For client-side use, mint a short-lived st_ session token from your own backend and hand that over instead.

Authorization header
# Server to server — a secret key.
curl https://api.arapulse.com/api/v1/characters \
  -H "Authorization: Bearer sk_live_..."

# Browser — a short-lived session token you minted server-side.
curl https://api.arapulse.com/api/v1/avatars/abc/chat \
  -H "Authorization: Bearer st_..."

Key prefixes are sk_live_ and sk_test_. Earlier documentation referred to a ck_live_ prefix. That format was never issued — if you are matching on it, that is why nothing validates.

Errors and limits

Errors come back with a consistent envelope and a meaningful status code. Quota exhaustion is a 429 and is refused rather than silently billed.

Error response
{
  "success": false,
  "error": {
    "message": "Daily quota exceeded for this API key",
    "statusCode": 429,
    "timestamp": "2026-08-01T19:14:08.508Z"
  }
}
  • 401 — missing or invalid credential.
  • 403 — the key is valid but lacks the scope.
  • 429 — rate limit or daily quota exhausted.
  • 501 — the capability exists but is not connected yet, such as adding a payment method.
  • 503 — an upstream model container is unavailable. Check status.

Rate limiting is applied per key across the whole /api/v1 surface, and daily quotas are enforced on every key-authenticated route rather than only at the edge.

Live sessions

Create a companion session, exchange turns, and issue viewer tokens. Sessions are published into a WebRTC room.

MethodPathDescription
POST/api/v1/avatarsCreate a real-time avatar session.
POST/api/v1/avatars/:id/chatSend a turn and receive her reply.
POST/api/v1/avatars/:id/viewer-tokenIssue an extra viewer token for the room.
GET/api/v1/avatars/:idFetch session state.
DELETE/api/v1/avatars/:idEnd the session and release capacity.

Speech

Synthesis and transcription. Both run on self-hosted models.

MethodPathDescription
POST/api/v1/text-to-speechSynthesise a complete WAV.
POST/api/v1/text-to-speech/streamSynthesise in chunks for early playback.
POST/api/v1/text-to-speech/with-timestampsAudio plus per-word offsets (interpolated).
POST/api/v1/speech-to-textTranscribe multipart or raw audio.
POST/api/v1/speech-to-text/streamProxies upstream; returns one response, not a live feed.
GET/api/v1/speech-to-text/healthUpstream transcription health.

Voices

Manage the voice library. Submit a reference clip and the clone is fetched, transcribed and registered; poll the voice until it reports active or failed.

MethodPathDescription
GET/api/v1/voicesList available and cloned voices.
POST/api/v1/voicesCreate a voice. With samples, cloning runs and the voice activates.
GET/api/v1/voices/:idFetch one voice.
DELETE/api/v1/voices/:idDelete a voice.

Characters

A character carries appearance, personality and its bound voice together.

MethodPathDescription
GET/api/v1/charactersList characters.
POST/api/v1/charactersCreate a character.
GET/api/v1/characters/:idFetch one character.
PATCH/api/v1/characters/:idUpdate appearance or personality.
DELETE/api/v1/characters/:idDelete a character.

Keys and session tokens

Mint a short-lived token server-side and hand that to the browser. Your secret key never reaches a client.

MethodPathDescription
POST/api/v1/sessions/tokenMint a short-lived browser token (st_).
GET/api/v1/sessionsList active session tokens.
DELETE/api/v1/sessions/:idRevoke a session token.
GET/api/v1/developer-sessionsDeveloper-side session management.
GET/api/v1/admin/keysManage API keys.

Account, usage and teams

Profile, analytics, plans and organisation membership.

MethodPathDescription
GET/api/v1/userProfile and settings.
GET/api/v1/analyticsUsage by key and endpoint.
GET/api/v1/billing/plansAvailable plans. Public.
GET/api/v1/billing/currentCurrent subscription.
GET/api/v1/organizationsOrganisations, roles and shared quota.
GET/api/v1/webhooksManage endpoints and inspect recent deliveries.
POST/api/v1/webhooks/:id/testFire a test delivery at your endpoint.
DELETE/api/v1/user/accountErase the account. `?dryRun=true` reports without deleting.

Public

The only endpoints that need no credentials at all.

MethodPathDescription
GET/api/v1/statusService health. Rate limited, cached 15s.
GET/api/v1/demo/voicesVoices the homepage demo may use.
POST/api/v1/demo/ttsHomepage demo synthesis. Tight per-IP limit.
GET/healthPlatform liveness.

Webhooks

Register an endpoint and subscribe it to any of the seven events. Deliveries retry with backoff on failure, and recent attempts are visible on the webhook record.

EventFires when
session.startedA live companion session opens.
session.endedA session closes — explicitly, by idle reap, or by eviction.
character.createdA character is created.
character.updatedA character changes. Carries changed field names, not values.
character.deletedA character is deleted.
tts.completedSpeech synthesis finishes.
voice.createdA voice becomes usable, including after a clone completes.

Verifying a delivery

Each request carries an X-AraPulse-Signature-V2 header of the form t=<timestamp>,v1=<signature>. The signature is an HMAC-SHA256 over `${timestamp}.${rawBody}` using your webhook secret.

Verify before you trust the body
import crypto from "crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map(p => p.split("="))
  );

  // Reject replays before spending time on the digest.
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!(age < 300)) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");

  // Constant time — a plain === leaks the digest byte by byte.
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  );
}

Compare in constant time and check the timestamp. The timestamp is inside the signed payload specifically so a captured request cannot be replayed later. Reject anything older than five minutes, and use a timing-safe comparison — a plain === on the digest leaks it byte by byte.

Spec coverage

The OpenAPI document currently describes a subset of the mounted routes, so this page is ahead of the machine-readable spec. If you are generating a client from the spec, expect gaps in analytics, billing, organisations, webhooks and speech-to-text, and refer to this page for those.

Get a key

Test keys on the free tier. No card.