# Voodootix API: agent booking guide

This guide is for an agent integrating with the voodootix ticketing SaaS when it has an API identifier (`api_id`) and API secret (`api_secret`). It describes the complete seat-booking flow:

1. authenticate;
2. list events;
3. inspect availability, seats, and articles;
4. temporarily reserve seats;
5. assign an article to every reserved seat and add the seats to the cart;
6. create the pending order;
7. finalize the paid order through the external-payment IPN call.

The examples use the production base URL from the OpenAPI document. Use the equivalent base URL supplied for another environment if one is configured.

## Important operating rules

- Use `https://api.voodootix.com/v1` as the base URL unless the account owner gives the agent another base URL.
- The API identifier and secret are credentials. Never put them in a user-visible response, browser code, logs, or an order reference.
- Authenticate once and reuse the returned JWT for the whole booking flow. The JWT identifies the API session that owns the temporary seats and cart.
- Send `Authorization: Bearer <jwt>` and `Content-Type: application/json` on every protected call.
- Do not start a second authentication flow while a cart is active. A new JWT can create a different session and will not contain the existing cart.
- Always use the numeric event ID (`increm` in event data), not the display event code.
- Seat reservations are temporary. Read `expire` from the booking response and complete the cart quickly.
- Treat the JSON `status` field as the application result. It can be returned as either a number or a string.
- For the requested IPN workflow, use a `partner` API key. A `backoffice` key uses cashdesk visibility and can also support direct cashdesk finalization; see [Backoffice variation](#backoffice-variation).

## Authentication

`POST /auth` is public and accepts the API pair in JSON. The `authType` value must be exactly `api`.

```bash
BASE_URL="https://api.voodootix.com/v1"
API_ID="<api_id>"
API_SECRET="<api_secret>"

AUTH_JSON=$(curl -sS -X POST "$BASE_URL/auth" \
  -H 'Content-Type: application/json' \
  --data "$(printf '{\"identifier\":\"%s\",\"secret\":\"%s\",\"authType\":\"api\"}' "$API_ID" "$API_SECRET")")

# Extract jwt with the JSON parser available in the agent runtime.
JWT="<AUTH_JSON.jwt>"
```

Request body:

```json
{
  "identifier": "<api_id>",
  "secret": "<api_secret>",
  "authType": "api"
}
```

The successful response contains `jwt` and a `data` object. Inspect `data.rol` (or the equivalent decoded context) before continuing. A normal partner integration should see `rol: "partner"`.

For an account selector only when explicitly supplied by Voodootix:

```json
{
  "identifier": "<api_id>",
  "secret": "<api_secret>",
  "authType": "api",
  "accountId": "<account_id>"
}
```

## End-to-end sequence

```text
POST /auth
    |
POST /listevents
    |
GET  /checkseats/{event_id}       (summary)
GET  /jsonmap/{event_id}           (seat IDs/status, if assigned seating)
GET  /prices/{event_id}            (article IDs/prices)
    |
POST /booksingleseat                (one selected seat)
     or POST /bookseat               (automatic seats)
    |
POST /addtocart                     (seat ID -> article ID)
GET  /cartdetails                   (verify the cart and amount)
    |
POST /ordercart                     (creates pending external order)
    |
POST /ipn                           (after payment succeeds)
GET  /checkipn/{cart_id}             (optional status check)
```

All calls after `/auth` use the same `Authorization` header.

## 1. List events

`POST /listevents` lists events visible to the authenticated API sales channel. `/eventsforsale` is an alias.

```bash
curl -sS -X POST "$BASE_URL/listevents" \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  --data '{
    "filter": {
      "begin": "2026-09-18",
      "end": "2026-12-31"
    }
  }'
```

The filter is optional. Supported documented fields are `begin`, `end`, `event_id`, `event_trace`, `group_id`, `master_id`, and `event_tags`.

The response has the form:

```json
{
  "status": 200,
  "channel": "partner",
  "data": [
    {
      "is_group": false,
      "is_recurring": false,
      "id": 1254,
      "data": {
        "increm": 1254,
        "titre": "Example event",
        "event_start": "2026-09-15 20:00:00",
        "event_status": "online",
        "channel_api": 1
      }
    }
  ]
}
```

Use `data[].data.increm` as `event_id`. For a grouped or recurring result, select the numeric `increm` of the child event that the customer actually wants.

The API key must have access to the event and the event must be enabled for the key's channel. Otherwise the API returns a permission or event-not-shared error.

## 2. Get availability, seats, and sellable articles

### Availability summary

`GET /checkseats/{event_id}` returns the total available seats. Partner and backoffice responses can also include sold and reserved totals.

```bash
curl -sS "$BASE_URL/checkseats/1254" \
  -H "Authorization: Bearer $JWT"
```

Typical response:

```json
{
  "status": 200,
  "data": {
    "event": 1254,
    "show_remain": 1,
    "avail_seat": 184,
    "sold_seat": 58,
    "reserved_seat": 8,
    "next_sale": ""
  }
}
```

`avail_seat: 0` is a successful sold-out response, not necessarily an API error.

### Seat allocation and map

For section/category counts, use `GET /allseats/{event_id}`. It returns totals such as `FREE`, `SOLD`, and `PENDING`, plus the article IDs available for each category.

For an assigned-seating event, use `GET /jsonmap/{event_id}` to obtain actual seat IDs, section IDs, category IDs, coordinates, and seat status. Only select seats whose returned status indicates they are available; re-check availability by attempting the booking because another buyer can reserve a seat between the map read and the booking call.

```bash
curl -sS "$BASE_URL/jsonmap/1254" \
  -H "Authorization: Bearer $JWT"

curl -sS "$BASE_URL/allseats/1254" \
  -H "Authorization: Bearer $JWT"
```

The map response contains `data.seats[]`. Each seat has at least `seat_id`, `section_id`, `seat`, and `category_id`. Use those values in `/booksingleseat`.

To get the complete role-filtered article catalog and prices, call `GET /prices/{event_id}`:

```bash
curl -sS "$BASE_URL/prices/1254" \
  -H "Authorization: Bearer $JWT"
```

Choose an article that is on sale for the selected category and record its numeric `id`. Do not calculate or trust a price supplied by the customer; the API recalculates the cart total from the article configuration.

## 3. Temporarily book seats

There are two supported reservation modes.

### A. Book specific seats

Use `/booksingleseat` once per selected seat:

```bash
curl -sS -X POST "$BASE_URL/booksingleseat" \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  --data '{
    "event": 1254,
    "section": 1,
    "categorie": "A",
    "seatid": 501
  }'
```

The response includes `seat` and the category's currently sellable `articles`:

```json
{
  "status": 200,
  "msg": "Seat booked",
  "seat": {
    "id": 501,
    "ref_section": 1,
    "categorie": "A",
    "rang": "B",
    "siege": "12"
  },
  "articles": {
    "42": {
      "id": 42,
      "libelle": "Adult",
      "price": 25
    }
  }
}
```

The reservation is temporary. A failed response means the seat was not reserved; do not add it to the cart.

### B. Let voodootix choose seats

Use `/bookseat` when the customer only needs a quantity in a section/category:

```bash
curl -sS -X POST "$BASE_URL/bookseat" \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  --data '{
    "event": 1254,
    "section": 1,
    "categorie": "A",
    "combien": 2
  }'
```

The successful response returns the selected seats keyed by seat ID, an `expire` Unix timestamp, and the available articles for the category. Use those returned seat IDs when adding the seats to the cart.

If any reservation call fails, release the remaining temporary selections before abandoning the flow:

```bash
curl -sS -X POST "$BASE_URL/cleantempcart" \
  -H "Authorization: Bearer $JWT"
```

## 4. Choose an article for every seat and add the seats to the cart

`POST /addtocart` requires an `articles` object keyed by the reserved seat ID. Each value is the article ID. Group seats with the same event, section, and category in one call; repeat the call for other groups.

```bash
curl -sS -X POST "$BASE_URL/addtocart" \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  --data '{
    "type": "ticket",
    "event": 1254,
    "section": 1,
    "categorie": "A",
    "articles": {
      "501": 42,
      "502": 42
    }
  }'
```

For the example above, seats `501` and `502` are both assigned article `42`. The seats must already belong to the current API session. If an article pack is being used, the optional `packs` object is keyed by seat ID and contains the pack UUID for each seat.

After adding all ticket groups, verify the cart:

```bash
curl -sS "$BASE_URL/cartdetails" \
  -H "Authorization: Bearer $JWT"
```

Check at least:

- `status == 200`;
- every requested seat exists in `data`;
- every cart line has the intended `article_id`;
- `total_net_price` is the expected amount;
- `expiration` has not passed;
- `booking_type_status` allows payment when that field is present.

### Combined booking shortcut

If the customer selects quantities by article rather than exact seat IDs, `/bookbyarticle` can reserve seats and add articles in one call:

```json
{
  "event": 1254,
  "seats": {
    "1": {
      "A": {
        "count": 2,
        "articles": {
          "42": 2
        }
      }
    }
  }
}
```

The article counts must add up to `count`. This shortcut does not let the agent choose individual seat IDs; use `/booksingleseat` plus `/addtocart` for assigned seating.

## 5. Create the order

For a partner API key, call `POST /ordercart` after the cart has been verified. This creates an external pending order. It does not mean that payment succeeded.

```bash
curl -sS -X POST "$BASE_URL/ordercart" \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  --data '{
    "contact": {
      "firstname": "Ada",
      "lastname": "Lovelace",
      "email": "ada@example.com",
      "mobile": "+33123456789",
      "country": "FR"
    },
    "dispatch": "digital"
  }'
```

The partner response is similar to:

```json
{
  "status": 200,
  "msg": "External order created and awaiting payment confirmation",
  "amount": 2700,
  "transaction_reference": "8e0b55e7f3664d6cbef78b8fa99d3c12",
  "cart_reference": "ABC_L8451",
  "cart_id": 8451,
  "order_status": "pending"
}
```

Save `amount`, `transaction_reference`, `cart_reference`, and `cart_id` exactly as returned. `amount` is in minor currency units: `2700` means `27.00` in a two-decimal currency. The cart reference includes the account prefix and must not be reconstructed by the agent.

The `contact` object can contain `civ`, `firstname`/`prenom`, `lastname`/`nom`, `email`, `mobile`, `address`, `zipcode`, `city`, `country`, and `newsletter`. Send the fields needed by the account's checkout rules.

## 6. Finalize the payment through IPN

Only call this after the payment provider has positively confirmed the transaction. Use the same JWT and API identity that created the pending order.

`POST /ipn` requires the exact order references and amount returned by `/ordercart`:

```bash
curl -sS -X POST "$BASE_URL/ipn" \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  --data '{
    "cart_reference": "ABC_L8451",
    "transaction_reference": "8e0b55e7f3664d6cbef78b8fa99d3c12",
    "amount": 2700,
    "transaction_status": "Completed",
    "transaction_id": "psp-transaction-123",
    "transaction_timestamp": "2026-09-18T12:34:56Z",
    "complementary_code": "approved",
    "dispatch": "digital"
  }'
```

Rules for the IPN payload:

- `cart_reference`, `transaction_reference`, and `amount` must exactly match the pending order;
- `transaction_status` must be exactly `Completed` to finalize the sale;
- `dispatch` is required for `Completed` and must be `digital`, `print`, `all`, or `whatsapp`;
- `transaction_id`, `transaction_timestamp`, and `complementary_code` are optional provider metadata;
- for a non-completed provider status, the API updates the pending payment status but does not finalize the sale.

A successful completion returns `status: 200` and normally an `orders` object whose entries also have `status: 200`. Do not treat the creation response's `order_status: "pending"` as a completed booking.

To inspect a pending external order, use the same credential:

```bash
curl -sS "$BASE_URL/checkipn/8451" \
  -H "Authorization: Bearer $JWT"
```

## Backoffice variation

The API key's configured role changes visibility and checkout behavior:

| API role | Event channel | Checkout type | IPN access |
| --- | --- | --- | --- |
| `partner` | `channel_api` | external partner order | yes |
| `backoffice` | `channel_cashdesk` | cashdesk order; default flow is still external/pending | yes |
| `front` | public web channel | web/PSP flow | no partner IPN |

With a `backoffice` credential, the default `/ordercart` call can still be finalized through `/ipn`. Alternatively, a backoffice agent may request direct finalization by sending `finalize_directly: true`, a configured `pmt_type`, and at least one `pmt_split` entry. That is a different cashdesk workflow and should only be used when the payment was actually collected by the cashdesk:

```json
{
  "contact": {
    "firstname": "Ada",
    "lastname": "Lovelace",
    "country": "FR"
  },
  "dispatch": "digital",
  "finalize_directly": true,
  "pmt_type": "cash",
  "pmt_split": [
    {
      "type": "cash",
      "amount": 27.00
    }
  ]
}
```

Do not send `finalize_directly` with a partner key; the API rejects that request.

## Failure handling

Common outcomes:

| Status/reason | Meaning | Agent action |
| --- | --- | --- |
| `401` | Missing/invalid JWT or insufficient operation rights | Re-authenticate only if the JWT is invalid; otherwise ask the account owner to grant the required API rights. |
| `403` / `event_not_shared` | The key cannot sell the event or use that channel | Stop and choose an event shared with this API key. |
| `404` on event/availability | Unknown, offline, expired, or no longer shared event | Refresh the event list and select another event. |
| `403` / `No more seats` | The requested inventory is unavailable | Refresh the map/availability and ask for another seat or quantity. |
| `400` from `/ipn` | Invalid or mismatched payment notification | Do not alter the amount or references; compare against the saved `/ordercart` response. |
| `404` from `/ipn` | The order does not match this API identity or references | Use the same API credential and exact saved values. Do not create a second order automatically. |

If the customer abandons the flow before `/ordercart`, call `POST /cleantempcart`. If one reserved seat must be released, call `POST /cleanseat` with `event`, `section`, `categorie`, and `seat`.

## Minimal agent checklist

Before reporting a booking as successful, confirm all of the following:

- authentication returned a JWT and the role is suitable;
- the selected event is visible to the API key;
- every selected seat was successfully temporarily reserved;
- every seat has an article ID that is on sale for its category;
- `/cartdetails` shows the expected seat/article mapping and total;
- `/ordercart` returned `order_status: "pending"` plus all four payment references;
- the payment provider confirmed the exact amount;
- `/ipn` returned `status: 200` and finalized order result(s) with `status: 200`.

The authoritative local OpenAPI source for this guide is `api/docs/v1/swagger.json` (OpenAPI 3.1.0).
