Rate Limits
VDClip rate-limits the API so one integration cannot exhaust shared capacity. When you send
requests faster than your plan allows, the API replies with 429 Too Many Requests and a
Retry-After header telling you how long to wait.
This applies to the REST surface at https://api.vdclip.com/v1/* (authenticated with Authorization Bearer).
The OAuth 2.1 endpoints under /oauth2/* and /.well-known/* are throttled separately.
Request throughput is tied to your subscription, and the exact numbers can change as plans evolve. This page intentionally does not list fixed limits. Check the current per-plan throughput on the pricing page.
What a rate-limited response looks like
When you exceed your limit, the /v1 endpoints return HTTP 429 with the direct JSON response.
The errors[].code is RATE_LIMITED, and a Retry-After header carries the seconds to wait.
HTTP/1.1 429 Too Many Requests
content-type: application/json
retry-after: 12
{
"errors": [
{
"code": "RATE_LIMITED",
"description": "Rate limit exceeded. Retry after 12 seconds."
}
]
}| Signal | Meaning |
|---|---|
HTTP 429 | The request was rejected because you are over your limit. |
errors[].code | RATE_LIMITED on the /v1 surface. |
Retry-After | Seconds to wait before sending the next request. |
X-Request-ID | Include this when you contact support about a throttling issue. |
Rate-limit headers
Every authenticated /v1 response includes these headers so you can track your
usage without waiting for a 429:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window. |
X-RateLimit-Remaining | Requests remaining in the current window. |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets. |
When Retry-After is present, wait at least that long before retrying. A tight retry loop will not
unblock you sooner and may extend the cooldown.
Handling 429 in code
Honour Retry-After when present, otherwise fall back to exponential backoff with jitter. Retry
only on 429 and 5xx responses, and give up after a bounded number of attempts.
const BASE_DELAY_MS = 1000
const MAX_DELAY_MS = 30_000
const MAX_ATTEMPTS = 5
async function callVdclip(path: string, init: RequestInit = {}) {
let attempt = 0
let delay = BASE_DELAY_MS
while (true) {
const res = await fetch(`https://api.vdclip.com${path}`, {
...init,
headers: {
'Authorization': process.env.VDCLIP_API_KEY!, // server-side only, never in the browser
'Content-Type': 'application/json',
...init.headers,
},
})
// Success, or a non-retryable error: return and let the caller handle it.
const isRetryable = res.status === 429 || res.status >= 500
if (!isRetryable || attempt >= MAX_ATTEMPTS - 1) return res
// Prefer the server's Retry-After. Otherwise back off exponentially with jitter.
const retryAfter = Number(res.headers.get('retry-after')) * 1000
const jitter = Math.random() * delay * 0.4
const wait = retryAfter > 0 ? retryAfter : delay + jitter
await new Promise((resolve) => setTimeout(resolve, wait))
delay = Math.min(delay * 2, MAX_DELAY_MS)
attempt++
}
}The same logic works in any language. Two rules matter: trust Retry-After first, and add
randomness to your fallback delay so that many clients do not all retry at the same instant.
Which errors to retry
Not every failure should be retried. Repeating a request that failed for a client-side reason wastes attempts and can trip abuse protection.
| Error | Retryable | What to do |
|---|---|---|
RATE_LIMITED (429) | Yes | Wait for Retry-After, then retry with backoff. |
UPSTREAM_ERROR / 5xx | Yes | Retry with exponential backoff and jitter. |
INVALID_API_KEY | No | The key is wrong or revoked. Fix the key, then resend. |
MISSING_SCOPE | No | The key lacks the required scope. See Authentication. |
INSUFFICIENT_CREDITS | No | Top up before retrying. See pricing . |
VALIDATION_ERROR | No | The request body is invalid. Fix it before resending. |
INVALID_JSON | No | The body is not valid JSON. Fix it before resending. |
NOT_FOUND | No | The resource does not exist. Do not retry. |
For the full list of error codes and their shapes, see Errors.
OAuth 2.1 endpoints
The OAuth 2.1 Authorization Server at /oauth2/* and /.well-known/* is rate limited
independently of the API-key surface and uses standard RFC bodies. If you build an MCP, agent, or
integration client that authenticates with Authorization: Bearer <JWT>, apply the same discipline:
respect Retry-After, back off on 429, and cache discovery documents and JWKS instead of
fetching them on every call.
Reducing how often you hit the limit
A few habits keep you comfortably under your throughput.
- Paginate instead of polling hard. List endpoints use cursor pagination (
cursorandlimit). Page through results with the returned cursor rather than re-requesting the first page in a loop. - Poll long-running work gently. Renders and other async jobs finish on their own schedule. Poll status on an interval with backoff instead of in a tight loop.
- Batch where you can. Group related work into fewer requests when an endpoint supports it.
- Cache stable data. Templates, brand-kit assets, and account details change rarely. Cache them on your side instead of re-fetching for every operation.
Tips & Best Practices
When a 429 includes a Retry-After header, wait at least that many seconds. It is the server telling you exactly when capacity is available again.
Randomize your fallback delay so that many clients recovering at once do not retry in lockstep and re-trigger the limit.
INVALID_API_KEY, MISSING_SCOPE, VALIDATION_ERROR, and similar codes will fail every time until you fix the request. Retrying only burns attempts.
Log X-Request-ID from throttled responses so support can trace the exact calls if you think a limit is wrong.