API reference
Clypt API reference.
Base URL: https://useclypt.com/api/v1. All requests use bearer-token auth and JSON bodies. Every response carries an X-Request-Id header you can quote in support questions.
Quick start
Sixty seconds, free, sandbox key. Submit a fake job, watch the status flip, see the fixture output.
# 1. Submit
curl -X POST https://useclypt.com/api/v1/jobs \
-H "Authorization: Bearer clk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": { "type": "audio_url", "url": "https://example.com/episode.mp3" }
}'
# Response (202 Accepted)
# { "id": "job_a1b2...", "status": "queued", ... }
# 2. Poll until status is "complete" or "failed"
curl https://useclypt.com/api/v1/jobs/job_a1b2... \
-H "Authorization: Bearer clk_test_YOUR_KEY"
# 3. When status is "complete", output.clips[] is populated.Wall-clock to terminal: 10-70s on sandbox keys (deterministic fixture, no real pipeline). ~2-5 min on live keys for audio and RSS sources, ~10-12 min for video sources that opt into a trailer.
Authentication
Every request needs an Authorization: Bearer <api_key> header. Keys come in two environments:
clk_live_…— production. Runs the real pipeline, bills against your account.clk_test_…— sandbox. Returns deterministic fixtures, never bills, never touches Deepgram or R2. Same base URL.
The prefix on the key is decorative — the server checks the key’s stored environment against the prefix and refuses if they disagree.
Source types
The source.type discriminator picks how Clypt acquires the underlying media. The downstream pipeline is identical regardless.
| type | url shape | Trailer | Status |
|---|---|---|---|
video_url | Public MP4 (HTTPS) | Yes | Live |
audio_url | Public MP3 or M4A | No | Live |
rss_feed_url | Podcast RSS feed (most recent episode) | No | Live |
youtube_url | YouTube watch / shorts / youtu.be URL | Yes | Live. Ingestion adds ~3-8 min wall-clock for the video download before the standard pipeline runs. Failures (private / region-locked / deleted / bot-detection) surface as youtube_ingestion_failed; no charge on failure. |
Submit a job
Returns 202 Accepted with a queued job envelope. The pipeline runs asynchronously — poll GET /v1/jobs/{id} or register a webhook to be notified.
Body
{
"source": {
"type": "video_url",
"url": "https://r2.example.com/episode.mp4"
},
"options": {
"include_trailer": true,
"caption_style": "yellow-pop" // optional override
}
}Response (202)
{
"id": "job_4f8d2a1c9e...",
"object": "job",
"status": "queued",
"source": { "type": "video_url", "url": "..." },
"options": { "include_trailer": true },
"created_at": "2026-05-19T12:00:00Z",
"completed_at": null,
"output": null,
"error": null
}Get a job
Returns the current job envelope. Status walks queued → processing → complete or queued → processing → failed. Once terminal, the row never changes again.
Output shape (status=complete)
{
"id": "job_4f8d2a1c9e...",
"status": "complete",
"completed_at": "2026-05-19T12:05:00Z",
"output": {
"clips": [
{
"id": "clip_xxx",
"rank": 1,
"category": "Bold/Contrarian",
"hook": "First-line text the clip leads with.",
"pull_quote": "...",
"start_time": 197.6,
"end_time": 228.7,
"duration_seconds": 31.0,
"rendered_urls": {
"linkedin": "https://r2.useclypt.com/.../9x16.mp4",
"x": "https://r2.useclypt.com/.../16x9.mp4"
},
"transcript_text": "..."
}
],
"trailer": {
"url": "https://r2.useclypt.com/.../trailer.mp4",
"duration_seconds": 67.4
},
"guest_share_url": "https://useclypt.com/guest/...",
"show_notes": "# Markdown show notes...",
"transcript": [
{ "speaker": "host", "start": 0.5, "end": 4.2, "text": "..." }
]
},
"error": null
}When status=failed: output is null and error carries { code, message }. See Errors.
List jobs
Returns the org’s recent jobs newest-first. Cursor pagination via starting_after — pass the last job’s id from the previous page.
Query params
limit— 1-100, default 20starting_after— cursor (a job id)
{
"object": "list",
"has_more": true,
"data": [ { "id": "job_...", "status": "complete", ... }, ... ]
}Webhooks
Register an HTTPS endpoint and Clypt will POST signed job.completed and job.failed events to it. No polling required.
Register
{
"url": "https://your.app/webhooks/clypt",
"events": ["job.completed", "job.failed"]
}Response includes a secret (whsec_…) — shown once at registration, never returned again. Store it; use it to verify signatures.
Signature header
Every delivery carries X-Clypt-Signature: t=<unix_ts>,v1=<hmac_sha256_hex> (Stripe-shape). The signed payload is `${t}.${raw_body}`.
// Node.js verification snippet
import { createHmac, timingSafeEqual } from "crypto";
export function verifyClyptSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=", 2))
);
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex")
);
}Reject deliveries with a t older than 5 minutes to prevent replay attacks against your endpoint.
Event shape
{
"id": "evt_...",
"object": "event",
"type": "job.completed",
"created": 1747500000,
"request_id": "req_...",
"data": {
"object": { /* same shape as GET /v1/jobs/{id} */ }
}
}Retries
Any non-2xx response (or timeout, or connection error) schedules a retry. Backoff: 1s, 10s, 1m, 10m, 1h, 6h, 24h. After 7 failed attempts the delivery is marked failed. 10 consecutive failed deliveries deactivates the webhook entirely (you can re-register it).
Test + replay
POST /v1/webhooks/{id}/test fires a synthetic job.completed event derived from your most recent real job — useful for wiring signature verification before a real submission. POST /v1/webhooks/deliveries/{id}/replay re-emits any past delivery, reusing the original request_id so your receiver can dedupe.
Errors
Every non-2xx response carries a JSON error envelope:
{
"error": {
"type": "invalid_request_error",
"code": "invalid_source_url",
"message": "source.url must be a valid HTTPS URL.",
"param": "source.url",
"request_id": "req_..."
}
}type is one of invalid_request_error, authentication_error, rate_limit_error, idempotency_error, api_error. Branch on code for specific handling.
| code | type | When |
|---|---|---|
missing_authorization | authentication_error | Authorization header absent. |
invalid_api_key_format | authentication_error | Key doesn't start with clk_live_ or clk_test_. |
invalid_api_key | authentication_error | Key not found. |
api_key_revoked | authentication_error | Key was revoked. |
invalid_json | invalid_request_error | Body isn't valid JSON. |
invalid_body | invalid_request_error | Body isn't a JSON object. |
invalid_source_type | invalid_request_error | source.type isn't one of the supported values. |
invalid_source_url | invalid_request_error | source.url is malformed for the chosen type. |
youtube_ingestion_failed | invalid_request_error | YouTube download failed (private, region-locked, deleted, or bot-detection blocked). Job lands in 'failed' state; no charge. |
trailer_not_supported_for_source | invalid_request_error | options.include_trailer set on audio_url or rss_feed_url. |
invalid_events | invalid_request_error | Webhook events array is empty or contains unsupported values. |
idempotency_key_reused_with_different_body | idempotency_error | Same Idempotency-Key submitted with a different body inside the 24h window. |
too_many_requests | rate_limit_error | Per-key budget exhausted. Retry-After header set. |
not_found | invalid_request_error | Job, webhook, or delivery ID doesn't exist for this org. |
This table covers the codes a normal caller will see. Internal codes (e.g., insert failures) are logged with the same envelope shape but aren’t enumerated here.
Idempotency
Pass an Idempotency-Key header on POST /v1/jobs (and only that route). 24-hour replay window.
- Same key + same body → returns the original job (with the same id).
- Same key + different body → 409
idempotency_key_reused_with_different_body. - Keys can be any 8-255 char string. UUIDs work; so does anything that hashes deterministically off your input.
Rate limits
Budgets are per-API-key, sliding 60-second windows.
- 60 writes / 60s on
POST/DELETEendpoints (submissions, webhook CRUD, test/replay). - 600 reads / 60s on
GETendpoints (status polling, lists).
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds). On 429 a Retry-After header is set too.
If you need higher limits, email nelson@useclypt.com with your use case.
Sandbox
Sandbox keys (clk_test_…) hit the same base URL as live keys but skip the pipeline entirely — every submission returns a deterministic fixture 10-70s later. No transcription cost, no R2 storage, no Stripe usage record. Webhooks fire as if from a real job, so you can wire your integration end-to-end before committing to live.
| Submission pattern | Outcome |
|---|---|
source.type = youtube_url (any URL) | Failed, error.code = youtube_ingestion_failed |
source.url contains fail (case-insensitive) | Failed, error.code = transcription_failed |
| Malformed URL | Rejected at submit with invalid_source_url |
| Anything else | Success fixture: 3 clips, optional trailer (video sources only), show notes, transcript stub. |
MCP server
The MCP server is a thin shim over the same API. Three tools — submit_job, get_job, list_jobs — over stdio MCP transport. Works with Claude Desktop, Cursor, Claude Code, and any MCP-compliant host.
# Claude Desktop config (macOS):
# ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"clypt": {
"command": "npx",
"args": ["-y", "@useclypt/mcp-server"],
"env": { "CLYPT_API_KEY": "clk_test_..." }
}
}
}All endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/jobs | Submit a new job. |
| GET | /v1/jobs/{id} | Get a job's current state. |
| GET | /v1/jobs | List recent jobs. |
| POST | /v1/webhooks | Register a webhook. |
| GET | /v1/webhooks | List webhooks. |
| DELETE | /v1/webhooks/{id} | Delete a webhook. |
| POST | /v1/webhooks/{id}/test | Fire a synthetic test event. |
| GET | /v1/webhooks/{id}/deliveries | List deliveries for a webhook. |
| POST | /v1/webhooks/deliveries/{id}/replay | Replay a delivery. |