Paths are relative to https://www.viaturquia.com/api/partner/v1. The curl examples assume the key is in $VT_API_KEY. {id} is a tour_id (every language version has its own); {ref} is a booking reference such as VT2026K7M2QX. The openapi.json describes the same endpoints, field by field.
Service and account
GET /ping
Is the service up, and does my Authorization header survive the trip?
No key needed
Request
curl "https://www.viaturquia.com/api/partner/v1/ping"
- The one endpoint that needs no key. authorization_header_seen is false when something between you and us removed the header - from our side that looks exactly like a wrong key, so check this first when every call answers 401.
- Answers 503 service_disabled while the API is switched off.
Response 200 - ok
{
"ok": true,
"version": "v1",
"time": "2026-10-22T10:15:00+03:00",
"authorization_header_seen": true
}
GET /me
Your partner profile, the key you are using, and your rate limit.
Scope: account:read
Request
curl "https://www.viaturquia.com/api/partner/v1/me" \
-H "Authorization: Bearer $VT_API_KEY"
- commission_pct is read only. rate_limit.remaining counts what is left after this request in the tighter of the two windows.
Response 200 - ok (51 lines)
{
"partner": {
"partner_id": 42,
"slug": "anatolia-trails",
"company_name": "Anatolia Trails",
"contact_name": "Elif Demir",
"email": "elif@anatolia-trails.example",
"phone": "+905321234567",
"country": "Turkey",
"license_number": "TURSAB-1234",
"type": "agency",
"website": "https://anatolia-trails.example",
"languages_spoken": [
"English",
"Spanish"
],
"description": "Day tours and short breaks in Istanbul and Cappadocia.",
"logo_url": null,
"status": "approved",
"api_enabled": true,
"notify_email": true,
"commission_pct": 15,
"bank_account_holder": "Anatolia Trails Ltd",
"bank_name": "Example Bank",
"bank_iban": "TR330006100519786457841326",
"bank_swift": "EXAMPTRIS",
"tax_id": "1234567890",
"approved_at": "2026-09-30T12:00:00+03:00",
"created_at": "2026-09-28T09:41:00+03:00"
},
"key": {
"key_id": "9f2c41d07ab35e18",
"label": "Production sync",
"scopes": [
"tours:read",
"tours:write",
"bookings:read",
"bookings:write",
"webhooks:write"
],
"expires_at": null,
"last_used_at": "2026-10-22T10:14:58+03:00",
"created_at": "2026-10-20T16:02:11+03:00"
},
"rate_limit": {
"per_minute": 120,
"per_day": 10000,
"remaining": 118,
"reset_at": "2026-10-22T10:16:00+03:00"
}
}
PATCH /me
Change your profile.
Scope: account:write
Request
curl -X PATCH "https://www.viaturquia.com/api/partner/v1/me" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+905321234567","website":"https://anatolia-trails.example","languages_spoken":["English","Spanish"]}'
Request body
{
"phone": "+905321234567",
"website": "https://anatolia-trails.example",
"languages_spoken": [
"English",
"Spanish"
]
}
- Editable: company_name, contact_name, phone (international form), country, website (http/https), description (simple formatting survives), languages_spoken (a list, at most 20), bank_account_holder, bank_name, bank_iban, bank_swift, tax_id.
- Your email, status, commission and everything else are not editable. They are not an error either: they come back in ignored[], so you can send back what GET /me gave you.
- A request with any invalid field changes nothing at all.
Response 200 - ok - what GET /me answers, plus ignored (52 lines)
{
"partner": {
"partner_id": 42,
"slug": "anatolia-trails",
"company_name": "Anatolia Trails",
"contact_name": "Elif Demir",
"email": "elif@anatolia-trails.example",
"phone": "+905321234567",
"country": "Turkey",
"license_number": "TURSAB-1234",
"type": "agency",
"website": "https://anatolia-trails.example",
"languages_spoken": [
"English",
"Spanish"
],
"description": "Day tours and short breaks in Istanbul and Cappadocia.",
"logo_url": null,
"status": "approved",
"api_enabled": true,
"notify_email": true,
"commission_pct": 15,
"bank_account_holder": "Anatolia Trails Ltd",
"bank_name": "Example Bank",
"bank_iban": "TR330006100519786457841326",
"bank_swift": "EXAMPTRIS",
"tax_id": "1234567890",
"approved_at": "2026-09-30T12:00:00+03:00",
"created_at": "2026-09-28T09:41:00+03:00"
},
"key": {
"key_id": "9f2c41d07ab35e18",
"label": "Production sync",
"scopes": [
"tours:read",
"tours:write",
"bookings:read",
"bookings:write",
"webhooks:write"
],
"expires_at": null,
"last_used_at": "2026-10-22T10:14:58+03:00",
"created_at": "2026-10-20T16:02:11+03:00"
},
"rate_limit": {
"per_minute": 120,
"per_day": 10000,
"remaining": 118,
"reset_at": "2026-10-22T10:16:00+03:00"
},
"ignored": []
}
422 invalid_phone, invalid_url, company_name_required, no_updatable_fields
GET /reference
Every fixed value you need to build a tour: languages, categories, destinations, seasons, hotel classes, room types.
Scope: any one of tours:read or account:read
Request
curl "https://www.viaturquia.com/api/partner/v1/reference" \
-H "Authorization: Bearer $VT_API_KEY"
- The ids you send in taxonomy and the season_id of a price cell come from here. Lists are shortened in the example.
- seasons[].date_ranges are MM-DD pairs and never cross the new year.
- Readable with either scope, so a catalogue-only key can resolve ids.
Response 200 - ok (170 lines)
{
"languages": [
{
"code": "en",
"name": "English"
},
{
"code": "es",
"name": "Español"
}
],
"categories": [
{
"category_id": 3,
"parent_id": null,
"slug": "city-tours",
"names": {
"en": "City tours",
"es": "Tours urbanos",
"pt": "Passeios urbanos",
"ar": null
}
}
],
"destinations": [
{
"destination_id": 1,
"slug": "istanbul",
"names": {
"en": "Istanbul",
"es": "Estambul",
"pt": "Istambul",
"ar": "إسطنبول"
}
}
],
"experience_types": [
{
"experience_type_id": 2,
"slug": "cultural",
"names": {
"en": "Cultural",
"es": "Cultural",
"pt": "Cultural",
"ar": null
}
}
],
"seasons": [
{
"season_id": 1,
"name_key": "highest",
"names": {
"en": "Highest season",
"es": "Temporada más alta",
"pt": null,
"ar": null
},
"date_ranges": [
{
"start": "08-01",
"end": "10-31"
}
]
}
],
"hotel_categories": [
{
"code": "A",
"names": {
"en": "5 stars",
"es": "5 estrellas",
"pt": null,
"ar": null
},
"descriptions": {
"en": "Luxury hotels",
"es": null,
"pt": null,
"ar": null
}
}
],
"room_types": [
{
"code": "double",
"capacity": 2,
"names": {
"en": "Double",
"es": "Doble",
"pt": "Duplo",
"ar": null
}
},
{
"code": "triple",
"capacity": 3,
"names": {
"en": "Triple",
"es": "Triple",
"pt": "Triplo",
"ar": null
}
},
{
"code": "individual",
"capacity": 1,
"names": {
"en": "Single",
"es": "Individual",
"pt": "Individual",
"ar": null
}
}
],
"difficulty_levels": [
{
"code": "easy",
"labels": {
"en": "Easy",
"es": "Fácil",
"pt": "Fácil",
"ar": "سهل"
}
}
],
"group_types": [
{
"code": "small_group",
"labels": {
"en": "Small group",
"es": "Grupo pequeño",
"pt": "Grupo pequeno",
"ar": "مجموعة صغيرة"
}
}
],
"age_ranges": [
{
"code": "all_ages",
"labels": {
"en": "All ages",
"es": "Todas las edades",
"pt": "Todas as idades",
"ar": "جميع الأعمار"
}
}
],
"document_types": [
"license",
"id",
"contract",
"insurance",
"other"
],
"guide_languages": [
"en",
"es",
"pt",
"ar",
"fr",
"de",
"it",
"ru",
"tr",
"ja",
"zh",
"ko"
]
}
GET /requests
Your own recent API calls (the audit trail).
Scope: account:read
Query parameters
| Name | Type | Meaning |
|---|
page | integer | Page number, from 1. |
per_page | integer | Rows per page. Default 50, at most 100. |
Request
curl "https://www.viaturquia.com/api/partner/v1/requests?page=1&per_page=50" \
-H "Authorization: Bearer $VT_API_KEY"
- Newest first. Request bodies are never stored. The request that asks is not in the list: it is logged as it is answered. Entries are kept for 30 days.
Response 200 - ok
{
"data": [
{
"request_id": "req_8f21c0a4e3",
"key_id": "9f2c41d07ab35e18",
"method": "GET",
"path": "/api/partner/v1/tours?page=1",
"status": 200,
"duration_ms": 31,
"ip": "203.0.113.7",
"idempotency_key": null,
"created_at": "2026-10-22T10:14:58+03:00"
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 1,
"has_more": false
}
}
GET /openapi.json
This API as an OpenAPI 3.1 document.
No key needed
Request
curl "https://www.viaturquia.com/api/partner/v1/openapi.json"
- Needs no key and is readable even while the API is switched off, so you can load it into Postman or Insomnia before you have an account.
200 ok — the OpenAPI 3.1 document, JSON.
Tours
GET /tours
Your tours, one summary row each.
Scope: tours:read
Query parameters
| Name | Type | Meaning |
|---|
page | integer | Page number, from 1. |
per_page | integer | Rows per page. Default 50, at most 100. |
language | string | en, es, pt or ar. |
active | string | 1 (published) or 0 (waiting for review). |
group_id | integer | Only the language versions of one tour. |
updated_since | string | ISO 8601 date or time, e.g. 2026-10-22T10:15:00+03:00. Only rows changed at or after it. Pass the previous list's pagination.as_of here. In a URL encode + as %2B. |
q | string | Matches the title or slug. |
Request
curl "https://www.viaturquia.com/api/partner/v1/tours?language=es&updated_since=2026-10-22T10%3A15%3A00%2B03%3A00" \
-H "Authorization: Bearer $VT_API_KEY"
- Ordered by tour_id. Every language version of a tour is its own row with its own tour_id; group_id ties them together.
- pagination.as_of is our clock, taken before the read. Store it and send it back as updated_since next time instead of your own clock: it cannot drift, and a row written while you read is caught by the next poll. updated_since is inclusive (>=), so the row at the boundary comes again - de-duplicate on tour_id.
Response 200 - ok
{
"data": [
{
"tour_id": 1207,
"group_id": 1207,
"language": "es",
"slug": "estambul-clasico",
"title": "Estambul Clásico",
"status": "awaiting_review",
"is_active": false,
"price": 85,
"duration_days": 1,
"duration_nights": 0,
"hotel_included": false,
"pricing_mode": "flat",
"main_image": "https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg",
"created_at": "2026-10-22T09:58:12+03:00",
"updated_at": "2026-10-22T10:02:40+03:00"
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 1,
"has_more": false,
"as_of": "2026-10-22T10:15:00+03:00"
}
}
422 one error listing every bad filter
POST /tours
Create a tour (one language).
Scope: tours:write · honours Idempotency-Key
Request
curl -X POST "https://www.viaturquia.com/api/partner/v1/tours" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d @body.json
Request body (71 lines)
{
"language": "es",
"title": "Estambul Clásico",
"short_description": "Un día completo entre Santa Sofía, la Mezquita Azul y el Bósforo.",
"description": "<p>Descubre el corazón histórico de Estambul con un guía local.</p>",
"highlights": [
"Santa Sofía",
"Mezquita Azul"
],
"starting_time": "09:00",
"meeting_point": "Vestíbulo de su hotel en Sultanahmet",
"duration": {
"days": 1,
"details": "8 horas"
},
"group": {
"type": "small_group",
"max_size": 16,
"min_participants": 2
},
"difficulty_level": "easy",
"includes": {
"hotel": false,
"guide": true,
"meals": true,
"transfers": true,
"tickets": true
},
"inclusions": [
"Guía en español",
"Entradas"
],
"itinerary": [
{
"time": "09:00",
"title": "Recogida en el hotel",
"description": "<p>Salida desde Sultanahmet.</p>"
}
],
"supplements": [
{
"name": "Cena en el Bósforo",
"price": 25,
"per_person": true
}
],
"pricing": {
"price": 85,
"original_price": 110,
"child_discount_pct": 30,
"infant_discount_pct": 100
},
"cancellation": {
"policy": "<p>Cancelación gratuita hasta 48 horas antes.</p>",
"free_hours": 48
},
"taxonomy": {
"category_id": 3,
"destinations": [
1
],
"experience_types": [
2
],
"guide_languages": [
"es",
"en"
]
},
"reference_url": "https://anatolia-trails.example/tour/123"
}
- Answers 201 with the same object GET /tours/{id} gives, plus ignored[]. The tour is born unpublished: status is awaiting_review until Via Turquia approves it. You cannot publish it yourself.
- language is en, es, pt or ar and defaults to es when you leave it out. To add the same tour in another language use POST /tours/{id}/translations - group_id cannot be set here.
- title is the only required field. Everything is described field by field in section 5.
- Send an Idempotency-Key so a retry after a timeout cannot create the tour twice.
Response 201 - created (123 lines)
{
"tour_id": 1207,
"group_id": 1207,
"language": "es",
"slug": "estambul-clasico",
"status": "awaiting_review",
"is_active": false,
"title": "Estambul Clásico",
"short_description": "Un día completo entre Santa Sofía, la Mezquita Azul y el Bósforo.",
"description": "<p>Descubre el corazón histórico de Estambul con un guía local.</p>",
"highlights": [
"Santa Sofía",
"Mezquita Azul",
"Crucero por el Bósforo"
],
"important_info": "Lleve calzado cómodo y hombros cubiertos para las mezquitas.",
"meeting_point": "Vestíbulo de su hotel en Sultanahmet",
"ending_point": "Gran Bazar",
"starting_time": "09:00",
"duration": {
"days": 1,
"nights": 0,
"details": "8 horas"
},
"group": {
"type": "Grupo pequeño",
"max_size": 16,
"min_participants": 2,
"max_participants": 16
},
"age": {
"min_age": 6,
"range": "Todas las edades",
"child_label": "6-11",
"infant_label": "0-5"
},
"difficulty_level": "easy",
"difficulty_label": "Fácil",
"includes": {
"hotel": false,
"guide": true,
"meals": true,
"transfers": true,
"tickets": true
},
"inclusions": [
"Guía en español",
"Entradas",
"Almuerzo"
],
"exclusions": [
"Bebidas",
"Propinas"
],
"itinerary": [
{
"time": "09:00",
"title": "Recogida en el hotel",
"description": "<p>Salida desde Sultanahmet.</p>"
},
{
"time": "09:30",
"title": "Santa Sofía",
"description": "<p>Visita guiada de una hora.</p>"
}
],
"faqs": [
{
"question": "¿Hay que reservar con antelación?",
"answer": "Sí, con 24 horas."
}
],
"supplements": [
{
"name": "Cena en el Bósforo",
"description": null,
"price": 25,
"per_person": true,
"active": true
}
],
"pricing": {
"mode": "flat",
"hotel_included": false,
"matrix_cells": 0,
"matrix_complete": false,
"price": 85,
"original_price": 110,
"child_discount_pct": 30,
"infant_discount_pct": 100
},
"cancellation": {
"policy": "<p>Cancelación gratuita hasta 48 horas antes.</p>",
"free_hours": 48
},
"media": {
"main_image": "https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg",
"gallery": [
"https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg"
],
"video_url": null
},
"taxonomy": {
"category_id": 3,
"categories": [
3
],
"destinations": [
1
],
"experience_types": [
2
],
"guide_languages": [
"es",
"en"
]
},
"reference_url": "https://anatolia-trails.example/tour/123",
"created_at": "2026-10-22T09:58:12+03:00",
"updated_at": "2026-10-22T10:02:40+03:00",
"ignored": []
}
Response 422 - every wrong field in error.errors
{
"error": {
"type": "validation_error",
"code": "tour_title_required",
"message": "A tour needs a title. (2 fields were refused - see error.errors.)",
"field": "title",
"errors": [
{
"code": "tour_title_required",
"field": "title",
"message": "A tour needs a title."
},
{
"code": "invalid_category",
"field": "taxonomy.category_id",
"message": "No active category with id 99. GET /reference lists them."
}
]
},
"request_id": "req_8f21c0a4e3"
}
GET /tours/{id}
One tour in full.
Scope: tours:read
Request
curl "https://www.viaturquia.com/api/partner/v1/tours/1207" \
-H "Authorization: Bearer $VT_API_KEY"
- The object is also the shape POST and PATCH accept: fetch it, change a value, send it back. Fields we compute or that are yours to read only (tour_id, group_id, slug, status, is_active, created_at, updated_at, difficulty_label, pricing.mode and the other derived pricing fields) are dropped on the way in and listed in ignored[].
- taxonomy.category_id is the tour's primary category, or null when none is set; taxonomy.categories is every category the tour is listed in.
- Someone else's tour is a 404, exactly like one that does not exist.
Response 200 - ok (122 lines)
{
"tour_id": 1207,
"group_id": 1207,
"language": "es",
"slug": "estambul-clasico",
"status": "awaiting_review",
"is_active": false,
"title": "Estambul Clásico",
"short_description": "Un día completo entre Santa Sofía, la Mezquita Azul y el Bósforo.",
"description": "<p>Descubre el corazón histórico de Estambul con un guía local.</p>",
"highlights": [
"Santa Sofía",
"Mezquita Azul",
"Crucero por el Bósforo"
],
"important_info": "Lleve calzado cómodo y hombros cubiertos para las mezquitas.",
"meeting_point": "Vestíbulo de su hotel en Sultanahmet",
"ending_point": "Gran Bazar",
"starting_time": "09:00",
"duration": {
"days": 1,
"nights": 0,
"details": "8 horas"
},
"group": {
"type": "Grupo pequeño",
"max_size": 16,
"min_participants": 2,
"max_participants": 16
},
"age": {
"min_age": 6,
"range": "Todas las edades",
"child_label": "6-11",
"infant_label": "0-5"
},
"difficulty_level": "easy",
"difficulty_label": "Fácil",
"includes": {
"hotel": false,
"guide": true,
"meals": true,
"transfers": true,
"tickets": true
},
"inclusions": [
"Guía en español",
"Entradas",
"Almuerzo"
],
"exclusions": [
"Bebidas",
"Propinas"
],
"itinerary": [
{
"time": "09:00",
"title": "Recogida en el hotel",
"description": "<p>Salida desde Sultanahmet.</p>"
},
{
"time": "09:30",
"title": "Santa Sofía",
"description": "<p>Visita guiada de una hora.</p>"
}
],
"faqs": [
{
"question": "¿Hay que reservar con antelación?",
"answer": "Sí, con 24 horas."
}
],
"supplements": [
{
"name": "Cena en el Bósforo",
"description": null,
"price": 25,
"per_person": true,
"active": true
}
],
"pricing": {
"mode": "flat",
"hotel_included": false,
"matrix_cells": 0,
"matrix_complete": false,
"price": 85,
"original_price": 110,
"child_discount_pct": 30,
"infant_discount_pct": 100
},
"cancellation": {
"policy": "<p>Cancelación gratuita hasta 48 horas antes.</p>",
"free_hours": 48
},
"media": {
"main_image": "https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg",
"gallery": [
"https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg"
],
"video_url": null
},
"taxonomy": {
"category_id": 3,
"categories": [
3
],
"destinations": [
1
],
"experience_types": [
2
],
"guide_languages": [
"es",
"en"
]
},
"reference_url": "https://anatolia-trails.example/tour/123",
"created_at": "2026-10-22T09:58:12+03:00",
"updated_at": "2026-10-22T10:02:40+03:00"
}
404 tour_not_found
PATCH /tours/{id}
Change part of a tour. A field you leave out is not touched.
Scope: tours:write
Request
curl -X PATCH "https://www.viaturquia.com/api/partner/v1/tours/1207" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Estambul Clásico en un día","pricing":{"price":89}}'
Request body
{
"title": "Estambul Clásico en un día",
"pricing": {
"price": 89
}
}
- A tour that is already published stays published: your change goes live at once, without a new review. It is recorded in your audit trail.
- Some fields change every language version at once - see "Which fields are shared" in section 5.
- Lists (inclusions, itinerary, faqs, supplements, gallery, taxonomy lists) are replaced as a whole when you send them. supplements is the sharp one: every supplement row is replaced, including the inactive ones, so leaving one out deletes it.
- Sending only {"pricing":{"price":N}} to a tour priced by a room matrix is 422 no_updatable_fields, because on such a tour the price is derived from the matrix and pricing.price is ignored. Together with other fields it simply appears in ignored[].
- A request that fails validation changes nothing.
Response 200 - ok (123 lines)
{
"tour_id": 1207,
"group_id": 1207,
"language": "es",
"slug": "estambul-clasico",
"status": "awaiting_review",
"is_active": false,
"title": "Estambul Clásico en un día",
"short_description": "Un día completo entre Santa Sofía, la Mezquita Azul y el Bósforo.",
"description": "<p>Descubre el corazón histórico de Estambul con un guía local.</p>",
"highlights": [
"Santa Sofía",
"Mezquita Azul",
"Crucero por el Bósforo"
],
"important_info": "Lleve calzado cómodo y hombros cubiertos para las mezquitas.",
"meeting_point": "Vestíbulo de su hotel en Sultanahmet",
"ending_point": "Gran Bazar",
"starting_time": "09:00",
"duration": {
"days": 1,
"nights": 0,
"details": "8 horas"
},
"group": {
"type": "Grupo pequeño",
"max_size": 16,
"min_participants": 2,
"max_participants": 16
},
"age": {
"min_age": 6,
"range": "Todas las edades",
"child_label": "6-11",
"infant_label": "0-5"
},
"difficulty_level": "easy",
"difficulty_label": "Fácil",
"includes": {
"hotel": false,
"guide": true,
"meals": true,
"transfers": true,
"tickets": true
},
"inclusions": [
"Guía en español",
"Entradas",
"Almuerzo"
],
"exclusions": [
"Bebidas",
"Propinas"
],
"itinerary": [
{
"time": "09:00",
"title": "Recogida en el hotel",
"description": "<p>Salida desde Sultanahmet.</p>"
},
{
"time": "09:30",
"title": "Santa Sofía",
"description": "<p>Visita guiada de una hora.</p>"
}
],
"faqs": [
{
"question": "¿Hay que reservar con antelación?",
"answer": "Sí, con 24 horas."
}
],
"supplements": [
{
"name": "Cena en el Bósforo",
"description": null,
"price": 25,
"per_person": true,
"active": true
}
],
"pricing": {
"mode": "flat",
"hotel_included": false,
"matrix_cells": 0,
"matrix_complete": false,
"price": 89,
"original_price": 110,
"child_discount_pct": 30,
"infant_discount_pct": 100
},
"cancellation": {
"policy": "<p>Cancelación gratuita hasta 48 horas antes.</p>",
"free_hours": 48
},
"media": {
"main_image": "https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg",
"gallery": [
"https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg"
],
"video_url": null
},
"taxonomy": {
"category_id": 3,
"categories": [
3
],
"destinations": [
1
],
"experience_types": [
2
],
"guide_languages": [
"es",
"en"
]
},
"reference_url": "https://anatolia-trails.example/tour/123",
"created_at": "2026-10-22T09:58:12+03:00",
"updated_at": "2026-10-22T10:30:00+03:00",
"ignored": []
}
404 tour_not_found
Response 422 - every wrong field in error.errors
{
"error": {
"type": "validation_error",
"code": "tour_title_required",
"message": "A tour needs a title. (2 fields were refused - see error.errors.)",
"field": "title",
"errors": [
{
"code": "tour_title_required",
"field": "title",
"message": "A tour needs a title."
},
{
"code": "invalid_category",
"field": "taxonomy.category_id",
"message": "No active category with id 99. GET /reference lists them."
}
]
},
"request_id": "req_8f21c0a4e3"
}
DELETE /tours/{id}
Delete one language version of a tour.
Scope: tours:write
Request
curl -X DELETE "https://www.viaturquia.com/api/partner/v1/tours/1207" \
-H "Authorization: Bearer $VT_API_KEY"
- Deletes only the row you address, not its other languages. A tour that has any booking - even a cancelled one - answers 409 tour_has_bookings and stays; ask us to take it off the site instead.
Response 200 - ok
{
"deleted": true,
"tour_id": 1207
}
409 tour_has_bookings
POST /tours/{id}/translations
Open a tour in another language.
Scope: tours:write · honours Idempotency-Key
Request
curl -X POST "https://www.viaturquia.com/api/partner/v1/tours/1207/translations" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"language":"en"}'
Request body
{
"language": "en"
}
- Creates a new language version in the same tour group, unpublished. It copies the structure - durations, group sizes, cancellation hours, prices and the whole matrix, the include switches, categories, destinations, experience types, guide languages, ages - and leaves every text empty. The translation is yours to write with PATCH.
- needs[] lists what you still have to fill in. Supplements arrive with their price but inactive and with no name (needs shows the source name as a hint); they are not sold until you PATCH supplements with a name and "active": true.
- A language the group already has is 409 translation_exists.
Response 201 - created (111 lines)
{
"tour_id": 1208,
"group_id": 1207,
"language": "en",
"slug": "estambul-clasico-en",
"status": "awaiting_review",
"is_active": false,
"title": "",
"short_description": null,
"description": null,
"highlights": [],
"important_info": null,
"meeting_point": null,
"ending_point": null,
"starting_time": null,
"duration": {
"days": 1,
"nights": 0,
"details": null
},
"group": {
"type": null,
"max_size": 16,
"min_participants": 2,
"max_participants": 16
},
"age": {
"min_age": 6,
"range": null,
"child_label": "6-11",
"infant_label": "0-5"
},
"difficulty_level": null,
"difficulty_label": null,
"includes": {
"hotel": false,
"guide": true,
"meals": true,
"transfers": true,
"tickets": true
},
"inclusions": [],
"exclusions": [],
"itinerary": [],
"faqs": [],
"supplements": [
{
"name": "",
"description": null,
"price": 25,
"per_person": true,
"active": false
}
],
"pricing": {
"mode": "flat",
"hotel_included": false,
"matrix_cells": 0,
"matrix_complete": false,
"price": 85,
"original_price": 110,
"child_discount_pct": 30,
"infant_discount_pct": 100
},
"cancellation": {
"policy": null,
"free_hours": 48
},
"media": {
"main_image": null,
"gallery": [],
"video_url": null
},
"taxonomy": {
"category_id": 3,
"categories": [
3
],
"destinations": [
1
],
"experience_types": [
2
],
"guide_languages": [
"es",
"en"
]
},
"reference_url": "https://anatolia-trails.example/tour/123",
"created_at": "2026-10-22T09:58:12+03:00",
"updated_at": "2026-10-22T10:02:40+03:00",
"ignored": [],
"needs": [
"title",
"short_description",
"description",
"highlights",
"important_info",
"itinerary",
"inclusions",
"exclusions",
"meeting_point",
"ending_point",
"cancellation.policy",
"faqs",
"media.main_image",
"media.gallery",
"supplements[0].name (source: Cena en el Bósforo)"
]
}
409 translation_exists
422 invalid_language
Pricing
GET /tours/{id}/pricing
The 27-cell room matrix of an accommodation tour, or the single price of a day tour.
Scope: tours:read
Request
curl "https://www.viaturquia.com/api/partner/v1/tours/1207/pricing" \
-H "Authorization: Bearer $VT_API_KEY"
- group_tour_ids lists the language versions that share these prices - the rows a write reaches.
- mode is matrix as soon as one cell exists. matrix_complete tells an accommodation tour whose matrix you have not finished from a day tour.
- The example shows three of the 27 cells.
Response 200 - a day tour
{
"tour_id": 1207,
"group_id": 1207,
"group_tour_ids": [
1207
],
"mode": "flat",
"hotel_included": false,
"matrix_cells": 0,
"matrix_complete": false,
"price": 85,
"original_price": 110,
"child_discount_pct": 30,
"infant_discount_pct": 100
}
Response 200 - an accommodation tour (38 lines)
{
"tour_id": 1301,
"group_id": 1301,
"group_tour_ids": [
1301,
1302,
1303,
1304
],
"mode": "matrix",
"hotel_included": true,
"matrix_cells": 27,
"matrix_complete": true,
"price": 410,
"original_price": null,
"child_discount_pct": 30,
"infant_discount_pct": 100,
"matrix": [
{
"season_id": 1,
"hotel_category": "A",
"room_type": "double",
"price": 640
},
{
"season_id": 1,
"hotel_category": "A",
"room_type": "triple",
"price": 610
},
{
"season_id": 1,
"hotel_category": "A",
"room_type": "individual",
"price": 840
}
]
}
PUT /tours/{id}/pricing
Write the prices: the whole matrix for an accommodation tour, or one price for a day tour.
Scope: tours:write
Request
curl -X PUT "https://www.viaturquia.com/api/partner/v1/tours/1207/pricing" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"matrix":[{"season_id":1,"hotel_category":"A","room_type":"double","price":640},{"season_id":1,"hotel_category":"A","room_type":"triple","price":610},{"season_id":1,"hotel_category":"A","room_type":"individual","price":840}]}'
Request body
{
"matrix": [
{
"season_id": 1,
"hotel_category": "A",
"room_type": "double",
"price": 640
},
{
"season_id": 1,
"hotel_category": "A",
"room_type": "triple",
"price": 610
},
{
"season_id": 1,
"hotel_category": "A",
"room_type": "individual",
"price": 840
}
]
}
Request body - A day tour sends one price
{
"price": 89
}
- matrix: all 27 cells are required (3 seasons x hotel classes A/B/C x room types double/triple/individual), each above 0. A missing cell is 422 and the message names every one that is missing. The figures are per person, in EUR.
- The prices are written to every language version of the tour (that is also an accommodation tour). tours.price - the "from" price on the site - is then set to the cheapest cell; you cannot set it by hand on such a tour.
- A matrix for a tour whose includes.hotel is false is 409 matrix_on_day_tour; a single price for a tour that includes accommodation is 409 flat_price_on_hotel_tour.
- Anything else in the body is dropped and listed in ignored[] - so you can send back what GET returned.
Response 200 - ok - what GET answers, plus ignored (39 lines)
{
"tour_id": 1301,
"group_id": 1301,
"group_tour_ids": [
1301,
1302,
1303,
1304
],
"mode": "matrix",
"hotel_included": true,
"matrix_cells": 27,
"matrix_complete": true,
"price": 410,
"original_price": null,
"child_discount_pct": 30,
"infant_discount_pct": 100,
"matrix": [
{
"season_id": 1,
"hotel_category": "A",
"room_type": "double",
"price": 640
},
{
"season_id": 1,
"hotel_category": "A",
"room_type": "triple",
"price": 610
},
{
"season_id": 1,
"hotel_category": "A",
"room_type": "individual",
"price": 840
}
],
"ignored": []
}
409 matrix_on_day_tour, flat_price_on_hotel_tour
422 matrix_cell_missing, invalid_matrix_cell, duplicate_matrix_cell, invalid_matrix_price, invalid_price
Images
POST /tours/{id}/images
Add an image to a tour's gallery - upload a file, or give us an address to fetch.
Scope: tours:write · honours Idempotency-Key
Request
curl -X POST "https://www.viaturquia.com/api/partner/v1/tours/1207/images" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url":"https://anatolia-trails.example/photos/santa-sofia.jpg"}'
Or upload the file
curl -X POST "https://www.viaturquia.com/api/partner/v1/tours/1207/images" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@santa-sofia.jpg"
Request body
{
"url": "https://anatolia-trails.example/photos/santa-sofia.jpg"
}
- Either multipart/form-data with the file in a part named "file" (or "image"), or JSON {"url": "https://..."} and our server downloads it.
- Accepted: JPEG, PNG, GIF, WebP. HEIC/HEIF photos (iPhones) are refused with heic_not_supported. Uploads up to 12 MB; downloads up to 5 MB; at most 50 megapixels. We apply the EXIF rotation and scale the longest edge down to 1920 px.
- A fetched address must be https, must resolve to a public address, is not allowed to redirect, and has 10 seconds. At most 20 image POSTs a minute per key.
- The image is appended to the gallery of that one language version (a gallery belongs to its row). The answer is the stored address; to make it the main image, send it as media.main_image with PATCH. A tour holds at most 40 gallery images.
- Media addresses in a PATCH/POST body must already be on Via Turquia - this endpoint is how an outside image gets there.
Response 201 - created
{
"tour_id": 1207,
"url": "https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg",
"source": "url",
"gallery_count": 2
}
422 image_required, unsafe_url, fetch_failed, heic_not_supported, not_an_image, too_many_images
413 file_too_large
DELETE /tours/{id}/images
Take an image out of a tour's gallery.
Scope: tours:write
Request
curl -X DELETE "https://www.viaturquia.com/api/partner/v1/tours/1207/images" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg"}'
Request body
{
"url": "https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg"
}
- The image leaves the gallery; the file itself is not deleted from our storage.
Response 200 - ok
{
"deleted": true,
"tour_id": 1207,
"url": "https://www.viaturquia.com/uploads/tours/3f9c1e0a7b52d4c8.jpg",
"gallery_count": 1
}
404 image_not_in_gallery, tour_not_found
Bookings
GET /bookings
Bookings made on your tours, newest first.
Scope: bookings:read
Query parameters
| Name | Type | Meaning |
|---|
page | integer | Page number, from 1. |
per_page | integer | Rows per page. Default 50, at most 100. |
status | string | pending, confirmed, cancelled or completed. |
tour_id | integer | One tour row. |
date_from | string | Travel date from, YYYY-MM-DD. |
date_to | string | Travel date to, YYYY-MM-DD. |
updated_since | string | ISO 8601 date or time, e.g. 2026-10-22T10:15:00+03:00. Only rows changed at or after it. Pass the previous list's pagination.as_of here. In a URL encode + as %2B. |
Request
curl "https://www.viaturquia.com/api/partner/v1/bookings?status=pending&date_from=2026-11-01" \
-H "Authorization: Bearer $VT_API_KEY"
- Customers book on the Via Turquia site (or through its chat assistant); you cannot create a booking through the API.
- pagination.as_of works as it does for tours.
- The customer's name, email and phone are in the booking: the same details your new-booking email already carries, so you can run the tour.
Response 200 - ok (102 lines)
{
"data": [
{
"booking_ref": "VT2026K7M2QX",
"booking_id": 8841,
"status": "pending",
"source": "web",
"tour": {
"tour_id": 1301,
"group_id": 1301,
"language": "en",
"title": "Cappadocia 3 Days",
"slug": "cappadocia-3-days"
},
"booking_date": "2026-11-14",
"travellers": {
"adults": 2,
"children": 0,
"infants": 0,
"total": 2
},
"rooms": [
{
"room_type_id": 1,
"room_type": "double",
"room_type_name": "Double",
"capacity": 2,
"hotel_category_id": 2,
"hotel_category": "B",
"hotel_category_name": "4 stars",
"season_id": 1,
"season": "highest",
"season_name": "Highest season",
"occupancy": 2,
"travellers": {
"adults": 2,
"children": 0,
"infants": 0
},
"price_per_person": 520,
"room_subtotal": 1040
}
],
"room_summary": {
"season_id": 1,
"season_name": "Highest season",
"hotel_category_id": 2,
"hotel_category_name": "4 stars",
"room_type_id": 1,
"room_type_name": "Double"
},
"supplements": {
"info": "Hot-air balloon x2",
"total": 300
},
"special_requests": "Vegetarian meals, please.",
"hotel": {
"name": "Cave Hotel Goreme",
"address": "Goreme, Nevsehir"
},
"pickup_time": "08:30:00",
"customer": {
"name": "Ana García",
"email": "ana@example.com",
"phone": "+34600111222",
"country": "Spain"
},
"amounts": {
"currency": "EUR",
"price_per_person": 520,
"discount": 0,
"promo_code": null,
"total": 1340,
"commission_pct": 15,
"commission": 201,
"net": 1139,
"currency_shown": "EUR"
},
"payment": {
"deposit_status": "pending",
"deposit_amount": 268,
"deposit_paid_at": null,
"deposit_method": null,
"payment_status": "pending"
},
"partner_answer": {
"action": "none",
"at": null,
"note": null
},
"created_at": "2026-10-22T09:12:30+03:00",
"updated_at": "2026-10-22T09:12:30+03:00"
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 1,
"has_more": false,
"as_of": "2026-10-22T10:15:00+03:00"
}
}
GET /bookings/{ref}
One booking, by its reference (VT2026...).
Scope: bookings:read
Request
curl "https://www.viaturquia.com/api/partner/v1/bookings/VT2026K7M2QX" \
-H "Authorization: Bearer $VT_API_KEY"
- rooms[] is one entry per room, with the room type, hotel class, season and who sleeps in it; room_summary is the single-room summary kept on the booking row.
- amounts are EUR. commission and net are zero for a cancelled booking. currency_shown is only what the customer saw on the page.
- payment tells you whether the deposit has arrived. partner_answer is what you answered (none until you do) and stays even if Via Turquia later changes the status.
Response 200 - ok (91 lines)
{
"booking_ref": "VT2026K7M2QX",
"booking_id": 8841,
"status": "pending",
"source": "web",
"tour": {
"tour_id": 1301,
"group_id": 1301,
"language": "en",
"title": "Cappadocia 3 Days",
"slug": "cappadocia-3-days"
},
"booking_date": "2026-11-14",
"travellers": {
"adults": 2,
"children": 0,
"infants": 0,
"total": 2
},
"rooms": [
{
"room_type_id": 1,
"room_type": "double",
"room_type_name": "Double",
"capacity": 2,
"hotel_category_id": 2,
"hotel_category": "B",
"hotel_category_name": "4 stars",
"season_id": 1,
"season": "highest",
"season_name": "Highest season",
"occupancy": 2,
"travellers": {
"adults": 2,
"children": 0,
"infants": 0
},
"price_per_person": 520,
"room_subtotal": 1040
}
],
"room_summary": {
"season_id": 1,
"season_name": "Highest season",
"hotel_category_id": 2,
"hotel_category_name": "4 stars",
"room_type_id": 1,
"room_type_name": "Double"
},
"supplements": {
"info": "Hot-air balloon x2",
"total": 300
},
"special_requests": "Vegetarian meals, please.",
"hotel": {
"name": "Cave Hotel Goreme",
"address": "Goreme, Nevsehir"
},
"pickup_time": "08:30:00",
"customer": {
"name": "Ana García",
"email": "ana@example.com",
"phone": "+34600111222",
"country": "Spain"
},
"amounts": {
"currency": "EUR",
"price_per_person": 520,
"discount": 0,
"promo_code": null,
"total": 1340,
"commission_pct": 15,
"commission": 201,
"net": 1139,
"currency_shown": "EUR"
},
"payment": {
"deposit_status": "pending",
"deposit_amount": 268,
"deposit_paid_at": null,
"deposit_method": null,
"payment_status": "pending"
},
"partner_answer": {
"action": "none",
"at": null,
"note": null
},
"created_at": "2026-10-22T09:12:30+03:00",
"updated_at": "2026-10-22T09:12:30+03:00"
}
404 booking_not_found
POST /bookings/{ref}/accept
Accept a pending booking.
Scope: bookings:write · honours Idempotency-Key
Request
curl -X POST "https://www.viaturquia.com/api/partner/v1/bookings/VT2026K7M2QX/accept" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"note":"Rooms held."}'
Request body
{
"note": "Rooms held."
}
- The booking becomes confirmed and Via Turquia may now ask the customer for the deposit. The customer is not mailed by this call.
- Only a pending booking can be accepted: anything else is 409 booking_not_pending. Two simultaneous accepts are safe - one wins, the other gets the 409.
- note is optional, plain text, 500 characters.
Response 200 - ok - the booking as it now stands, plus action, message and ignored (94 lines)
{
"booking_ref": "VT2026K7M2QX",
"booking_id": 8841,
"status": "confirmed",
"source": "web",
"tour": {
"tour_id": 1301,
"group_id": 1301,
"language": "en",
"title": "Cappadocia 3 Days",
"slug": "cappadocia-3-days"
},
"booking_date": "2026-11-14",
"travellers": {
"adults": 2,
"children": 0,
"infants": 0,
"total": 2
},
"rooms": [
{
"room_type_id": 1,
"room_type": "double",
"room_type_name": "Double",
"capacity": 2,
"hotel_category_id": 2,
"hotel_category": "B",
"hotel_category_name": "4 stars",
"season_id": 1,
"season": "highest",
"season_name": "Highest season",
"occupancy": 2,
"travellers": {
"adults": 2,
"children": 0,
"infants": 0
},
"price_per_person": 520,
"room_subtotal": 1040
}
],
"room_summary": {
"season_id": 1,
"season_name": "Highest season",
"hotel_category_id": 2,
"hotel_category_name": "4 stars",
"room_type_id": 1,
"room_type_name": "Double"
},
"supplements": {
"info": "Hot-air balloon x2",
"total": 300
},
"special_requests": "Vegetarian meals, please.",
"hotel": {
"name": "Cave Hotel Goreme",
"address": "Goreme, Nevsehir"
},
"pickup_time": "08:30:00",
"customer": {
"name": "Ana García",
"email": "ana@example.com",
"phone": "+34600111222",
"country": "Spain"
},
"amounts": {
"currency": "EUR",
"price_per_person": 520,
"discount": 0,
"promo_code": null,
"total": 1340,
"commission_pct": 15,
"commission": 201,
"net": 1139,
"currency_shown": "EUR"
},
"payment": {
"deposit_status": "pending",
"deposit_amount": 268,
"deposit_paid_at": null,
"deposit_method": null,
"payment_status": "pending"
},
"partner_answer": {
"action": "accepted",
"at": "2026-10-22T10:20:05+03:00",
"note": "Rooms held."
},
"created_at": "2026-10-22T09:12:30+03:00",
"updated_at": "2026-10-22T10:20:05+03:00",
"action": "accepted",
"message": "The booking is confirmed. Via Turquia will ask the customer for the deposit.",
"ignored": []
}
Response 409 - booking_not_pending
{
"error": {
"type": "conflict",
"code": "booking_not_pending",
"message": "Only a booking that is still pending can be accepted. This one is confirmed."
},
"request_id": "req_a41b7c90d2"
}
POST /bookings/{ref}/decline
Decline a pending or confirmed booking.
Scope: bookings:write · honours Idempotency-Key
Request
curl -X POST "https://www.viaturquia.com/api/partner/v1/bookings/VT2026K7M2QX/decline" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"reason":"No rooms on that date."}'
Request body
{
"reason": "No rooms on that date."
}
- The booking is cancelled and the customer is emailed the cancellation automatically, in their own language. customer_notified is false when that email could not be delivered (a blocked address, a failed send) - then reach the customer another way.
- Declining works only while the deposit has not been paid. Once it has, the answer is 409 deposit_already_paid and Via Turquia is told: money has changed hands, so only we can cancel and refund.
- reason is optional, plain text, 500 characters; it is stored with the booking.
Response 200 - ok - the booking as it now stands, plus action, message, customer_notified and ignored (95 lines)
{
"booking_ref": "VT2026K7M2QX",
"booking_id": 8841,
"status": "cancelled",
"source": "web",
"tour": {
"tour_id": 1301,
"group_id": 1301,
"language": "en",
"title": "Cappadocia 3 Days",
"slug": "cappadocia-3-days"
},
"booking_date": "2026-11-14",
"travellers": {
"adults": 2,
"children": 0,
"infants": 0,
"total": 2
},
"rooms": [
{
"room_type_id": 1,
"room_type": "double",
"room_type_name": "Double",
"capacity": 2,
"hotel_category_id": 2,
"hotel_category": "B",
"hotel_category_name": "4 stars",
"season_id": 1,
"season": "highest",
"season_name": "Highest season",
"occupancy": 2,
"travellers": {
"adults": 2,
"children": 0,
"infants": 0
},
"price_per_person": 520,
"room_subtotal": 1040
}
],
"room_summary": {
"season_id": 1,
"season_name": "Highest season",
"hotel_category_id": 2,
"hotel_category_name": "4 stars",
"room_type_id": 1,
"room_type_name": "Double"
},
"supplements": {
"info": "Hot-air balloon x2",
"total": 300
},
"special_requests": "Vegetarian meals, please.",
"hotel": {
"name": "Cave Hotel Goreme",
"address": "Goreme, Nevsehir"
},
"pickup_time": "08:30:00",
"customer": {
"name": "Ana García",
"email": "ana@example.com",
"phone": "+34600111222",
"country": "Spain"
},
"amounts": {
"currency": "EUR",
"price_per_person": 520,
"discount": 0,
"promo_code": null,
"total": 1340,
"commission_pct": 15,
"commission": 0,
"net": 0,
"currency_shown": "EUR"
},
"payment": {
"deposit_status": "pending",
"deposit_amount": 268,
"deposit_paid_at": null,
"deposit_method": null,
"payment_status": "pending"
},
"partner_answer": {
"action": "declined",
"at": "2026-10-22T10:20:05+03:00",
"note": "No rooms on that date."
},
"created_at": "2026-10-22T09:12:30+03:00",
"updated_at": "2026-10-22T10:20:05+03:00",
"action": "declined",
"message": "The booking is cancelled and the customer has been told by email.",
"customer_notified": true,
"ignored": []
}
409 booking_not_cancellable, deposit_already_paid
Webhook
GET /webhook
Your webhook registration and how delivery is going.
Scope: any one of webhooks:write or account:read
Request
curl "https://www.viaturquia.com/api/partner/v1/webhook" \
-H "Authorization: Bearer $VT_API_KEY"
- webhook is null until you register one. The signing secret is never shown here - only secret_set. If you lost it, rotate it.
- deliveries counts every delivery you have by state. fail_streak is the number of failed attempts in a row; at 20 the webhook is switched off.
- A key with account:read can read the registration and the delivery log without being able to change it.
Response 200 - ok (47 lines)
{
"webhook": {
"url": "https://anatolia-trails.example/hooks/viaturquia",
"events": [
"booking.created",
"booking.confirmed",
"booking.cancelled",
"booking.deposit_paid",
"booking.updated",
"tour.approved",
"tour.unpublished"
],
"is_active": true,
"fail_streak": 0,
"secret_set": true,
"created_at": "2026-10-21T11:00:00+03:00",
"updated_at": "2026-10-21T11:00:00+03:00"
},
"available_events": [
"booking.created",
"booking.confirmed",
"booking.cancelled",
"booking.deposit_paid",
"booking.updated",
"tour.approved",
"tour.unpublished",
"ping"
],
"last_delivery": {
"delivery_id": 311,
"event_id": "evt_4be1f09a77c3d2a05c18",
"event_type": "booking.created",
"status": "delivered",
"attempts": 1,
"max_attempts": 6,
"next_attempt_at": null,
"last_status": 200,
"last_error": null,
"created_at": "2026-10-22T09:12:31+03:00",
"delivered_at": "2026-10-22T09:13:02+03:00"
},
"deliveries": {
"pending": 0,
"delivered": 14,
"failed": 1
}
}
PUT /webhook
Register or change your webhook: the address, the events, on or off.
Scope: webhooks:write
Request
curl -X PUT "https://www.viaturquia.com/api/partner/v1/webhook" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://anatolia-trails.example/hooks/viaturquia","events":["booking.created","booking.confirmed","booking.cancelled"],"is_active":true}'
Request body
{
"url": "https://anatolia-trails.example/hooks/viaturquia",
"events": [
"booking.created",
"booking.confirmed",
"booking.cancelled"
],
"is_active": true
}
- url is required, https only, no user name or password in it, and the host must resolve to a public address (never a private or local network). The address is checked again at every send.
- events is a list of names from available_events. Left out on the first registration it subscribes you to every event except ping; left out later, it keeps what you have. ping is only ever sent when you ask for it with POST /webhook/test.
- The first PUT creates the signing secret and is the only answer, besides rotate_secret, that carries it. Copy it now: it is not shown again.
- A PUT that leaves the webhook active clears the failure streak. After the automatic shut-off, this is how you say "I have fixed my endpoint".
Response 200 - ok - what GET /webhook answers, plus secret on the first registration and ignored (49 lines)
{
"webhook": {
"url": "https://anatolia-trails.example/hooks/viaturquia",
"events": [
"booking.created",
"booking.confirmed",
"booking.cancelled",
"booking.deposit_paid",
"booking.updated",
"tour.approved",
"tour.unpublished"
],
"is_active": true,
"fail_streak": 0,
"secret_set": true,
"created_at": "2026-10-21T11:00:00+03:00",
"updated_at": "2026-10-21T11:00:00+03:00"
},
"available_events": [
"booking.created",
"booking.confirmed",
"booking.cancelled",
"booking.deposit_paid",
"booking.updated",
"tour.approved",
"tour.unpublished",
"ping"
],
"last_delivery": {
"delivery_id": 311,
"event_id": "evt_4be1f09a77c3d2a05c18",
"event_type": "booking.created",
"status": "delivered",
"attempts": 1,
"max_attempts": 6,
"next_attempt_at": null,
"last_status": 200,
"last_error": null,
"created_at": "2026-10-22T09:12:31+03:00",
"delivered_at": "2026-10-22T09:13:02+03:00"
},
"deliveries": {
"pending": 0,
"delivered": 14,
"failed": 1
},
"secret": "whsec_Q7mX2dT9pLk4vN8cR1sZ6bYh3WaE5uJf0GoIxA9tKeD",
"ignored": []
}
422 url_required, invalid_url, unsafe_url, unknown_event, invalid_events
POST /webhook/rotate_secret
Create a new signing secret.
Scope: webhooks:write · honours Idempotency-Key
Request
curl -X POST "https://www.viaturquia.com/api/partner/v1/webhook/rotate_secret" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"
- The old secret stops working the moment this answers. Deploy the new one to your receiver first if you can, because events signed after this call use it.
- Shown once. 404 webhook_not_configured if you have no webhook yet.
Response 200 - ok (49 lines)
{
"webhook": {
"url": "https://anatolia-trails.example/hooks/viaturquia",
"events": [
"booking.created",
"booking.confirmed",
"booking.cancelled",
"booking.deposit_paid",
"booking.updated",
"tour.approved",
"tour.unpublished"
],
"is_active": true,
"fail_streak": 0,
"secret_set": true,
"created_at": "2026-10-21T11:00:00+03:00",
"updated_at": "2026-10-21T11:00:00+03:00"
},
"available_events": [
"booking.created",
"booking.confirmed",
"booking.cancelled",
"booking.deposit_paid",
"booking.updated",
"tour.approved",
"tour.unpublished",
"ping"
],
"last_delivery": {
"delivery_id": 311,
"event_id": "evt_4be1f09a77c3d2a05c18",
"event_type": "booking.created",
"status": "delivered",
"attempts": 1,
"max_attempts": 6,
"next_attempt_at": null,
"last_status": 200,
"last_error": null,
"created_at": "2026-10-22T09:12:31+03:00",
"delivered_at": "2026-10-22T09:13:02+03:00"
},
"deliveries": {
"pending": 0,
"delivered": 14,
"failed": 1
},
"secret": "whsec_n4B8kV2zQxT7pLd1sWm9RhYc6AeU3uJf5GoIxC0tKeH",
"ignored": []
}
404 webhook_not_configured
POST /webhook/test
Queue a ping event to your webhook.
Scope: webhooks:write · honours Idempotency-Key
Request
curl -X POST "https://www.viaturquia.com/api/partner/v1/webhook/test" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"
- Answers 202 Accepted, not 200: the event is queued, not sent. It goes out within a minute; read its outcome from GET /webhook/deliveries.
- This, the test button on your partner panel and /webhook/deliveries/{id}/retry share one allowance of 10 an hour per partner (not per key). A test event counts whichever of the two doors queued it; every POST to the retry path counts whatever the answer, including a 403 for a missing scope and a 429. Over the allowance: 429 with Retry-After.
- 409 webhook_inactive if the webhook is switched off; 404 webhook_not_configured if you have none.
Response 202 - queued
{
"data": [
{
"delivery_id": 312,
"event_id": "evt_c07d6e25b19f8a3340e7",
"event_type": "ping",
"status": "pending",
"attempts": 0,
"max_attempts": 6,
"next_attempt_at": "2026-10-22T10:21:00+03:00",
"last_status": null,
"last_error": null,
"created_at": "2026-10-22T10:21:00+03:00",
"delivered_at": null
}
],
"pagination": {
"page": 1,
"per_page": 1,
"total": 1,
"has_more": false
},
"ignored": []
}
409 webhook_inactive
429 rate_limit_exceeded
GET /webhook/deliveries
Recent deliveries, newest first, with their state and what your endpoint answered. Deliveries are kept for 90 days.
Scope: any one of webhooks:write or account:read
Query parameters
| Name | Type | Meaning |
|---|
page | integer | Page number, from 1. |
per_page | integer | Rows per page. Default 50, at most 100. |
status | string | pending, delivered or failed. |
Request
curl "https://www.viaturquia.com/api/partner/v1/webhook/deliveries?page=1&per_page=50" \
-H "Authorization: Bearer $VT_API_KEY"
- last_error holds the first 500 characters of what your endpoint answered, or the reason nothing did. It is empty on a delivered row.
- attempts counts tries so far out of max_attempts. A pending row carries next_attempt_at.
- Readable with webhooks:write or account:read.
Response 200 - ok (49 lines)
{
"data": [
{
"delivery_id": 312,
"event_id": "evt_c07d6e25b19f8a3340e7",
"event_type": "ping",
"status": "pending",
"attempts": 0,
"max_attempts": 6,
"next_attempt_at": "2026-10-22T10:21:00+03:00",
"last_status": null,
"last_error": null,
"created_at": "2026-10-22T10:21:00+03:00",
"delivered_at": null
},
{
"delivery_id": 311,
"event_id": "evt_4be1f09a77c3d2a05c18",
"event_type": "booking.created",
"status": "delivered",
"attempts": 1,
"max_attempts": 6,
"next_attempt_at": null,
"last_status": 200,
"last_error": null,
"created_at": "2026-10-22T09:12:31+03:00",
"delivered_at": "2026-10-22T09:13:02+03:00"
},
{
"delivery_id": 305,
"event_id": "evt_91d0a3c6e84b7f21a9d3",
"event_type": "booking.confirmed",
"status": "failed",
"attempts": 6,
"max_attempts": 6,
"next_attempt_at": null,
"last_status": 503,
"last_error": "The endpoint answered 503.",
"created_at": "2026-10-21T17:40:10+03:00",
"delivered_at": null
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 3,
"has_more": false
}
}
POST /webhook/deliveries/{id}/retry
Send a failed delivery again.
Scope: webhooks:write · honours Idempotency-Key
Request
curl -X POST "https://www.viaturquia.com/api/partner/v1/webhook/deliveries/305/retry" \
-H "Authorization: Bearer $VT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"
- Acts only on a delivery whose status is failed - one that has given up. A delivered row is 409 already_delivered and a still-queued one 409 delivery_not_failed (it will be tried on its own).
- The schedule starts over (a fresh run of 6 attempts), the event id does not change. Answers 202: it is queued, not sent.
- Shares the 10-an-hour allowance with /webhook/test (see there). Needs an active webhook: 409 webhook_inactive otherwise.
Response 202 - queued
{
"data": [
{
"delivery_id": 305,
"event_id": "evt_91d0a3c6e84b7f21a9d3",
"event_type": "booking.confirmed",
"status": "pending",
"attempts": 0,
"max_attempts": 6,
"next_attempt_at": "2026-10-22T10:25:00+03:00",
"last_status": 503,
"last_error": "The endpoint answered 503.",
"created_at": "2026-10-21T17:40:10+03:00",
"delivered_at": null
}
],
"pagination": {
"page": 1,
"per_page": 1,
"total": 1,
"has_more": false
},
"ignored": []
}
409 delivery_not_failed, already_delivered, webhook_inactive
404 delivery_not_found