{
"error": {
"type": "invalid_request_error",
"code": "ticket_type_not_allowed",
"message": "Esta API key no tiene acceso al tipo de entrada indicado.",
"param": "ticket_type",
"request_id": "req_9f2c1a4e7b304d51"
}
}
| Field | Use |
|---|---|
type | Broad category. Use it to decide whether retrying makes sense. |
code | Stable. This is what you should branch on. |
message | Human-readable text. May be reworded without notice. |
param | Request field that caused the error, where applicable. |
request_id | Request identifier. Also sent in X-Request-Id. |
Never branch on
message. Rewording is not considered a breaking change;
changing a code is.Error messages are currently returned in Spanish, regardless of this page’s
language. They are meant for your logs and for support, not for showing to end
users — build your user-facing text from
code.Types and HTTP status
type | HTTP | Retry? |
|---|---|---|
authentication_error | 401 | No. Fix the key. |
permission_error | 403 | No. Missing scopes. |
invalid_request_error | 400 / 422 | No. Fix the request. |
not_found_error | 404 | No. |
conflict_error | 409 | Depends. Read the code. |
rate_limit_error | 429 | Yes, after retry-after. |
api_error | 500 | Yes, with backoff. |
Code catalogue
Authentication and permissions
| Code | HTTP | Meaning |
|---|---|---|
missing_api_key | 401 | The Authorization header is missing or malformed |
invalid_api_key | 401 | The key does not exist |
revoked_api_key | 401 | The key has been revoked |
expired_api_key | 401 | The key has expired |
key_pending_activation | 403 | Pending manual activation |
ip_not_allowed | 403 | Origin not authorised |
insufficient_scope | 403 | Missing the scope the endpoint requires |
Request
| Code | HTTP | Meaning |
|---|---|---|
validation_error | 400 | Body or parameters do not match the schema |
invalid_limit | 400 | limit out of range (1–200) |
invalid_cursor | 400 | starting_after is not a valid cursor |
invalid_date | 400 | A date is not ISO 8601 |
invalid_id | 400 / 422 | An identifier is not in the expected format |
unknown_endpoint | 404 | The route does not exist |
empty_batch | 400 | A batch was sent with no tickets |
Key scope
| Code | HTTP | Meaning |
|---|---|---|
ticket_type_not_allowed | 422 | The ticket type is outside your key’s scope |
day_not_allowed | 422 | The day is outside your key’s scope |
day_required | 422 | Your key is limited to specific days: say which one |
Resources
| Code | HTTP | Meaning |
|---|---|---|
ticket_not_found | 404 | The ticket does not exist, or is outside your scope |
wristband_not_found | 404 | The wristband does not exist, or is outside your scope |
ticket_type_not_found | 422 | The ticket type does not exist |
day_not_found | 422 | The day does not exist |
zone_not_found | 422 | The zone does not exist |
A resource outside your key’s scope returns
404, not 403. This is deliberate:
telling them apart would confirm that a given ticket code exists, which is
exactly what someone enumerating codes would be after.Conflicts
| Code | HTTP | Meaning |
|---|---|---|
resource_already_exists | 409 | A ticket code in the batch already exists |
ticket_already_claimed | 409 | The ticket has already been accredited |
wristband_in_use | 409 | That wristband is already assigned to another attendee |
idempotency_in_flight | 409 | The same Idempotency-Key is being processed right now |
api_key_device_missing | 409 | The key has no device attached: ask for it to be reactivated |
Writes
| Code | HTTP | Meaning |
|---|---|---|
missing_idempotency_key | 400 | The Idempotency-Key header is missing |
invalid_idempotency_key | 400 | The idempotency key is not of a reasonable length |
idempotency_key_reused | 422 | That key was already used with a different body |
checkin_failed | 422 | The accreditation could not be completed |
topup_failed | 422 | The wristband does not accept top-ups (blocked, under review…) |
Other
| Code | HTTP | Meaning |
|---|---|---|
rate_limit_exceeded | 429 | Quota exhausted. Wait for what retry-after says. |
internal_error | 500 | Server failure. Retry and, if it persists, get in touch quoting the request_id. |
Recommended retries
async function callVentry(path, options = {}, attempt = 0) {
const response = await fetch(`https://api.ventry.es/v1${path}`, {
...options,
headers: { Authorization: `Bearer ${process.env.VENTRY_KEY}`, ...options.headers },
});
if (response.ok) return response.json();
const { error } = await response.json();
// 429: the server tells you exactly how long to wait.
if (response.status === 429 && attempt < 5) {
const wait = Number(response.headers.get("retry-after") ?? 1) * 1000;
await new Promise((r) => setTimeout(r, wait));
return callVentry(path, options, attempt + 1);
}
// 5xx: exponential backoff. With an Idempotency-Key, retrying is safe.
if (response.status >= 500 && attempt < 3) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
return callVentry(path, options, attempt + 1);
}
// The rest are your errors: retrying gives exactly the same result.
throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}
import os, time, requests
BASE = "https://api.ventry.es/v1"
def call_ventry(method, path, attempt=0, **kwargs):
headers = {
"Authorization": f"Bearer {os.environ['VENTRY_KEY']}",
**kwargs.pop("headers", {}),
}
response = requests.request(method, f"{BASE}{path}", headers=headers, timeout=30, **kwargs)
if response.ok:
return response.json()
error = response.json()["error"]
# 429: the server tells you exactly how long to wait.
if response.status_code == 429 and attempt < 5:
time.sleep(int(response.headers.get("retry-after", 1)))
return call_ventry(method, path, attempt + 1, headers=headers, **kwargs)
# 5xx: exponential backoff. With an Idempotency-Key, retrying is safe.
if response.status_code >= 500 and attempt < 3:
time.sleep(2 ** attempt)
return call_ventry(method, path, attempt + 1, headers=headers, **kwargs)
# The rest are your errors: retrying gives exactly the same result.
raise RuntimeError(f"{error['code']}: {error['message']} ({error['request_id']})")
<?php
function callVentry(string $method, string $path, array $options = [], int $attempt = 0): array
{
$ch = curl_init("https://api.ventry.es/v1$path");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => array_merge(
["Authorization: Bearer " . getenv("VENTRY_KEY")],
$options["headers"] ?? []
),
] + (isset($options["body"]) ? [CURLOPT_POSTFIELDS => $options["body"]] : []));
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHeaders = substr($raw, 0, $headerSize);
$body = json_decode(substr($raw, $headerSize), true);
curl_close($ch);
if ($status < 400) {
return $body;
}
// 429: the server tells you exactly how long to wait.
if ($status === 429 && $attempt < 5) {
preg_match('/retry-after:\s*(\d+)/i', $rawHeaders, $m);
sleep((int) ($m[1] ?? 1));
return callVentry($method, $path, $options, $attempt + 1);
}
// 5xx: exponential backoff. With an Idempotency-Key, retrying is safe.
if ($status >= 500 && $attempt < 3) {
sleep(2 ** $attempt);
return callVentry($method, $path, $options, $attempt + 1);
}
// The rest are your errors: retrying gives exactly the same result.
throw new RuntimeException(sprintf(
"%s: %s (%s)",
$body["error"]["code"],
$body["error"]["message"],
$body["error"]["request_id"]
));
}