event.read and tickets.write (the latter requires manual
activation by the organiser).
1. Map your catalogue against VENTRY’s
First, and only once per event: find out which identifiers to use.curl https://api.ventry.es/v1/ticket-types \
-H "Authorization: Bearer $VENTRY_KEY"
const types = await (
await fetch("https://api.ventry.es/v1/ticket-types", {
headers: { Authorization: `Bearer ${process.env.VENTRY_KEY}` },
})
).json();
types = requests.get(
"https://api.ventry.es/v1/ticket-types",
headers={"Authorization": f"Bearer {os.environ['VENTRY_KEY']}"},
timeout=10,
).json()
$types = callVentry("GET", "/ticket-types");
{
"object": "list",
"data": [
{
"object": "ticket_type",
"id": "6a3d04709bcd4477a1bfe4b3",
"name": "General pass",
"allowed_zones": [{ "id": "6a3d2a67a1e963300ee2c99c", "name": "Venue" }],
"included_items": []
},
{
"object": "ticket_type",
"id": "6a3d04709bcd4477a1bfe4c1",
"name": "VIP pass",
"allowed_zones": [
{ "id": "6a3d2a67a1e963300ee2c99c", "name": "Venue" },
{ "id": "6a3d2a6ea1e963300ee2c99d", "name": "VIP area" }
],
"included_items": [
{ "product_id": "6a3d2b11a1e963300ee2ca02", "name": "Drink", "quantity": 2 }
]
}
]
}
curl https://api.ventry.es/v1/days \
-H "Authorization: Bearer $VENTRY_KEY"
{
"object": "list",
"data": [
{ "object": "day", "id": "6a3d0512...", "name": "Friday", "date": "2026-06-12T00:00:00.000Z",
"starts_at": "2026-06-12T16:00:00.000Z", "ends_at": "2026-06-13T05:00:00.000Z" },
{ "object": "day", "id": "6a3d0518...", "name": "Saturday", "date": "2026-06-13T00:00:00.000Z",
"starts_at": "2026-06-13T16:00:00.000Z", "ends_at": "2026-06-14T05:00:00.000Z" }
]
}
ticket_type plus a
list of valid_days.
2. Create the tickets
curl -X POST https://api.ventry.es/v1/tickets \
-H "Authorization: Bearer $VENTRY_KEY" \
-H "Idempotency-Key: order-84213-tickets" \
-H "Content-Type: application/json" \
-d '{
"tickets": [
{
"code": "TR-84213-001",
"ticket_type": "6a3d04709bcd4477a1bfe4c1",
"valid_days": ["6a3d0512...", "6a3d0518..."],
"attendee": {
"name": "Ada Lovelace",
"email": "[email protected]",
"document_type": "DNI",
"document_number": "12345678Z"
}
},
{
"code": "TR-84213-002",
"ticket_type": "6a3d04709bcd4477a1bfe4b3",
"valid_days": ["6a3d0512..."],
"attendee": { "name": "Alan Turing", "email": "[email protected]" }
}
]
}'
const response = await fetch("https://api.ventry.es/v1/tickets", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VENTRY_KEY}`,
"Idempotency-Key": "order-84213-tickets",
"Content-Type": "application/json",
},
body: JSON.stringify({
tickets: [
{
code: "TR-84213-001",
ticket_type: "6a3d04709bcd4477a1bfe4c1",
valid_days: ["6a3d0512...", "6a3d0518..."],
attendee: {
name: "Ada Lovelace",
email: "[email protected]",
document_type: "DNI",
document_number: "12345678Z",
},
},
{
code: "TR-84213-002",
ticket_type: "6a3d04709bcd4477a1bfe4b3",
valid_days: ["6a3d0512..."],
attendee: { name: "Alan Turing", email: "[email protected]" },
},
],
}),
});
response = requests.post(
"https://api.ventry.es/v1/tickets",
headers={
"Authorization": f"Bearer {os.environ['VENTRY_KEY']}",
"Idempotency-Key": "order-84213-tickets",
},
json={
"tickets": [
{
"code": "TR-84213-001",
"ticket_type": "6a3d04709bcd4477a1bfe4c1",
"valid_days": ["6a3d0512...", "6a3d0518..."],
"attendee": {
"name": "Ada Lovelace",
"email": "[email protected]",
"document_type": "DNI",
"document_number": "12345678Z",
},
},
{
"code": "TR-84213-002",
"ticket_type": "6a3d04709bcd4477a1bfe4b3",
"valid_days": ["6a3d0512..."],
"attendee": {"name": "Alan Turing", "email": "[email protected]"},
},
]
},
timeout=60,
)
<?php
$payload = [
"tickets" => [
[
"code" => "TR-84213-001",
"ticket_type" => "6a3d04709bcd4477a1bfe4c1",
"valid_days" => ["6a3d0512...", "6a3d0518..."],
"attendee" => [
"name" => "Ada Lovelace",
"email" => "[email protected]",
"document_type" => "DNI",
"document_number" => "12345678Z",
],
],
[
"code" => "TR-84213-002",
"ticket_type" => "6a3d04709bcd4477a1bfe4b3",
"valid_days" => ["6a3d0512..."],
"attendee" => ["name" => "Alan Turing", "email" => "[email protected]"],
],
],
];
$response = callVentry("POST", "/tickets", [
"headers" => [
"Idempotency-Key: order-84213-tickets",
"Content-Type: application/json",
],
"body" => json_encode($payload),
]);
{
"object": "list",
"data": [
{
"object": "ticket",
"id": "6a3d0611a1e963300ee2cb10",
"code": "TR-84213-001",
"status": "active",
"ticket_type": { "id": "6a3d04709bcd4477a1bfe4c1", "name": "VIP pass" },
"attendee": {
"name": "Ada Lovelace",
"email": "[email protected]",
"document": { "type": "DNI", "last4": "5678" }
},
"valid_days": [
{ "id": "6a3d0512...", "name": "Friday" },
{ "id": "6a3d0518...", "name": "Saturday" }
],
"extra_zones": [],
"wristband": null,
"checked_in_at": null,
"created_at": "2026-05-30T09:14:02.113Z",
"updated_at": "2026-05-30T09:14:02.113Z"
}
],
"has_more": false,
"next_cursor": null
}
Send your own
code. It is optional — omit it and VENTRY generates one — but
sending your platform’s ticket number lets you reconcile the two systems later
without keeping a lookup table.Things that matter
The batch is all or nothing
The batch is all or nothing
If a code already exists or a reference is invalid, no ticket in the batch
is created and you get a
409 or 422. Fix it and resend the whole batch:
you do not have to work out which ones went through.500 per call, maximum
500 per call, maximum
And with a write quota of 20 calls per minute, that is 10,000 tickets per
minute. Batch them: one call per ticket is the fastest way to hit the limit.
Document and date of birth are optional
Document and date of birth are optional
If you do not collect them, do not send them. VENTRY fills in neutral values
and accreditation works the same. The document number is never returned in
full: only the last four digits.
extra_zones for add-ons
extra_zones for add-ons
If you sell an add-on that grants access to an extra zone — a VIP area pass on
top of a general ticket — pass it in
extra_zones with the zone id. On
accreditation, the wristband receives the ticket type’s zones plus these.3. Cancel refunded tickets
curl -X POST https://api.ventry.es/v1/tickets/TR-84213-001/cancel \
-H "Authorization: Bearer $VENTRY_KEY"
await fetch("https://api.ventry.es/v1/tickets/TR-84213-001/cancel", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.VENTRY_KEY}` },
});
requests.post(
"https://api.ventry.es/v1/tickets/TR-84213-001/cancel",
headers={"Authorization": f"Bearer {os.environ['VENTRY_KEY']}"},
timeout=30,
)
callVentry("POST", "/tickets/TR-84213-001/cancel");
inactive and can no longer be used for accreditation.
A ticket that has already been redeemed cannot be cancelled: it returns
409 ticket_already_claimed. The attendee is inside the venue with an active
wristband, and cancelling the ticket would leave that wristband working against a
dead ticket. For that case, the wristband has to be blocked from the VENTRY
dashboard.4. Check the state
During the event, to find out who has come in:curl "https://api.ventry.es/v1/tickets?status=used&updated_since=2026-06-12T18:00:00Z&limit=200" \
-H "Authorization: Bearer $VENTRY_KEY"
curl https://api.ventry.es/v1/tickets/TR-84213-001 \
-H "Authorization: Bearer $VENTRY_KEY"
checked_in_at tells you when it was accredited and wristband which wristband
was assigned to it.
End-to-end flow
const VENTRY = "https://api.ventry.es/v1";
const headers = {
Authorization: `Bearer ${process.env.VENTRY_KEY}`,
"Content-Type": "application/json",
};
// Map configured once per event.
const TYPE_BY_PRODUCT = {
"vip-pass": "6a3d04709bcd4477a1bfe4c1",
"general-pass": "6a3d04709bcd4477a1bfe4b3",
};
const ALL_DAYS = ["6a3d0512...", "6a3d0518..."];
async function pushOrder(order) {
const body = {
tickets: order.tickets.map((t) => ({
code: t.number,
ticket_type: TYPE_BY_PRODUCT[t.product],
valid_days: t.product === "friday-pass" ? [ALL_DAYS[0]] : ALL_DAYS,
attendee: {
name: `${t.firstName} ${t.lastName}`.trim(),
email: t.email,
...(t.documentNumber && {
document_type: t.documentType ?? "DNI",
document_number: t.documentNumber,
}),
},
})),
};
const response = await fetch(`${VENTRY}/tickets`, {
method: "POST",
headers: {
...headers,
// Derived from the order: survives this process crashing.
"Idempotency-Key": `order-${order.id}-tickets`,
},
body: JSON.stringify(body),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}
return response.json();
}
import os, requests
VENTRY = "https://api.ventry.es/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['VENTRY_KEY']}"}
# Map configured once per event.
TYPE_BY_PRODUCT = {
"vip-pass": "6a3d04709bcd4477a1bfe4c1",
"general-pass": "6a3d04709bcd4477a1bfe4b3",
}
ALL_DAYS = ["6a3d0512...", "6a3d0518..."]
def push_order(order):
tickets = []
for t in order["tickets"]:
attendee = {
"name": f"{t['first_name']} {t['last_name']}".strip(),
"email": t["email"],
}
if t.get("document_number"):
attendee["document_type"] = t.get("document_type", "DNI")
attendee["document_number"] = t["document_number"]
tickets.append({
"code": t["number"],
"ticket_type": TYPE_BY_PRODUCT[t["product"]],
"valid_days": [ALL_DAYS[0]] if t["product"] == "friday-pass" else ALL_DAYS,
"attendee": attendee,
})
response = requests.post(
f"{VENTRY}/tickets",
headers={
**HEADERS,
# Derived from the order: survives this process crashing.
"Idempotency-Key": f"order-{order['id']}-tickets",
},
json={"tickets": tickets},
timeout=60,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']} ({error['request_id']})")
return response.json()
<?php
const VENTRY = "https://api.ventry.es/v1";
// Map configured once per event.
const TYPE_BY_PRODUCT = [
"vip-pass" => "6a3d04709bcd4477a1bfe4c1",
"general-pass" => "6a3d04709bcd4477a1bfe4b3",
];
const ALL_DAYS = ["6a3d0512...", "6a3d0518..."];
function pushOrder(array $order): array
{
$tickets = [];
foreach ($order["tickets"] as $t) {
$attendee = [
"name" => trim("{$t['first_name']} {$t['last_name']}"),
"email" => $t["email"],
];
if (!empty($t["document_number"])) {
$attendee["document_type"] = $t["document_type"] ?? "DNI";
$attendee["document_number"] = $t["document_number"];
}
$tickets[] = [
"code" => $t["number"],
"ticket_type" => TYPE_BY_PRODUCT[$t["product"]],
"valid_days" => $t["product"] === "friday-pass"
? [ALL_DAYS[0]]
: ALL_DAYS,
"attendee" => $attendee,
];
}
// callVentry(): see the implementation on the Errors page.
return callVentry("POST", "/tickets", [
"headers" => [
// Derived from the order: survives this process crashing.
"Idempotency-Key: order-{$order['id']}-tickets",
"Content-Type: application/json",
],
"body" => json_encode(["tickets" => $tickets]),
]);
}