Skip to content
Garfunkel by GAR AI

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 jsonl

Authentication

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

EndpointWhat it does
POST /api/v1/jobsSubmit 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}/itemsPaginated items; filter with ?status=failed.
POST /api/v1/jobs/{id}/confirmSet the price aside for an account job awaiting confirmation.
POST /api/v1/jobs/{id}/cancelCancel queued items; what was set aside goes back to the balance.
POST /api/v1/jobs/{id}/retryRetry failed items (re-estimated, re-reserved).
POST /api/v1/uploadsPresigned PUT for your own media.
GET /api/v1/resultsFilter by job_id, platform, tag, q.
POST /api/v1/results/searchSemantic search over embeddings.
GET /api/v1/results/exportStreaming CSV or JSONL.
GET /api/v1/creditsBalance (cents + balance_usd), reserved, packs, spending rewards.
DELETE /api/v1/dataDelete 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_foundThe post doesn’t exist (anymore).
post_privateThe post isn’t public.
unsupported_urlNot a TikTok, Instagram or X post URL.
media_unavailableThe platform didn’t return the media.
media_too_largeOver the size limit.
upload_invalidThe uploaded file couldn’t be read.
provider_errorUpstream failure — retryable.
internal_errorOur 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 50000

Exit 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_…"
      }
    }
  }
}