API, CLI and MCP
Everything you can do in the app, your code and your agents can do too: submit links, uploads or whole accounts, wait for the tags, and read the results. One contract with three ways in. Jobs are async, writes are idempotent, and you see the cost before and after.
Quickstart
Create an API key in the app under Settings → Developers, set it as GARFUNKEL_API_KEY, then pick your way in. MCP clients can also sign in with OAuth instead of a key.
export GARFUNKEL_API_KEY=gma_live_…
garfunkel submit urls.txt --category sports --dry-run
garfunkel submit urls.txt --category sports --wait
garfunkel results export job_… --format jsonlAuthentication
Send Authorization: Bearer gma_live_… — an org-scoped API key from API keys. The CLI and MCP read GARFUNKEL_API_KEY. Every POST accepts an Idempotency-Key; replays return the original response and never double-charge.
Submit a job
Submitting returns a job id immediately (202). priority is standard (cheaper, minutes to hours) or fast. Set dry_run: true for a free estimate with per-item validation.
curl -X POST https://garfunkel-grub-404s-projects.vercel.app/api/v1/jobs \
-H "Authorization: Bearer $GARFUNKEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"items": [{ "url": "https://www.tiktok.com/@user/video/7412345678901234567" }],
"options": { "translate_to": "en", "shot_detection": true },
"dry_run": true
}'Item statuses: queued → fetching → processing → succeeded | failed | canceled.
Account jobs
“Every post from @handle since 2025-01-01.” Discovery is free; the job then waits in awaiting_confirmation with per-account posts_found, oldest_post_at, reached_since and notices (e.g. when a platform only returns posts back to a later date). Confirm with POST /jobs/{id}/confirm, or set options.max_credits to auto-confirm.
{
"items": [
{ "account": "@nike", "platform": "tiktok", "since": "2025-01-01", "max_posts": 1000 },
{ "account": "https://x.com/nasa", "since": "2025-01-01" }
],
"options": { "max_credits": 50000 }
}Uploads
Bytes go directly to storage, never through the API. Uploads not attached to a job within 24 hours are deleted.
POST /api/v1/uploads
{ "filename": "clip.mp4", "content_type": "video/mp4", "size_bytes": 1234567 }
→ { "id": "upl_…", "upload_url": "<presigned PUT>", "method": "PUT", "headers": {…}, "expires_at": "…" }
# PUT the bytes straight to storage, then submit { "upload_id": "upl_…" } as a job item.Results
One analyzed post. Embedding vectors are returned only with ?include=embedding. pacing comes from shot detection (beta).
{
"item_id": "itm_01J…",
"job_id": "job_01J…",
"source": { "platform": "tiktok", "url": "…", "author": "…", "posted_at": "…",
"metrics": { "views": 0, "likes": 0, "comments": 0, "shares": 0 } },
"caption": "…",
"language": "es",
"transcript": { "text": "…", "segments": [{ "start": 0.0, "end": 2.1, "text": "…" }] },
"translation": { "language": "en", "caption": "…", "transcript": "…", "on_screen_text": "…" },
"summary": "…",
"tags": { "format": "…", "theme": "…", "tone": ["…"], "topics": ["…"], "keywords": ["…"] },
"hook": { "type": "…", "timing_s": 1.2, "confidence": 0.8 },
"pacing": { "beta": true, "shot_count": 14, "avg_shot_s": 2.1, "cuts_per_s": 0.47, "first_cut_s": 0.9 },
"embedding": { "model": "qwen3-vl-embedding-2b", "dims": 1024 },
"credits_charged": 12,
"processed_at": "…"
}Endpoints
| Endpoint | What it does |
|---|---|
| POST /api/v1/jobs | Submit URLs, uploads or accounts (dry_run for a free estimate). |
| GET /api/v1/jobs/{id} | Status, per-status counts, amounts (cents) reserved / charged / released, accounts + notices. |
| GET /api/v1/jobs/{id}/items | Paginated items; filter with ?status=failed. |
| POST /api/v1/jobs/{id}/confirm | Set the price aside for an account job awaiting confirmation. |
| POST /api/v1/jobs/{id}/cancel | Cancel queued items; what was set aside goes back to the balance. |
| POST /api/v1/jobs/{id}/retry | Retry failed items (re-estimated, re-reserved). |
| POST /api/v1/uploads | Presigned PUT for your own media. |
| GET /api/v1/results | Filter by job_id, platform, tag, q. |
| POST /api/v1/results/search | Semantic search over embeddings. |
| GET /api/v1/results/export | Streaming CSV or JSONL. |
| GET /api/v1/credits | Balance (cents + balance_usd), reserved, packs, spending rewards. |
| DELETE /api/v1/data | Delete everything for your org. |
Errors
Every non-2xx uses one envelope. Codes: unauthorized, forbidden, not_found, invalid_request, insufficient_credits, rate_limited, idempotency_conflict, internal_error.
{ "error": { "code": "insufficient_credits",
"message": "Job needs $1.80; your balance is 42¢.",
"details": { "required": 180, "balance": 42 },
"retryable": false } }Item failure codes are stable and never charged:
| post_not_found | The post doesn’t exist (anymore). |
| post_private | The post isn’t public. |
| unsupported_url | Not a TikTok, Instagram or X post URL. |
| media_unavailable | The platform didn’t return the media. |
| media_too_large | Over the size limit. |
| upload_invalid | The uploaded file couldn’t be read. |
| provider_error | Upstream failure — retryable. |
| internal_error | Our failure — retryable. |
CLI
export GARFUNKEL_API_KEY=gma_live_…
garfunkel submit urls.txt --category sports --dry-run # free estimate
garfunkel submit urls.txt --category sports --wait # run it
garfunkel results export job_… --format jsonl -o results.jsonl
# Whole accounts: discovery is free, you confirm before any charge
garfunkel submit --accounts accounts.txt --since 2025-01-01 --max-credits 50000Exit codes: 0 ok, 2 usage, 3 auth, 4 not enough balance, 5 finished with failed items, 6 timeout.
MCP
Remote MCP over Streamable HTTP at /api/mcp with OAuth (add the URL in Claude, ChatGPT or Cursor and approve) or an API key. Tools: estimate_job, submit_job, confirm_job, get_job, wait_for_job, list_results, get_result, search_results, export_results, chat_with_data, get_credit_balance, create_checkout.
{
"mcpServers": {
"garfunkel": {
"type": "http",
"url": "https://garfunkel-grub-404s-projects.vercel.app/api/mcp",
"headers": {
"Authorization": "Bearer gma_live_…"
}
}
}
}