Loading documentation…
{{ error }}
Integration recipes
Connect documented operations into complete workflows.
Prepare your environment
Run these examples on your server with an authorized synthetic test organization. Set PAYDRO_ORIGIN to the company API hostname, and provide PAYDRO_EMAIL and PAYDRO_PASSWORD through your secret manager. The developer hostname serves documentation. Use Node.js 18 or later for the complete catalog example. This recipe creates configuration; it leaves publication to your organizer workflow.
Create an event, questions and products
Create an event with future start/end dates.
Create a uniquely named category; confirm the legacy creation redirect with a fresh list.
Create a text question; read the category again to obtain the assigned question UUID.
Create a free ticket linked to that category.
Read the product catalog to confirm its configuration and computed availability.
Set EVENT_START and EVENT_END to future server-local SQL timestamps, for example 2027-06-20 09:00:00 and 2027-06-20 18:00:00. Save this code as a Node .mjs file. Category and question creation can return 302 with a misleading generic error body; the helper only permits those expected redirects and the recipe confirms persistence by reading back.
const origin = process.env.PAYDRO_ORIGIN;
const auth = Buffer.from(`${process.env.PAYDRO_EMAIL}:${process.env.PAYDRO_PASSWORD}`).toString('base64');
if (!origin || !process.env.PAYDRO_EMAIL || !process.env.PAYDRO_PASSWORD || !process.env.EVENT_START || !process.env.EVENT_END) {
throw new Error('Set Paydro origin/credentials and EVENT_START/EVENT_END');
}
async function request(method, path, body, redirectPath) {
const response = await fetch(new URL(path, origin), {
method, redirect: 'manual', signal: AbortSignal.timeout(30000),
headers: { Authorization: `Basic ${auth}`, Accept: 'application/json',
...(body ? { 'Content-Type': 'application/json' } : {}) },
...(body ? { body: JSON.stringify(body) } : {})
});
if (response.status === 302 && redirectPath) {
const location = new URL(response.headers.get('location'), origin);
if (location.origin !== new URL(origin).origin || !location.pathname.startsWith(redirectPath)) {
throw new Error('Unexpected creation redirect');
}
return; // The next read must confirm the new record.
}
if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) {
throw new Error(`Unexpected Paydro response: ${response.status}`);
}
const data = await response.json();
if (data.error || data.ok === false || data.status?.success === false) {
throw new Error('Paydro rejected the request');
}
return data;
}
const created = await request('POST', '/events/add.json', { Event: {
name: 'Integration example', start_date: process.env.EVENT_START,
end_date: process.env.EVENT_END, currency: 'EUR', timezone: 'Europe/Amsterdam'
} });
if (created.ok !== true || !created.event_id) throw new Error('Event creation not confirmed');
const eventId = created.event_id;
const categoryName = `Example tickets ${Date.now()}`;
await request('POST', `/${eventId}/categories/add.json`,
{ Category: { name: categoryName } }, `/${eventId}/categories/index`);
let categories = await request('GET', `/${eventId}/categories/index.json`);
let category = categories.find(row => row.Category.name === categoryName);
if (!category) throw new Error('Category creation not confirmed');
const categoryId = category.Category.id;
await request('PUT', `/${eventId}/category_fields/edit/${categoryId}.json`,
{ CategoryField: { name: 'Dietary preference', type: 'text', mandatory: 0, on_ticket: 1 } },
`/${eventId}/category_fields/edit/${categoryId}/`);
categories = await request('GET', `/${eventId}/categories/index.json`);
category = categories.find(row => row.Category.id === categoryId);
const question = category.CategoryField.find(field => field.name === 'Dietary preference');
if (!question) throw new Error('Question creation not confirmed');
const ticket = await request('PUT', `/${eventId}/products/add/free.json`, { Product: {
title: 'General admission', category_id: categoryId, price: '0.00', stock: 20, visible: 1
} });
if (ticket.ok !== true || !ticket.id) throw new Error('Product creation not confirmed');
const products = await request('GET', `/${eventId}/products/index.json`);
const product = products.find(row => row.id === ticket.id);
if (!product) throw new Error('Product missing from catalog');
console.log({ eventId, categoryId, questionId: question.id, productId: product.id,
stock: product.stock, currentStock: product.current_stock });Keep the returned IDs. A transport failure after creation has an uncertain outcome; read current records before replaying. There is no idempotency key. For paid products, configure the organization's payment provider first and review product fields. Availability reads do not reserve tickets; checkout owns reservations and payment.
Update an attendee's answers
Read
GET /{event_id}/order_lines/edit_data/{attendee_id}.json. Confirmok:true, the order's edit capability, and the current/historical question definitions.Choose the UUID from that attendee's permitted questions. Format its value using question types and answers.
Save contact fields and UUID-keyed answers; require
ok:true.Read detail again and compare the returned answer. A changed live definition does not replace historical snapshots automatically.
curl --fail-with-body --user "$PAYDRO_EMAIL:$PAYDRO_PASSWORD" \
--request PUT --header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{"OrderLine":{"firstname":"Alex","77777777-7777-4777-8777-777777777777":"Vegetarian"}}' \
"$PAYDRO_ORIGIN/$EVENT_ID/order_lines/save/$ATTENDEE_ID.json"Replace the synthetic question UUID. File uploads and purchased paid-question amounts are not changed by this path. HTTP 409 means the order is no longer editable; refresh it and resolve the state before retrying. For a choice question send a selected-label array, not its definition's options array.
Invite and track waiting-list subscribers
Read
GET /{event_id}/waitinglists/list_data.json?status=waitingand select entries in the same event.Read each
waitinglists/detail_data/{waitinglist_id}.jsonand the product catalog. Confirm the eligible product, current invitation and quantity.PUT the invitation body below to
/{event_id}/waitinglists/invite.json. Requirestatus.success:true, including when HTTP is 200.Re-read each detail and inspect the returned invitation/expiry. Queue acceptance is distinct from final email delivery and a completed order.
Read the order list when reconciling conversions. Match the resulting order's promo-code ID when present; an invitation code alone does not prove a purchase.
{"Waitinglist":{
"ids":["66666666-6666-4666-8666-666666666666"],
"product_id_invite":"22222222-2222-4222-8222-222222222222",
"amount":1,"invite_valid_for_hours":48
}}Amount is 1–50; allowed hours are 0, 24, 48, 72, 168, 336, 744, 1488 and 8760. Zero means no expiry. Invitations create replacement promo codes and queue email in a transaction. An uncertain retry can revoke the prior code and queue another message, so read details before replaying. Visitor signup happens through the ticket shop, not this organizer endpoint.
Verify and maintain
Test validation errors, foreign IDs, empty answers and uncertain outcomes alongside success. Read the permissions matrix, verification boundaries and workflow limits. Use the reference for all four language samples and exact response formats.