Developer reference v1
Clifzo API documentation
A public media link in. A structured, source-dependent result out. Start with a server-side request and keep one request key for each logical task.
Updated
API base URL
https://YOUR_API_HOSTUse the backend API origin shown here, not the address of this documentation site. Examples update from this deployment’s public configuration.
Backend address is not configured here. Replace YOUR_API_HOST with the API origin supplied by the administrator.
Access & authentication
Ask the administrator to create your API account and confirm a credit top-up. Sign in to the private console and create a named API key. There is no public self-service registration or automatic online payment in this version.
Send Authorization: Bearer YOUR_API_KEY on every API request. A key is shown in full only when created; save it securely. Keys in the same account share the wallet and access to that account’s API tasks.
Keep long-lived keys on your server. Do not put them in browser code, mobile application bundles, query strings, analytics or support screenshots. An API key cannot open administrator interfaces.
First request
Submit one supported public source URL. A 202 response accepts a task; it does not mean that parsing has finished.
| Field | Type | Description |
|---|---|---|
url | string · required | An absolute HTTPS source page URL from a supported platform. Extract the URL from share text before sending. |
action | "resolve" · required | The third-party API supports parsing. Server downloads remain administrator-only. |
Idempotency-Key | header · 8–128 bytes | Your stable identifier for one logical request. Reuse it with the same JSON body after a timeout. |
curl 'https://YOUR_API_HOST/v1/api/jobs' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: YOUR_UNIQUE_REQUEST_ID' \
-d '{"url":"https://www.youtube.com/watch?v=VbDupSuRf_Q","action":"resolve"}'Example accepted response. IDs, times, origin and available source formats below are illustrative.
{
"job_id": "019a0000-0000-7000-8000-000000000001",
"status": "queued",
"durability_confirmed": null,
"job_token": null,
"status_url": "https://YOUR_API_HOST/v1/api/jobs/019a0000-0000-7000-8000-000000000001",
"expires_at": "2026-10-23T08:00:00Z",
"request_id": "req_example",
"billing": {
"credits": 1,
"state": "enqueued"
}
}Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /v1/api/jobs | Submit a parsing task |
| GET | /v1/api/jobs/{job_id} | Read task status and result |
| POST | /v1/api/jobs/{job_id}/cancel | Request cancellation |
| GET | /v1/api/balance | Read available and frozen credits |
| GET | /v1/api/ledger | Read account credit transactions |
All endpoints require a valid Bearer key. Ledger pagination uses limit (default 50, maximum 200) and offset. Keys can access only tasks belonging to their account.
Cancellation response · 200 OK
POST /v1/api/jobs/{job_id}/cancel has no JSON body. The response contains job_id, the current status and request_id. A task that already finished can keep its terminal status; cancellation does not undo a successful parse or its charge. Read status again when necessary.
{
"job_id": "019a0000-0000-7000-8000-000000000001",
"status": "cancelled",
"request_id": "req_example"
}Tasks & results
Poll the returned job_id at a bounded interval. Build the result URL from your configured API origin; do not forward your key to arbitrary URLs.
curl 'https://YOUR_API_HOST/v1/api/jobs/JOB_ID' \
-H 'Authorization: Bearer YOUR_API_KEY'queued- Waiting for execution or provider capacity.
running- Parsing is in progress.
succeeded- The task finished. Read result.formats or result.collection.
failed / cancelled / expired- Stop polling. Read error.code, error.message and request_id when present.
The submission response uses job_id; the task detail response identifies the task as id.
{
"id": "019a0000-0000-7000-8000-000000000001",
"status": "succeeded",
"result": {
"title": "Example public work",
"formats": [
{
"format_id": "video-1",
"delivery_type": "direct",
"track_type": "audio_video",
"source_url": "https://media.example/video.mp4"
}
],
"collection": null
},
"result_expired": false,
"request_id": "req_example"
}- A successful HTTP request can contain a failed task: check the task status as well as the HTTP status.
- Source media addresses may expire before the task does. Respect expires_at, content_expires_at and result_expired. A parsed URL is not permanent file storage.
- Formats describe what the source supplied. Inspect delivery_type, track_type and source_url. Separate video/audio tracks may need your own authorized download and merge workflow; an HLS/DASH URL is not a single MP4 file.
Profile collection · selected fields
A profile result uses result.collection.items: each item has id, title, thumbnail_url and webpage_url. This discovers works; it does not include a detailed media result for each work. next_page_url, when present, is a source page URL to submit in a new POST /v1/api/jobs request with action: "resolve". A missing or null next_page_url means no next page is available from this result; stop instead of inventing a cursor. truncated indicates a bounded result, not a guarantee that the whole profile was retrieved.
{
"result": {
"collection": {
"items": [
{
"id": "work_example",
"title": "Example public work",
"thumbnail_url": null,
"webpage_url": "https://www.youtube.com/watch?v=VbDupSuRf_Q"
}
],
"truncated": false
}
}
}Node.js & Python
These examples use built-in HTTP clients and stop after 60 status checks. Set CLIFZO_API_KEY and persist a CLIFZO_REQUEST_ID before the first submission. Keep that request ID when recovering the same logical task; use a new ID only for a deliberate new task.
Node.js
// Node.js 22+; set these variables on your server.
const base = new URL(process.env.CLIFZO_API_BASE || 'https://YOUR_API_HOST').origin;
const key = process.env.CLIFZO_API_KEY;
const requestId = process.env.CLIFZO_REQUEST_ID; // persisted per logical task
if (!key || !requestId) throw new Error('Set API key and stable request ID');
async function call(path, body) {
const response = await fetch(base + path, {
method: body ? 'POST' : 'GET',
headers: { Authorization: 'Bearer ' + key,
...(body ? { 'Content-Type': 'application/json',
'Idempotency-Key': requestId } : {}) },
body: body ? JSON.stringify(body) : undefined,
redirect: 'error', signal: AbortSignal.timeout(15000),
});
const data = await response.json();
if (!response.ok) throw new Error(JSON.stringify(data.error));
return data;
}
const job = await call('/v1/api/jobs', {
url: 'https://www.youtube.com/watch?v=VbDupSuRf_Q', action: 'resolve',
});
for (let attempt = 0; attempt < 60; attempt++) {
await new Promise(resolve => setTimeout(resolve, 3000));
const task = await call('/v1/api/jobs/' + encodeURIComponent(job.job_id));
if (task.status === 'succeeded') { console.log(task.result); break; }
if (['failed', 'cancelled', 'expired'].includes(task.status)) {
throw new Error(JSON.stringify({ job_id: task.id,
error: task.error, request_id: task.request_id }));
}
if (attempt === 59) throw new Error('Polling budget exhausted; resume this job later');
}Python
# Python 3; environment variables are kept on your server.
import json, os, time
from urllib.request import Request, build_opener, HTTPRedirectHandler
base = os.environ.get('CLIFZO_API_BASE', 'https://YOUR_API_HOST').rstrip('/')
key = os.environ['CLIFZO_API_KEY']
request_id = os.environ['CLIFZO_REQUEST_ID'] # persisted per logical task
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
client = build_opener(NoRedirect)
def call(path, body=None):
headers = {'Authorization': 'Bearer ' + key}
data = None
if body is not None:
headers.update({'Content-Type': 'application/json',
'Idempotency-Key': request_id})
data = json.dumps(body).encode()
with client.open(Request(base + path, data=data, headers=headers),
timeout=15) as response:
return json.load(response)
job = call('/v1/api/jobs', {'url': 'https://www.youtube.com/watch?v=VbDupSuRf_Q', 'action': 'resolve'})
for attempt in range(60):
time.sleep(3)
task = call('/v1/api/jobs/' + job['job_id'])
if task['status'] == 'succeeded':
print(json.dumps(task['result'], ensure_ascii=False))
break
if task['status'] in ('failed', 'cancelled', 'expired'):
raise RuntimeError({'job_id': job['job_id'], 'error': task.get('error'),
'request_id': task.get('request_id')})
else:
raise TimeoutError('Polling budget exhausted; resume this job later')Credits & pagination
| Operation | Credits |
|---|---|
| One successfully parsed work | 1 credit |
| One successfully parsed profile page | 2 credits |
| Balance, ledger, status and existing result queries | 0 credits |
| Failed, cancelled, expired or empty paid results | 0 credits |
Submission reserves the task price from available credits. Success consumes the reservation; failure releases it. Internal provider fallback and task recovery do not create extra charges. Request-rate and concurrent-task limits still apply when credits are sufficient.
A profile page is a separate task. Submit result.collection.next_page_url only when you want another page, with a new request key. Each successful next page costs 2 credits; the server does not automatically retrieve an entire profile.
Reading a profile list does not also bill each listed work. Submitting one of those works as a new detailed parsing task costs 1 credit. Empty final pages are not charged.
After a timeout, reuse the original key and body. A timeout may already have accepted the task. Do not create a second charged task simply because the first response did not arrive.
Balance response · 200 OK
GET /v1/api/balance returns account_id, available_credits, frozen_credits and pricing. available_credits can fund new tasks; frozen_credits is reserved for pending settlement. pricing.single and pricing.profile are the per-task credit prices, not currency prices.
{
"account_id": "account_example",
"available_credits": 99,
"frozen_credits": 0,
"pricing": {
"single": 1,
"profile": 2
}
}Ledger response · 200 OK
GET /v1/api/ledger?limit=50&offset=0 returns items, limit and offset, without a total count. Advance offset by the number of items received; stop when fewer than limit are returned. created_at_ms is a Unix timestamp in milliseconds. available_credits and frozen_credits on each entry are the wallet balances after that event.
kind identifies purchase (package top-up), recharge, gift, refund, adjustment, freeze, consume or release. amount is positive for credited value and negative for consumption; an adjustment may have either sign. freeze and release have amount: 0 because they move credits between available and frozen balances without changing the total. Correlate job_id or order_id when present.
{
"items": [
{
"id": "ledger_example",
"account_id": "account_example",
"kind": "consume",
"amount": -1,
"available_credits": 99,
"frozen_credits": 0,
"reason": "Parse succeeded",
"actor": "system",
"job_id": "019a0000-0000-7000-8000-000000000001",
"order_id": null,
"created_at_ms": 1792656000000
}
],
"limit": 50,
"offset": 0
}Errors & retries
Read error.code together with the HTTP status. request_id helps the administrator investigate a failure without receiving your API key. This table covers common cases; additional validation and source-specific task errors can occur.
| HTTP | error.code | What to do |
|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | Include a stable Idempotency-Key header of 8–128 bytes. |
| 401 | API_KEY_REQUIRED / API_KEY_INVALID | Supply an active account API key. Revoked, expired or disabled-account keys are rejected. |
| 402 | INSUFFICIENT_CREDITS | Available credits cannot cover this task. Contact the administrator to add credits. |
| 403 | API_RESOLVE_ONLY | Use action: "resolve". Third-party keys cannot request server downloads. |
| 404 | JOB_NOT_FOUND | The task does not exist, is not available yet, has expired, or belongs to a different account. |
| 409 | IDEMPOTENCY_CONFLICT | This request key was used with a different body. Keep the original body for retries. |
| 410 | API_JOB_EXPIRED / JOB_EXPIRED | The original task expired or was not accepted. A deliberate new parse needs a new request key. |
| 422 | INVALID_URL / UNSUPPORTED_URL | Check the public source URL and the supported platform/link type. |
| 429 | RATE_LIMITED / ACCOUNT_RATE_LIMIT / ACCOUNT_CONCURRENCY_LIMIT | Wait for capacity. Honor Retry-After when present; query requests also count toward the request rate. |
| 503 | QUEUE_FULL / SERVICE_UNAVAILABLE | Capacity or a dependency is unavailable. After uncertain submission, retry with the same request key. |
Integration checklist
- Start with one source URL you are authorized to process; confirm its result before increasing volume.
- Persist your own request identifier, job_id and original public page URL. Do not treat short-lived source media addresses as permanent identifiers.
- Use bounded polling and backoff, honor Retry-After, and stop on terminal task states. Reusing a request key prevents duplicate tasks, not rate-limit consumption.
- Collect only the fields your application needs. Keep credentials and source access restrictions out of customer-facing logs.
Clifzo resolves supported, lawfully accessible public content. Source deletion, sign-in, regional, age, payment, DRM and other access restrictions can prevent a result. The API does not promise universal platform compatibility, permanent media hosting or a particular service level.
Explore integration ideas