Viazo API
Your bookings, departures, rota and tours, in JSON or CSV — plus the two calls that keep Viazo and your own booking system from selling the same seat. Every key is scoped to one company and can never see anyone else’s data.
Getting a key
Dashboard → Profile / API → API keys. The key is shown once when you create it; we store only a hash, so if you lose it you replace it rather than recover it. Revoke a key and it stops working immediately. Reading is the default; writing is a separate tick box, so a key that leaks cannot close your departures.
curl https://www.viazo.co.uk/api/v1/bookings \
-H "Authorization: Bearer vz_live_xxxxxxxxxxxx"Endpoints
GET /api/v1/bookings
Every booking Viazo holds for you, in one shape, from both places they come from: the platforms you have connected — Viator, GetYourGuide, Airbnb, Civitatis and the rest — and bookings Viazo sold you directly. Direct ones carry platform: "viazo" and an availabilityId; imported ones carry the platform’s name and no availability of their own. Cancelled bookings are excluded unless you ask for status=all.
GET /api/v1/availability
What Viazo currently believes it can still sell, departure by departure — seats left, capacity, language, and whether the departure is closed. This is where you get the ids that PATCH takes, and it is what you call for a first sync and after any downtime of your own. Defaults to departures from now onwards; pass from for older ones, or tourId for a single tour.
POST /api/v1/bookings
Write a booking you took in your own system into Viazo, so it reaches the rota and the guide’s manifest instead of only shrinking a seat count. Needs a key with write access. See Keeping two systems in step below.
GET /api/v1/schedule
The rota: what runs when, and who is on it.
GET /api/v1/tours
Your tour catalogue — the reference data behind the titles in the others.
Parameters
| Name | Meaning |
|---|---|
| from, to | Date window. yyyy-mm-dd or a full ISO timestamp. |
| limit | Rows per page. Default 100, maximum 500. |
| offset | Rows to skip. Use nextOffset from the previous response. |
| format | json (default) or csv. |
| status | all to include cancelled bookings. /bookings only. |
| tourId | Restrict to one tour. /availability only. |
Paging
Responses carry nextOffset when there may be more. It is null on the last page, so you can loop until it is.
let offset = 0;
do {
const res = await fetch(
`https://www.viazo.co.uk/api/v1/bookings?limit=500&offset=${offset}`,
{ headers: { Authorization: `Bearer ${process.env.VIAZO_KEY}` } }
);
const page = await res.json();
handle(page.bookings);
offset = page.nextOffset;
} while (offset !== null);Straight into a spreadsheet
curl -L "https://www.viazo.co.uk/api/v1/bookings?format=csv&from=2026-11-01" \
-H "Authorization: Bearer $VIAZO_KEY" -o bookings.csvKeeping two systems in step
If you take bookings in your own software as well, the risk is selling the same seat twice. Closing that needs both directions, so there are four pieces: read the departures, hold seats you have sold, write the booking itself across, and listen for what we sell.
PATCH /api/v1/availability/{id}
Tell Viazo what is left, or close a departure outright. Needs a key with write access — tick Also allow writes when you create it. The id comes from GET /api/v1/availability, or from the availabilityId in a webhook.
curl -X PATCH https://www.viazo.co.uk/api/v1/availability/$ID \
-H "Authorization: Bearer $VIAZO_KEY" \
-H "Content-Type: application/json" \
-d '{"spotsRemaining": 4}'
# or close it completely
-d '{"bookingsPaused": true}'POST /api/v1/bookings
PATCH holds seats but says nothing about who is coming, so a guide sees a headcount with no names behind it. Post the booking instead and it lands on the rota and the manifest like any other.
Pass availabilityId and one request does both jobs in one transaction — records the booking and holds the seats — refusing with 409 rather than overselling. Leave it out and give tourTitle and startDateinstead; the booking is recorded and matched to the rota by title and time, exactly as an emailed booking is.
externalRef is your own reference and acts as the idempotency key. Retry a request whose response you never saw and you get the booking already stored, with idempotent: true and a 200 instead of a 201 — the seats are not taken twice. Only guests (adults) counts against capacity, matching how Viazo counts its own bookings. A timestamp without an offset or a Z is read as UK local time.
curl -X POST https://www.viazo.co.uk/api/v1/bookings \
-H "Authorization: Bearer $VIAZO_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "tourcms",
"externalRef": "ABC-12345",
"availabilityId": "cf553ed8-…",
"guests": 2,
"children": 1,
"guestName": "Alex Fuentes",
"guestEmail": "alex@example.com",
"totalPrice": "45.00",
"currency": "GBP"
}'
{"id":"…","platform":"tourcms","availabilityId":"cf553ed8-…",
"guests":2,"children":1,"status":"confirmed","spotsRemaining":9}This emits no webhook. You told us, so sending it back to you would only risk a loop.
Webhooks
Add an endpoint under Profile / API and we POST to it when Viazo takes or loses a booking. There are three events: booking.created, booking.cancelled and departure.cancelled.
Be aware of the timing before you design around it. A delivery is queued in the same breath as the booking, so it can never fail because your endpoint is down — but the queue is drained on a schedule, so expect a delivery within about five minutes, not instantly. If your own system sells the same departures, treat the webhook as the thing that catches you up and PATCH as the thing that keeps you safe in the meantime. Retries use exponential backoff, and an endpoint that keeps failing is disabled rather than hammered. HTTPS only — the payload carries guest details.
POST https://your-system.example/viazo-webhook
X-Viazo-Event: booking.created
X-Viazo-Signature: t=1730300000,v1=5f2b…
X-Viazo-Delivery: 9f1c2d3e-…
{
"event": "booking.created",
"sentAt": "2026-10-30T18:00:00.000Z",
"data": {
"bookingRef": "VZ-8H2K",
"tourTitle": "Soho, Covent Garden and Piccadilly",
"availabilityId": "cf553ed8-…",
"startsAt": "2026-11-14T16:30:00.000Z",
"numAdults": 2,
"spotsRemainingAfter": 10
}
}A cancellation looks the same but reports the seat count as it now stands, and says who cancelled — traveller, guide, operator or admin. When a whole departure is called off, departureCancelled is true and spotsRemaining is null: the departure is gone, so close it rather than resizing it.
{
"event": "booking.cancelled",
"sentAt": "2026-11-01T09:12:00.000Z",
"data": {
"bookingRef": "VZ-8H2K",
"tourTitle": "Soho, Covent Garden and Piccadilly",
"availabilityId": "cf553ed8-…",
"startsAt": "2026-11-14T16:30:00.000Z",
"numAdults": 2,
"numChildren": 0,
"spotsRemaining": 12,
"departureCancelled": false,
"cancelledBy": "traveller"
}
}departure.cancelled is separate on purpose. A booking.cancelled says seats were freed, which you might reasonably read as “sell them again”; when the departure itself has been called off that is exactly wrong. You get one of these per departure, alongside a booking.cancelled for each booking that was on it.
{
"event": "departure.cancelled",
"sentAt": "2026-11-02T09:12:00.000Z",
"data": {
"availabilityId": "48b557fc-…",
"tourId": "457e1577-…",
"tourTitle": "Soho, Covent Garden and Piccadilly",
"startsAt": "2026-11-14T16:30:00.000Z",
"language": "en",
"bookingsCancelled": 3,
"cancelledBy": "operator"
}
}Verify the signature before trusting a delivery. It is an HMAC-SHA256 of `${timestamp}.${rawBody}` using the secret shown when you added the endpoint, and we reject our own deliveries older than five minutes — you should too, or an old one can be replayed at you.
import { createHmac, timingSafeEqual } from "crypto";
function verify(secret, rawBody, header) {
const [, ts, mac] = header.match(/^t=(\d+),v1=([0-9a-f]+)$/) ?? [];
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(mac));
}Limits and good manners
- 120 requests per minute, per company. A 429 carries
Retry-After. - Reading cannot change anything. The two writes are
PATCH /api/v1/availability/{id}andPOST /api/v1/bookings, and both need a key you explicitly gave write access. - An id that belongs to another company returns 404, not 403 — we will not confirm that someone else’s departure exists.
- Responses are never cached. Do not put a key in a browser or a mobile app.
- Booking rows contain guest names and email addresses. Treat the export as personal data — you are its controller once it reaches your systems.
