Skip to content

API reference

The Raclip HTTP API exposes everything you can do programmatically: list devices, fetch recordings, get statistics, receive webhook events. Every public endpoint lives at https://api.raclip.ai.

Base URL

https://api.raclip.ai

All public endpoints are under the /api/ prefix. Non-/api/ paths are reserved for device-to-cloud traffic and are not part of the public contract.

Authentication

Every request must carry a Bearer token:

Authorization: Bearer raclip_prod_a1b2c3d4e5f6g7h8_9ijklmno

The token is your API key, generated in the console under API Keys. Keys are organization-scoped — the gateway derives your org_id from the key, so you never include it in requests.

See Get an API key for the generation flow.

Errors

Errors return a uniform JSON body with the appropriate HTTP status code:

{ "detail": "Audio call not found" }
Status Meaning
400 Bad request — invalid JSON or malformed parameters.
401 Missing or invalid API key.
403 Authenticated, but not allowed (e.g. accessing another org's device).
404 Resource doesn't exist.
422 Validation failed (e.g. parameter out of allowed range).
429 Rate-limit exceeded. Honor Retry-After.
5xx Server error. Retry with exponential backoff.

Pagination

Endpoints that return lists use uniform pagination parameters:

Param Default Range
page 1 ≥ 1
per_page 50 1100

The response includes total, page, and per_page so you can iterate:

{
  "calls": [  ],
  "total": 137,
  "page": 1,
  "per_page": 50
}

Versioning

URLs are stable. Changes are additive — new fields may appear on response objects, but existing fields will not be removed or change shape. Breaking changes will ship under a new path prefix.


Calls

Endpoints for listing, retrieving, and managing recordings. All paths require the Authorization: Bearer <api_key> header.

List recordings

GET /api/raclip/calls/{device_id}

Paginated list of a device's recordings, newest first.

Path parameters

Name Type Description
device_id UUID Device identifier. Find it via the console or client.devices.list().

Query parameters

Name Type Default Description
page int 1 Page number, ≥ 1.
per_page int 50 Items per page, 1–100.
days_back int 30 Limit results to the last N days, 1–365.
include_urls bool true If true, response items include download_url values.

Response200 OK

{
  "device_id": "8f1a4b2e-…",
  "calls": [
    {
      "call_id": "abc123",
      "duration_seconds": 42.7,
      "file_size_bytes": 685000,
      "created_at": "2026-04-29T10:30:45Z",
      "download_url": "https://…?signature=…"
    }
  ],
  "total": 137,
  "page": 1,
  "per_page": 50
}

Get the latest recording

GET /api/raclip/calls/{device_id}/latest

The most recent recording for a device. Useful for "fetch what just happened" workflows.

Query parameters

Name Type Default Description
download bool true If true, response includes a download_url.

Response200 OK

{
  "call_id": "abc123",
  "device_id": "8f1a4b2e-…",
  "duration_seconds": 42.7,
  "file_size_bytes": 685000,
  "created_at": "2026-04-29T10:30:45Z",
  "download_url": "https://…?signature=…"
}

404 Not Found if the device has no recordings yet.

Get a recording by ID

GET /api/raclip/calls/{device_id}/{call_id}

Same shape as /latest, but for a specific recording.

Query parameters

Name Type Default Description
download bool true If true, response includes a download_url.

Get recording metadata only

GET /api/raclip/calls/{device_id}/{call_id}/info

Same data as GET /{call_id} minus the download URL. Useful when you've already cached the URL or just want to inspect metadata cheaply.

Response200 OK

{
  "call_id": "abc123",
  "device_id": "8f1a4b2e-…",
  "duration_seconds": 42.7,
  "file_size_bytes": 685000,
  "created_at": "2026-04-29T10:30:45Z"
}

Download a recording directly

GET /api/raclip/calls/{device_id}/{call_id}/download

Issues a 302 Found redirect to a short-lived download_url. Follow the redirect to download the audio.

Query parameters

Name Type Default Description
expiration int 3600 URL TTL in seconds.

Don't forward auth headers to the download URL

The download_url rejects extra Authorization headers. If your HTTP client persists headers across redirects, drop the Authorization header before following the 302.

curl -H "Authorization: Bearer $RACLIP_API_KEY" -L \
  -o recording.mp3 \
  https://api.raclip.ai/api/raclip/calls/<device_id>/<call_id>/download

curl -L automatically drops cross-origin auth headers, so the example above works as written.

Delete a recording

DELETE /api/raclip/calls/{device_id}/{call_id}

Permanently removes the recording. There is no soft-delete or undo.

Response200 OK

{ "status": "success", "message": "Call abc123 deleted successfully" }

404 Not Found if the recording doesn't exist.

Statistics

GET /api/raclip/calls/{device_id}/statistics

Aggregate counts and durations for a device.

Query parameters

Name Type Default Description
days_back int 30 Window to aggregate over, 1–365.

Response200 OK

{
  "total_calls": 137,
  "total_duration_seconds": 5482.3,
  "total_size_bytes": 87654321,
  "avg_duration_seconds": 40.0,
  "first_call": "2026-04-01T08:14:22Z",
  "latest_call": "2026-04-29T10:30:45Z"
}

Usage summary

GET /api/raclip/calls/{device_id}/storage-summary

Usage broken down with derived metrics — total recordings, total duration, and per-recording averages. Same input as /statistics, with a richer response.

Query parameters

Name Type Default Description
days_back int 30 Window to summarize over, 1–365.

Response200 OK

{
  "device_id": "8f1a4b2e-…",
  "period_days": 30,
  "total_calls": 137,
  "total_duration_hours": 1.52,
  "storage": {
    "total_bytes": 87654321,
    "total_mb": 83.59,
    "total_gb": 0.082,
    "average_file_mb": 0.61
  },
  "efficiency": {
    "mb_per_hour": 54.99,
    "bytes_per_second": 15983
  },
  "period": {
    "first_call": "2026-04-01T08:14:22Z",
    "latest_call": "2026-04-29T10:30:45Z"
  }
}

Webhooks

Webhooks deliver events to your server as HTTP POSTs. This section is the receiver-side contract: payload shape, headers, signature verification, retries.

For the configuration flow (URL + secret), see Webhooks in Getting Started.

Events

Event When it fires
recording.completed A device finished uploading audio and the recording is available via the API.

More events are planned. Your handler should ignore unknown type values gracefully.

Delivery

Each event is sent as an HTTP POST with a JSON body to the URL you configured in the console.

Request headers

Header Description
Content-Type Always application/json.
User-Agent Raclip-Webhooks/1.0.
X-Webhook-Event-Id Unique event ID, e.g. evt_a1b2c3d4e5f6. Use this for idempotency.
X-Webhook-Event-Type The event type, e.g. recording.completed.
X-Webhook-Timestamp Unix timestamp (seconds) the delivery was generated.
X-Webhook-Signature sha256=<hex> HMAC-SHA256 over {timestamp}.{raw_body}. Only sent when a secret is configured.

Body — recording.completed

{
  "id": "evt_a1b2c3d4e5f6",
  "type": "recording.completed",
  "created_at": "2026-04-29T10:30:45Z",
  "data": {
    "org_id": "…",
    "device_id": "8f1a4b2e-…",
    "call_id": "abc123",
    "recording_id": "abc123",
    "duration_seconds": 42.7,
    "file_size_bytes": 685000,
    "start_timestamp": "2026-04-29T10:30:00Z",
    "end_timestamp": "2026-04-29T10:30:42Z",
    "presigned_url": "https://…?signature=…",
    "presigned_url_expires_at": "2026-04-29T11:30:45Z",
    "presigned_url_ttl_seconds": 3600
  }
}

recording_id and call_id are currently identical and refer to the same recording — recording_id is the field name we'll standardize on going forward.

presigned_url is valid for presigned_url_ttl_seconds (currently 1 hour). After that, regenerate one via GET /api/raclip/calls/{device_id}/{call_id}/download.

Verify signatures

When you configure a secret in the console, every delivery is signed with HMAC-SHA256.

The signed string is {timestamp}.{raw_body} — the literal X-Webhook-Timestamp value, a ., then the raw bytes of the request body. Re-serializing the JSON will produce a different signature.

Python

import hmac, hashlib

def verify_webhook(headers: dict, body: bytes, secret: str) -> bool:
    signature = headers.get("X-Webhook-Signature", "")
    timestamp = headers.get("X-Webhook-Timestamp", "")
    if not signature or not timestamp:
        return False

    signed_payload = f"{timestamp}.".encode("utf-8") + body
    expected = hmac.new(
        secret.encode("utf-8"),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, f"sha256={expected}")

Node.js

const crypto = require("crypto");

function verifyWebhook(headers, rawBody, secret) {
  const signature = headers["x-webhook-signature"] || "";
  const timestamp = headers["x-webhook-timestamp"] || "";
  if (!signature || !timestamp) return false;

  const signedPayload = Buffer.concat([
    Buffer.from(`${timestamp}.`, "utf8"),
    rawBody,
  ]);
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected),
  );
}

Always use a constant-time comparison (hmac.compare_digest / crypto.timingSafeEqual) — a regular == check leaks timing information.

Retries

Raclip retries failed deliveries up to 4 times total with exponential backoff:

Attempt Delay before
1 0 s
2 1 s
3 4 s
4 16 s

Per-attempt timeout is 10 seconds. Any non-2xx response or network error counts as a failure.

After the 4th failure, the event is dropped — there's no dead-letter queue today, so make sure your endpoint can handle the load you'd expect.

Best practices

  • Acknowledge fast. Return 2xx within 10 seconds. Push real work to a background queue.
  • Idempotency. Use X-Webhook-Event-Id to deduplicate — retries can deliver the same event multiple times.
  • Verify signatures. Always — even on internal networks.
  • Tolerate new fields. We add fields additively; ignore ones you don't recognize.
  • Use the presigned_url while it's hot. It's valid for an hour. If you can't process the recording within that window, store the call_id and regenerate a URL via the API later.