---
description: "Build a complete event catalog, update attendee answers, and issue and track waiting-list invitations."
group: "Build an integration"
order: 30
---

# Integration recipes

Connect documented operations into complete workflows.

## Prepare your environment {#prepare}

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-event-catalog}

1.  Create an event with future start/end dates.
2.  Create a uniquely named category; confirm the legacy creation redirect with a fresh list.
3.  Create a text question; read the category again to obtain the assigned question UUID.
4.  Create a free ticket linked to that category.
5.  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](/en-us/docs/product-management.md). Availability reads do not reserve tickets; checkout owns reservations and payment.

## Update an attendee's answers {#update-attendee-answers}

1.  Read `GET /{event_id}/order_lines/edit_data/{attendee_id}.json`. Confirm `ok:true`, the order's edit capability, and the current/historical question definitions.
2.  Choose the UUID from that attendee's permitted questions. Format its value using [question types and answers](/en-us/docs/question-types.md).
3.  Save contact fields and UUID-keyed answers; require `ok:true`.
4.  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 {#invite-waiting-list}

1.  Read `GET /{event_id}/waitinglists/list_data.json?status=waiting` and select entries in the same event.
2.  Read each `waitinglists/detail_data/{waitinglist_id}.json` and the product catalog. Confirm the eligible product, current invitation and quantity.
3.  PUT the invitation body below to `/{event_id}/waitinglists/invite.json`. Require `status.success:true`, including when HTTP is 200.
4.  Re-read each detail and inspect the returned invitation/expiry. Queue acceptance is distinct from final email delivery and a completed order.
5.  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 {#verify-and-maintain}

Test validation errors, foreign IDs, empty answers and uncertain outcomes alongside success. Read the [permissions matrix](/en-us/docs/permissions.md), [verification boundaries](/en-us/docs/testing.md) and [workflow limits](/en-us/docs/api-coverage.md). Use the [reference](/en-us/reference/overview) for all four language samples and exact response formats.
