Skip to main content

Documentation Index

Fetch the complete documentation index at: https://onlytraffic.com/docs/llms.txt

Use this file to discover all available pages before exploring further.

The OnlyTraffic Studio API lets you read and act on your Studio data: subscribers, transactions, orders, campaigns and more. Use it to build integrations, dashboards and automated workflows.

Base URL

https://studio-api.onlytraffic.com/api/external/v1

Authentication

Get your API key and start making requests.

Pagination

Page-based and cursor-based listings.

Rate Limits

Per-key, per-tier hourly and burst caps.

Authentication

All requests require the X-API-Key header. Create your API key in the Studio Dashboard.
curl https://studio-api.onlytraffic.com/api/external/v1/subscribers \
  -H "X-API-Key: your-api-key-here"
See the Authentication page for error codes and detailed setup.

Rate Limits

Limits are per API key, rolling window, and tiered by request kind:
TierPer hourPer minute (burst)Applies to
read1,00060GET requests
write10020POST / PUT / PATCH / DELETE
upstream105A small set of endpoints with stricter limits
Both windows are enforced together. The hour quota is the long-window cap; the per-minute cap is burst protection on top of it.

Response headers (on every response, success or error)

HeaderDescription
X-RateLimit-LimitThe active tier’s hourly cap
X-RateLimit-RemainingRequests left in the current hour for this tier
X-RateLimit-ResetUnix timestamp when the hour window resets
X-RateLimit-TierActive tier (read, write, or upstream)
Example:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 947
X-RateLimit-Reset: 1746453296
X-RateLimit-Tier: read

When you go over

429 Too Many Requests with the standard error envelope plus a Retry-After header (seconds to wait):
{
  "success": false,
  "error": "rate_limit_exceeded",
  "message": "Too many requests. Try again in 37 seconds.",
  "retry_after": 37
}
Best practice: read X-RateLimit-Remaining after each call and back off when it gets low, instead of waiting for the 429.

Pagination

The API uses two pagination styles depending on the dataset size and access pattern.

Page-based (default for orders / campaigns)

Endpoints with bounded result sets (cpl/orders, cpc/orders, revshare/campaigns, etc.) use page-based pagination. The default page size is 50, max 100. Pass ?page=N to navigate.
{
  "pagination": {
    "page": 1,
    "total": 150,
    "total_pages": 2
  }
}

Cursor-based (high-volume feeds)

Endpoints over high-volume tables (subscribers, transactions) use cursor pagination. The first call returns the first page plus a next_cursor token; pass that token back as ?after= to continue.
GET /api/external/v1/transactions?limit=50
{
  "data": [/* 50 rows, newest first */],
  "pagination": {
    "next_cursor": "eyJ0cyI6MTc0NjQ1MzI5NiwiaWQiOjk4NzY1fQ==",
    "has_next": true,
    "limit": 50
  }
}
Continue:
GET /api/external/v1/transactions?limit=50&after=eyJ0cyI6MTc0NjQ1MzI5NiwiaWQiOjk4NzY1fQ==
When has_next is false (or next_cursor is null), you’ve reached the end. Why cursor on these endpoints
  • Stable ordering even when new rows are inserted between calls (page-based would shift).
  • No total count: cursor feeds optimize for partial reads, not row counts.
  • O(1) per page regardless of how deep you’ve scrolled.
Treat the cursor as an opaque string, don’t parse it. Sort direction Pass ?sort=<field>_asc to walk oldest to newest. Default is <field>_desc (newest first). The cursor is bound to the direction it was issued for; switching mid-pagination requires restarting.

Retries & idempotency

Concurrent mutations

Some write endpoints are serialized per account: a second request hitting the same resource while the first is still in flight returns 429 action_in_progress. Wait for the original to complete, then re-poll the corresponding GET to see the result. Don’t blind-retry through the in-flight window.

Safe to retry

CodeWhy retryable
502Upstream temporarily unavailable. Retry with back-off.
429 rate_limit_exceededBody has retry_after (seconds), header has Retry-After. Wait the suggested duration before retrying.
Network timeoutRetryable for idempotent methods (GET, DELETE). For non-idempotent methods, re-check state via GET first.

NOT safe to blind-retry

400, 404, 409, 422, 426 are deterministic. The same request will produce the same error. Investigate (read error and details), fix the input, and only then resubmit. Exponential, capped: 1s, 2s, 4s, 8s, 16s, 30s. If the response carries retry_after (body) or Retry-After (header), honour the server-suggested wait, it beats local back-off.

Example: handling 429

JavaScript
async function fetchWithRetry(url, options) {
  const res = await fetch(url, options);
  if (res.status !== 429) return res;
  const retryAfter = Number(res.headers.get('Retry-After')) || 5;
  await new Promise(r => setTimeout(r, retryAfter * 1000));
  return fetch(url, options); // retry once
}

Response Format

All responses follow a consistent structure:
{
  "success": true,
  "data": [ ... ],
  "pagination": { ... }
}

Write responses & schema evolution

Writes return id-only

Successful POST / PUT / PATCH / DELETE responses carry just the resource id (and success: true), not the full record:
{
  "success": true,
  "data": { "order_id": "cplo_xxxxxxx" }
}
To read the post-write state, call the matching GET endpoint. The list / detail endpoints are the single source of truth for the response shape. Two exceptions:
  • Image upload endpoints also return thumbnail_url (a 3-day signed URL) so you can render the just-saved image without a follow-up call. Originals stay behind the regular GET.
  • One-time secrets (the plaintext API key returned on key creation, verification tokens) appear in data for that single response and are documented as “shown once, never again”.

Schema evolution policy

We may add fields to existing responses without notice. Consumers MUST ignore unknown fields rather than fail-on-extra. v1 is still young and may change without warning as we fix and refine it. Keep an eye on this doc — if something breaks for you, check here first. Practically:
  • Configure your parser to allow extra fields rather than reject them.
  • Validate inputs strictly, treat outputs liberally.