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¶
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:
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:
| 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 |
1 – 100 |
The response includes total, page, and per_page so you can iterate:
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¶
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. |
Response — 200 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¶
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. |
Response — 200 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¶
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¶
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.
Response — 200 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¶
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¶
Permanently removes the recording. There is no soft-delete or undo.
Response — 200 OK
404 Not Found if the recording doesn't exist.
Statistics¶
Aggregate counts and durations for a device.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
days_back |
int | 30 |
Window to aggregate over, 1–365. |
Response — 200 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¶
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. |
Response — 200 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
2xxwithin 10 seconds. Push real work to a background queue. - Idempotency. Use
X-Webhook-Event-Idto 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_urlwhile it's hot. It's valid for an hour. If you can't process the recording within that window, store thecall_idand regenerate a URL via the API later.