Developer reference

A small, predictable API.

Base URL: https://api.twitterdownloads.com

Authentication

Verify your account, claim trial credits and create a key in the dashboard. Send it as a Bearer header. Do not put keys in URLs or public browser code.

Resolve one post

curl 'https://api.twitterdownloads.com/v1/resolve' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: unique-request-001' \
  --data '{"url":"https://x.com/username/status/2104150555035857259"}'

JavaScript

const response = await fetch('https://api.twitterdownloads.com/v1/resolve', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.TWITTERDOWNLOADS_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({ url: postUrl })
});
const result = await response.json();

Python

import json, os, urllib.request, uuid
request = urllib.request.Request('https://api.twitterdownloads.com/v1/resolve',
    data=json.dumps({'url': post_url}).encode(),
    headers={'Authorization': 'Bearer ' + os.environ['TWITTERDOWNLOADS_API_KEY'],
             'Content-Type': 'application/json',
             'Idempotency-Key': str(uuid.uuid4())}, method='POST')
with urllib.request.urlopen(request) as response:
    result = json.load(response)

Response

{
  "post": {
    "id": "…",
    "url": "…",
    "text": "…",
    "author": {
      "name": "…",
      "username": "…"
    }
  },
  "media": [
    {
      "id": "…",
      "type": "video",
      "thumbnail": "…",
      "duration_ms": null,
      "variants": [
        {
          "url": "https://video.twimg.com/…mp4",
          "content_type": "video/mp4",
          "width": 1280,
          "height": 720,
          "label": "720p",
          "bitrate": null
        }
      ]
    }
  ],
  "cached": false,
  "request_id": "…",
  "credits_used": 1
}

Animated GIF media may have type gif but the downloadable file remains MP4. Width, height, duration or bitrate may be null when unavailable.

Balance and usage

GET /v1/balance returns available credits and their batches, including remaining, reserved and expiry. GET /v1/usage returns status totals and the latest 30 calls. Both require your Bearer key.

Idempotency

Use an 8–128-character Idempotency-Key of letters, digits, underscores or hyphens. Reuse it only for the same post. Completed responses are retained for 24 hours and replayed without charging again. A different post with the same key returns 409. An active duplicate returns REQUEST_IN_PROGRESS; retry after the indicated delay.

Completed failures are also replayed. To try the source again after a completed failure, use a new key. After 24 hours, a reused key can become a new billable request.

Errors

StatusCodeNext step
400INVALID_URL / INVALID_REQUESTSend one valid HTTPS post URL.
401INVALID_API_KEYCheck or replace your key.
402CREDITS_EXHAUSTEDCheck your available credit batches.
409IDEMPOTENCY_CONFLICT / REQUEST_IN_PROGRESSCheck the key or retry later.
422NO_VIDEOThe returned post has no downloadable MP4.
429RATE_LIMITED / CONCURRENCY_LIMIT / TRIAL_DAILY_LIMITWait for Retry-After.
503UPSTREAM_UNAVAILABLE / UPSTREAM_TIMEOUT / SERVICE_BUSYRetry later.

Limits and headers

Paid accounts: 60 resolve requests/minute, 3 concurrent. Trial: 10/minute, 1 concurrent, 20 successful parses per UTC day. Limits are shared across keys. Global protective budgets and a bounded queue also apply. Errors do not spend credits but still count toward limits.

Responses include X-Request-Id. Rate limits include Retry-After. Replays include Idempotent-Replayed: true.

Supported scope

One URL per request. Batch arrays are rejected. Public posts only, original MP4 links, no hosting or permanent-link guarantee. X may change its interface; no fixed SLA is promised.