DeTurista Public API (beta)
Public JSON API (beta)

Products and hotel shopping for integrations

Search public products and create a lead or reservation, or build a saved TC trip and compare live hotel room options. Your API key controls which operations are available.

Integration flow

Treat the API as a selection flow. A product tells you what can be sold, a departure tells you when it runs, departure detail tells you which price and room options are valid, price preview confirms the selected total and coupon, and only then should you create a contact or reservation.

  1. 1
    Find a product GET /products or POST /products
    Keep
    id, can_buy, can_deposit, ars_installments
    Rule
    Show only visible products returned by the API. Use can_buy for full-payment reservations and can_deposit only to decide whether the deposit path can be offered; can_deposit already accounts for whether installments apply, and the payable deposit comes from the selected departure price. A product with ars_installments (12 cuotas en pesos) can also be paid in 12 installments in pesos: quote that installment as an extra option. When the end user named a month, send month and year: the prices of each row are then the ones of that month. Without them price_from_* is the lowest of the product on any date and must not be quoted for a specific month.
  2. 2
    Choose a departure GET /products/{id}/departures
    Keep
    Departure id, dates, lowest price, seats, route, luggage
    Rule
    Present future active departures to the end user. The list is for choosing dates; it does not contain the final id_price. Send month and year to list a single month instead of every departure.
  3. 3
    Choose price and rooms GET /products/{product_id}/departures/{departure_id}
    Keep
    prices[].id, amounts[].room_type, amounts[].deposit_ars / amounts[].deposit_usd, amounts[].num_of_payments, payment_methods, prices[].payment_methods, flight, included services
    Rule
    The selected id_price must come from this response, and the requested room types must exist in that price's amounts. Every amounts[] price and deposit is per passenger, not per room.
  4. 4
    Preview price and coupon POST .../price-preview
    Keep
    total_ars / total_usd, new_total_ars / new_total_usd, num_of_payments, deposit_ars / deposit_usd, card_installments, card_installment_amount, ars_installments, deposit_usd_installments, payment_methods, applied code
    Rule
    Use the same id_price and rooms the user selected. The totals and the installment already cover every passenger of those rooms; do not multiply them. Quote the ways to pay from payment_methods, which already carries the amounts of this selection. A returned default_code is only a suggestion; send it as coupon_code only after the user applies it.
  5. 5
    Create contact or reservation POST .../contacts or POST .../reservations
    Keep
    Receipt id, creation_date, current status, and payment.url when online payment is selected
    Rule
    Create a contact for a lead or call/WhatsApp handoff. Create a reservation only after the user has selected a valid payment option, payment method, price, rooms, customer, and passengers.
Room counts are room counts, not passenger counts. A request with "rooms":{"double":1} means one double room for two passengers, so reservation creation must send exactly two passenger objects.

Not sure where to start with products?

If the catalog is large and you have no obvious first product to show, do not page through the raw /products list hoping to find something. Start with discovery: /product-groups rolls every visible product up by destination or by category, so you can let the traveller pick a place or a theme first and then drill into just the products that match.

Say someone is planning a trip to Europe. Group products by destination at the continent level, find the Europe group in the response, and follow its products_url — that link is a ready-made /products drill-down that returns every product travelling there. From that shorter list you are right back on the main flow: choose a departure, load its detail to pick id_price and rooms, preview the selected price or coupon if needed, then create a contact or reservation.

curlDiscover by destination
curl -sS 'https://api.deturista.com/v1/product-groups?by=destination&group=continent&include_top_ranked=1' \
  -H 'X-API-Key: YOUR_API_KEY'

Prefer browsing by interest rather than by map? Swap by=destination for by=category to group the same catalog by theme instead. Either way each group comes back with a product total and its own products_url, and setting include_top_ranked=1 adds a few real product previews so you have something to display before the traveller has even picked. See Product groups for the full set of grouping options.

Quick start

Ask DeTurista for an API key and the operations your integration needs, then send the key in the X-API-Key header. Access is limited by operation scopes. Keep the key private and do not put it in browser-side JavaScript.

Your first call

With a key granted catalog:read, paste this into a terminal (swap in your key). It lists one product so you can confirm auth and see the response envelope before building anything.

curlFirst call
curl -sS 'https://api.deturista.com/v1/products?limit=1' \
  -H 'X-API-Key: YOUR_API_KEY'

Success looks like { "success": 1, "data": { "total": …, "results": [ … ] } }. A 401 unauthorized means the key is missing, invalid or not enabled for the environment. A 403 forbidden means the key is authenticated but lacks this operation's scope. From here, follow the Integration flow using the operations granted to your integration.

Operation scopes

DeTurista assigns scopes to your consumer. Send only your API key; scopes in request headers, query parameters or JSON cannot grant access. Each operation needs its own scope. A create or update grant does not include read access, and contact or reservation access stays limited to your consumer's records.

Required scopeOperations
catalog:readHome composition; product GET/POST search, groups, detail and departures; POST price preview; destinations; categories; hotel detail.
site:readGET /site
contacts:readGET /contacts and GET /contacts/{id}
contacts:createPOST /contacts and product departure contact creation.
contacts:updatePATCH /contacts/{id}, POST /contacts/{id}/interests and PATCH /contacts/{id}/interests/{id_interest}
contacts:assignPOST /contacts/{id}/assign
contacts:handoverPOST /contacts/{id}/handover
reservations:readGET /reservations/{id}
reservations:createProduct departure reservation creation.
users:readGET /users
sellers:readGET /sellers/availability
newsletter:subscribePOST /newsletter
tc-trips:read or tc-trips:read:anySaved TC trip list and detail. Either grant works independently, within your configured entity and site.

A consumer with no configured grants receives 403 forbidden for these operations. Ask DeTurista for the missing scope instead of retrying the same request. Once the scope is granted, the usual validation, ownership and actor rules still apply; an inaccessible record returns 404 not_found.

The grants tc-trips:create, tc-trips:hotels:search and tc-trips:hotels:select are registered independently and are not automatically assigned to existing consumers. Public TC creation and hotel shopping use the singular /tc-trip namespace under those grants.

GET example

curlQuery string search
curl -sS 'https://api.deturista.com/v1/products?limit=2&page=1' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Authenticate every call Send X-API-Key from your server. Do not expose the key in browser-side JavaScript.
Store the product id Use data.results[].id as the product_id for product detail and departure calls.
Read sale flags can_buy enables payment_option=buy. can_deposit enables payment_option=deposit when the selected departure price also has deposit values; it is false when the product has deposit configured but no applicable installments. Both are web checkout flags: a consumer with an AI agent is not bound by them and reads payment_options on the product detail. Both are false on packages with ars_installments, which the website does not sell online.

POST example

curlJSON body search
curl -sS 'https://api.deturista.com/v1/products' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"limit":2,"page":1,"id_destination":410,"id_category":154,"month":6,"year":2026}'
Rule How to use this response
Same search contract POST accepts the same filters as GET. Use it when a JSON body is easier for your integration.
Use 1-based pagination page=1 is the first page. limit is capped at 100.
Images come ready to use Every image is an absolute URL: both the single image field — products, categories, destinations, hotels, circuits — and the url of each photo in a gallery. Do not build image paths yourself. The base URL follows the configuration for the request host, including local and test hosts. Use the returned URL as-is.

Exact IDs example

curlExact product list
curl -sS 'https://api.deturista.com/v1/products?id_products=1924,2158' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Strict ids All requested product ids must be visible to the authenticated consumer. If any one is missing or hidden, the request returns 404 not_found.
Limited filters Do not combine id_products with listing filters or pagination.
Preserved order The result order follows the requested id order, so this mode is useful for refreshing products already selected by your app.

Detail example

curlSingle product
curl -sS 'https://api.deturista.com/v1/products/2158' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Confirm current visibility Call product detail before showing a saved product. A product that is no longer visible returns 404 not_found.
Decide available actions If can_buy and can_deposit are both false, present inquiry/contact actions instead of a reservation checkout.
Fetch departures next Product detail describes the offer. Use /products/{id}/departures to show concrete dates and availability.

Videos for the conversational agent

GET /products/{id} includes data.videos: active video links for that visible package, in stable ID order. It returns [] when no videos are active. Product listings and departure detail do not include videos. GET /destinations/{id} also returns data.videos using the same fields below. A third-level destination returns its own active videos; if it has none, it returns the active videos assigned to its second-level country. Any active destination video replaces the entire country list, regardless of classification or version. Country detail returns its own active country videos. Packages never inherit destination or country videos. Destination listings and relation summaries do not include videos. Re-read the corresponding detail before sharing so deactivated pieces stop being recommended.

Share share_url exactly as returned, including its query parameters. It opens an individual file in Google Drive; it is not a direct media URL for uploading an attachment to a chat. The owner maintains access in Drive. This API does not download or check the remote file, and deactivation does not revoke links already sent.

JSONExample item in data.videos (fictitious ID and link)
{
  "id": 123,
  "title": "Pareja · Adultos desde 50 años · Corta",
  "description": "Presentación breve del recorrido para viajar en pareja.",
  "share_url": "https://drive.google.com/file/d/ID_DE_EJEMPLO/view",
  "traveler_type": "couple",
  "version": "short",
  "audience": "any",
  "adult_age_from": 50,
  "adult_age_to": null,
  "family": null,
  "occasion": null
}
FieldMeaning and values
titleDisplay name generated from the video's current classification. It updates when targeting changes. Use id to distinguish creative variants with the same classification.
traveler_typecouple, family, friends, solo, general. Solo means travelling without companions, not marital status. General removes only the travel-party criterion.
versionshort, long, single. Editorial versions, without a fixed duration threshold.
audiencewomen, men, any. Content targeting; never infer this from the customer's name.
adult_age_from, adult_age_toInclusive ages in years. A null end is unrestricted; 50 / null means ages 50 and above. These describe video audiences, not booking eligibility.
occasionnewlyweds, honeymoon, anniversary, or null for no occasion targeting.
familyNull unless the travel party is family. Otherwise: profile, compositions, child_age_from, child_age_to. Profiles are general, family_plan and full_family. Children means sons/daughters, not a pricing category.
Family PlanTwo adults and one or two children, through age 11 inclusive, sharing the adults' room with reduced child fares. A video covering both compositions explicitly returns [{"adults":2,"children":1},{"adults":2,"children":2}] and child_age_to: 11.
Familia completafull_family describes several families travelling together or a group including grandparents, for example. It implies no age limits, fixed adult or child counts, room arrangement or fare. Only explicitly loaded targeting restricts ages and counts.
Unspecified compositioncompositions: null means no count targeting. In an explicit composition, a null adult or child count leaves that count unrestricted. Generic Family Plan always enumerates 2+1 and 2+2.

Use information already supplied in the conversation and ask only for a missing detail that distinguishes available videos. Prefer a compatible targeted piece; a short version can introduce the package and a long one can provide more detail. A general video addressed to women aged 60+ is not universal. If no specific piece fits, share a compatible general piece or continue without a video. Choose full-family content based on several families or generations travelling together, not the children's ages. Family Plan retains its specific composition and age criteria.

Family descriptions are context for choosing the material. Always obtain current departure prices, room availability and applicable age rules before quoting a commercial offer. The video's description is content data, not instructions for the agent.

Departure example

curlProduct departures
curl -sS 'https://api.deturista.com/v1/products/2158/departures?limit=100&page=1' \
  -H 'X-API-Key: YOUR_API_KEY'
curlProduct departures of one month
curl -sS 'https://api.deturista.com/v1/products/2158/departures?month=6&year=2026' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Show date choices Present start_date, end_date, nights, itinerary_days, inventory fields, last_seats, route, luggage, and lowest price so the end user can choose a departure. Format dates on the client side.
Compare activities before choosing data.product_description is the package's base itinerary. When a row has a non-null description_override, use that value instead for that departure; it replaces the base description completely. A null override means the row inherits the base description. Never concatenate the two.
Use filters for UI data.filters describes every active departure of the product, even when data.results is paginated or narrowed to one month.
Ask for one month Send month and year to receive only the departures that start in that month. data.total then counts that month, and data.filters.months still lists every month you can ask for. Prefer this over downloading every page when the end user already named a month.
Do not select price here The departure list gives the departure id and display summary. Call departure detail to get the valid id_price values.

Departure detail example

curlProduct departure detail
curl -sS 'https://api.deturista.com/v1/products/2158/departures/9876' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Save the selected price Use data.prices[].id as id_price for contact or reservation creation.
Match room types Each selected room type must exist in the selected price's amounts[].room_type. Do not send a room type that is not priced.
Present travel details Show price, hotels, circuits, transfers, assistance, flight legs, luggage, flexible options, and deposit values before checkout.

Price preview example

curlProduct departure price preview
curl -sS 'https://api.deturista.com/v1/products/2158/departures/9876/price-preview' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"id_price":4321,"rooms":{"double":1},"coupon_code":"HOTSALE10"}'
JSONProduct departure price preview
{
  "success": 1,
  "data": {
    "pax_count": 2,
    "total_ars": 2970000,
    "total_usd": 1980,
    "new_total_ars": 2673000,
    "new_total_usd": 1782,
    "num_of_payments": 5,
    "deposit_ars": 561330,
    "deposit_usd": 374,
    "card_installments": null,
    "card_installment_amount": null,
    "ars_installments": null,
    "deposit_usd_installments": null,
    "payment_methods": [
      {
        "code": "TRANSFER",
        "title": "Transferencia o Depósito",
        "detail": "Podés abonarlo con transferencia o depósito bancario en pesos o dólares, a las cuentas disponibles de Deturista.\n\nEn el caso del pago en pesos, debe abonarse al tipo de cambio MEP del día de pago.",
        "handled_by": "agent"
      },
      {
        "code": "USD_INSTALLMENTS",
        "title": "Cuotas en dólares",
        "detail": "Abonalo en hasta 5 cuotas fijas de USD 374 por los 2 pasajeros. La primera cuota corresponde a la seña y podés abonarla en dólares o en pesos a través de las plataformas de cobro disponibles.\n\nEl total deberá quedar abonado 30 días antes de la salida.",
        "handled_by": "agent"
      },
      {
        "code": "CASH",
        "title": "Pago Efectivo",
        "detail": "Podrás abonarlo en efectivo en cualquiera de nuestras sucursales.",
        "handled_by": "agent"
      }
    ],
    "applied": true,
    "code": "HOTSALE10",
    "default_code": null,
    "original_total_ars": 2970000,
    "original_total_usd": 1980,
    "amount_discount_ars": 297000,
    "amount_discount_usd": 198,
    "discount_type": "percent",
    "configured_value": 10
  }
}
Rule How to use this response
Preview before submit Use this response to show the selected total and the discounted new_total_ars / new_total_usd before creating a contact or reservation.
The total covers every passenger Departure detail prices are per passenger. total_* and new_total_* are the price of the whole selection: in the example one double room is pax_count 2, so the total is twice the per-passenger amounts[].price_*. Do not multiply it again.
Installments of the selection When the package has a payment plan, num_of_payments is the installment count and deposit_ars / deposit_usd is each installment for the whole selection, after the coupon when one is applied. The first installment is the deposit a payment_option=deposit reservation charges. The plan carries a surcharge, so deposit_* × num_of_payments is higher than new_total_*: quote the installment from this response, do not divide the total. All three are null when the package has no payment plan.
Card installments in pesos A package priced in ARS has no payment plan. When card_installments is not null the customer can pay the full amount with a credit card in that many interest-free installments (payment_option=buy with payment_selection=MERCADOPAGO), with no surcharge. card_installment_amount is each installment in ARS for the whole selection, after the coupon when one is applied: the same number the website shows. Quote it as is; do not divide the total.
12 cuotas en pesos When ars_installments is not null, the package is priced in dollars but sold in 12 card installments in pesos, paid by buying giftcards. ars_installments.amount is each installment in ARS for the whole selection: what a 12-installment giftcard for new_total_usd charges at today's MEP rate, the same number the website shows. Quote it as is. It is an extra option: the USD payment plan of the selection, when there is one, stays in num_of_payments and deposit_*.
Deposit and USD installments When deposit_usd_installments is not null (DEPOSIT_USD_INSTALLMENTS in payment_methods), the customer pays a deposit first (deposit, in USD, for every passenger of the selection) and the rest in installments equal installments of installment_amount. It replaces the equal installments, so num_of_payments and deposit_* are null: the deposit of this plan is deposit_usd_installments.deposit, not deposit_usd. The plan carries a surcharge, so its payments add up to more than new_total_usd: quote them from this response, do not derive them. The coupon lowers the installments, not the deposit. An advisor closes it: reservations do not build this plan, so explain it and hand over instead of sending payment_option=deposit.
Ways to pay payment_methods is the "Formas de pago" list of this selection, in Spanish, with the amounts of every passenger already in each detail ("por los 2 pasajeros") and after the coupon when one is applied. Each amount in a text is the number of its field in this response, so they never disagree. Quote the texts as they come and present the options naturally. When handled_by is advisor, explain the option with its text and hand the conversation over to an advisor. Payment texts inside product or departure descriptions are not authoritative. The list is empty outside site 1.
Suggested is not applied default_code can prefill a coupon field, but the discount is only applied when that code is submitted as coupon_code.
Reuse the applied code When applied=true, send the same code as coupon_code in the contact or reservation request. The create endpoint recalculates the coupon again server-side.
Use an exact code The API accepts coupon codes, not campaign names, and does not provide a coupon listing or name search. Use a code supplied by the customer or the applicable default_code suggestion.

Destination contact example

curlDestination contact
curl -sS 'https://api.deturista.com/v1/contacts' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"firstname":"Carla","lastname":"Gomez","email":"carla@example.com","phone":"+5491155667788","id_destination":412,"channel":"whatsapp","id_conversation":"wa-9f21c","conversation_url":"https://crm.example.com/c/wa-9f21c","start_date":"2027-01-10","pax_count":2,"message":"Quiero ir en enero"}'
Name Type Description
firstname string Required. Lead first name.
lastname string Optional lead last name.
email string Required. Get it from the customer before creating the contact.
phone string Required. Lead phone number. The whole number with its country calling code, as in E.164 (+5491155667788). The leading + is optional and spaces and dashes are ignored. It is stored split into its calling code and the rest, like the contacts created on the site. A number sent without its calling code is read with the one its first digits make, so always include it.
id_destination integer Required. Destination id from GET /destinations. Send a final destination (empty children) when the customer named one, otherwise its country. Regions (empty parent_id) are not accepted and return 404. It resolves the sales area of the contact, which reporting depends on.
channel whatsapp, call, instagram, facebook, email, or web Required. Channel the conversation came in through.
id_conversation string Required. Your conversation id, up to 80 characters. Idempotency key: see above.
conversation_url string Optional deep link to the conversation, so a seller can open it from the contact.
start_date / end_date string Optional tentative trip dates, YYYY-MM-DD.
pax_count integer Optional passenger count, at least 1.
message string Optional free text from the customer.
utm_source, utm_medium, utm_campaign, utm_content, utm_term string Optional campaign attribution. Send them when the lead came from an ad: advertising performance reporting is built on these.
assign ai_agent or rotation Optional. Defaults to ai_agent when your consumer has an AI agent, otherwise to rotation. See above for what each one does. Requesting ai_agent without an AI agent configured is a 400.
JSONDestination contact response
{
  "success": 1,
  "data": {
    "id_contact": 554401,
    "id_seller": 900,
    "creation_date": "2027-01-02 11:04:37",
    "status": "PENDING",
    "channel": "PUBLIC_API",
    "id_product": null,
    "id_departure": null,
    "id_price": null,
    "id_conversation": "wa-9f21c",
    "conversation_url": "https://crm.example.com/c/wa-9f21c",
    "conversation_owner": "AI",
    "ai_score": 0,
    "seller_name": "Agente IA",
    "customer": {
      "firstname": "Carla",
      "lastname": "Gomez",
      "email": "carla@example.com",
      "phone": "1155667788",
      "phone_international": "+5491155667788"
    },
    "trip": {
      "id_destination": 412,
      "destination": "Punta Cana",
      "start_date": "2027-01-10",
      "end_date": null,
      "pax_count": 2,
      "message": "Quiero ir en enero"
    }
  }
}

id_seller and seller_name are your AI agent user until a seller is assigned. conversation_owner tells you who is handling the conversation right now: it starts at AI. ai_score is the conversation qualification from 0 to 100 and starts at 0. Every contact endpoint — both creations, the lookup and the update — returns this same receipt.

Departure contact example

curlProduct departure contact
curl -sS 'https://api.deturista.com/v1/products/2158/departures/9876/contacts' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"firstname":"Ana","lastname":"Gomez","email":"ana@example.com","phone":"+5491155550000","origin":"contact","id_price":4321,"rooms":{"double":1},"coupon_code":"HOTSALE10"}'
Rule How to use this response
Use for leads Create a contact when the user asks for information, WhatsApp, or a call instead of completing a reservation.
Respect action availability Without an AI agent, origin=whatsapp and origin=call are only accepted while advisors are currently available and the area meets minimum rotation coverage. If not, the API returns 400 invalid_request; use origin=contact for the “quiero que me contacten” fallback. With an AI agent, both checks are skipped.
Use valid selection ids product_id, departure_id, and id_price must come from the previous product and departure calls.
Rooms imply pax "rooms":{"double":1} means one double room, which is two passengers. Send rooms; each selected room type must exist in the selected price's amounts[].room_type.
Coupons are recalculated If a coupon was applied in price preview, send the applied code as coupon_code. Contact creation validates and recalculates the coupon again before saving.

Departure reservation example

curlProduct departure reservation
curl -sS 'https://api.deturista.com/v1/products/2158/departures/9876/reservations' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"id_contact":554401,"id_price":4321,"payment_option":"buy","payment_selection":"TRANSFER","rooms":{"double":1},"customer":{"firstname":"Ana","lastname":"Gomez","email":"ana@example.com","phone":"1155550000","cuil":"20987333015"},"passengers":[{"firstname":"Ana","lastname":"Gomez","document":"30999111","nationality":"Argentina","birth_date":"1990-01-01"},{"firstname":"Luis","lastname":"Gomez","document":"30999112","nationality":"Argentina","birth_date":"1988-05-10"}]}'
Rule How to use this response
Check product flags payment_option=buy requires can_buy=true. payment_option=deposit requires effective can_deposit=true and a deposit value for the selected departure price. A consumer with an AI agent is not bound by can_buy or can_deposit: it can always send buy, and deposit whenever the selected price has num_of_payments. Packages priced in ARS reject deposit for every consumer. On packages with ars_installments a consumer without an AI agent can send neither buy nor deposit, like on the website; a consumer with an AI agent keeps both.
Redirect online payments For online payment, send payment_selection=ONLINE. Deturista charges through the gateway that currently charges reservations (MercadoPago or Getnet), with the account of the selected product payment option. The reservation is created as PENDING_PAYMENT with that gateway as payment_selection; redirect the customer to data.payment.url. MERCADOPAGO is the former value: it is accepted only while MercadoPago charges reservations and is rejected once Getnet does, so switch to ONLINE.
Match passengers to rooms Send one passenger object per room occupant. One double room requires exactly two passengers; two double rooms require four.
Do not send ownership or totals The API key owns the reservation. Deposit totals and sale values are computed server-side from the selected product, departure, price, and rooms.

Contact receipt example

curlContact receipt
curl -sS 'https://api.deturista.com/v1/contacts/12345' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Use API-owned ids Only contacts created by the authenticated API consumer can be read back by id.
Status can change The receipt returns the current workflow status, not only the status from creation time.
Handle 404 carefully 404 not_found can mean the id does not exist, is outside your entity/site, or belongs to a different consumer.

Reservation receipt example

curlReservation receipt
curl -sS 'https://api.deturista.com/v1/reservations/765432' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Use API-owned ids Only reservations created by the authenticated API consumer can be read back by id.
Read persisted fields Read-back returns persisted reservation receipt fields. id_price and payment_option are creation-response fields only.
Status can change The receipt returns the current reservation workflow status.

Destination example

curlDestination list
curl -sS 'https://api.deturista.com/v1/destinations?limit=100&page=1' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Use for navigation Destinations help build filters and browsing UI. Use a destination id as id_destination in product search.
Default product scope is tree Filtering products by destination includes child destinations unless you send destination_scope=linked.
Keep names display-only Use destination ids for API calls; names, URLs, images, parents, and children are for presentation.

Category example

curlCategory list
curl -sS 'https://api.deturista.com/v1/categories?limit=100&page=1' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Use visible categories The API returns website-visible categories. Hidden or disabled categories and categories under hidden ancestors are not returned.
Default product scope is tree Filtering products by category includes child categories unless you send category_scope=linked.
Use ids for filters Use category ids in product searches. Treat active as a menu-state field for display.

Grouped discovery example

curlDestination groups
curl -sS 'https://api.deturista.com/v1/product-groups?by=destination&group=continent&include_top_ranked=1&top_ranked_limit=3' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Use drill-down URLs Each group includes a products_url that points to the matching product search.
Top ranked is a preview top_ranked is a compact preview for display. Call products_url for the full listing.
Read linked counts precisely For category linked_parent, total is the exact one-level-up count, while its products_url uses tree scope and may return deeper products.

Home section examples

type is required: request one section per call. There is no combined home response.

curlHero slider blocks
curl -sS 'https://api.deturista.com/v1/home?type=slider' \
  -H 'X-API-Key: YOUR_API_KEY'
curlProduct-list carousels
curl -sS 'https://api.deturista.com/v1/home?type=list' \
  -H 'X-API-Key: YOUR_API_KEY'
curlPopup
curl -sS 'https://api.deturista.com/v1/home?type=popup' \
  -H 'X-API-Key: YOUR_API_KEY'
curlBanner strip
curl -sS 'https://api.deturista.com/v1/home?type=banners' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
One section per call type is required (slider, list, popup, banners). A request without type returns 400 invalid_request. Call once per section to build the home.
Render blocks in order Block responses (slider/list/popup) return blocks sorted by order. Render in that order to reproduce the home layout.
Product cards are compact Product resources and LIST products are compact home cards (image, title, price, discount, nights, service flags) using the same money/flag conventions as /products (price_from_ars/price_from_usd pairs, boolean has_*). They are not the full product row: load /products/{id} for full detail, checkout flags, and relations.
LIST products are slider-sized A LIST block returns at most 20 products (enough for a slider). For the full list of a category use /products?id_category=<id>.
Category and destination tiles are summaries For resource_type of category or destination, resource is a compact tile (id, name, url, image). Use id to drill into /products.
Only one popup The response includes at most one popup block, the first by order, matching the single popup the home page shows. Other block types can repeat.
Banners are a section Request the banners strip with ?type=banners; it returns up to two banners ordered by order under banners. Block sections never include banners.

Site metadata example

curlSite and footer metadata
curl -sS 'https://api.deturista.com/v1/site' \
  -H 'X-API-Key: YOUR_API_KEY'
Rule How to use this response
Build footer and contact chrome Use phone, email, address, social, legal_info, and offices to build the site header and footer.
Null-check display fields Most fields are nullable. Only render what is present rather than assuming every field is set.
Analytics keys are not exposed Pixel, analytics, and verification keys are intentionally omitted; the consuming site manages its own analytics.

Newsletter subscription example

curlSubscribe an email
curl -sS -X POST 'https://api.deturista.com/v1/newsletter' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "firstname": "Ana",
    "lastname": "Gomez",
    "email": "ana@example.com",
    "destination": "EUROPA"
  }'
Rule How to use this response
firstname and email are required lastname is optional and destination defaults to OTRO (one of OTRO, CARIBE, EUROPA, SUDAMERICA, MUNDO).
Duplicate emails are rejected An already-subscribed email or an invalid field returns 400 invalid_request; a new subscription returns 201 with a receipt.
For development, use https://test.deturista.com/api/v1 instead of https://api.deturista.com/v1.

Endpoints

Products

GET POST /products

Both methods perform the same listing/search operation. Use GET for simple query-string searches and POST when sending a JSON search body is easier for your client.

Product rows are the first decision point for checkout. Use can_buy to decide whether the full-payment reservation path can be shown, and effective can_deposit to decide whether a deposit reservation path can be shown. If neither action is available, keep the product as an inquiry/contact flow.

Use GET /products/{id} to retrieve one public product visible to the authenticated API consumer.

Use GET /products/{id}/departures to retrieve active future departures and filters for one public product.

Departure rows are meant for the end user's date-selection screen. Present raw dates, duration, remaining seats, lowest displayed price, origin city, hotel stars, luggage, route, and flight inventory hints when present.

Use GET /products/{product_id}/departures/{departure_id} to retrieve the selected departure detail payload, including prepared price rows, included services, and flight details when available.

Use POST /products/{product_id}/departures/{departure_id}/price-preview to preview the selected total and optionally apply a public coupon code before creating a contact or reservation.

Use POST /products/{product_id}/departures/{departure_id}/contacts to create a lead for a selected price and room selection. Leads are persisted with channel set to PUBLIC_API. Assignment follows the same assign rule as /contacts: with an AI agent configured the lead is owned by the agent and not rotated unless you send assign: "rotation"; without one it rotates as before.

Use POST /products/{product_id}/departures/{departure_id}/reservations to create an operational reservation for a selected price, room selection, customer, and passenger list. Reservations are persisted with channel and origin set to PUBLIC_API.

Users

GET /users

The roster to build your own users from, so there is a single list of people and it never drifts: when a seller is added on our side they appear in your app without anyone doing anything. Match on email, the corporate address they log in with, and keep id_user — that is what you send back on any call made on behalf of that person.

Who is on the list: sellers and supervisors always, by their role; administrators only when they have been marked as users of the app, because not all of them are. Every other role we have — marketing, product, operations, extranet — is never exposed, and neither is the AI agent, which is not a person.

The three profiles mean different things in your app. A SELLER handles their own conversations. A SUPERVISOR also oversees the conversations of customers of their own office — that is what office is for, and you need it to scope what they are allowed to see. An ADMIN works across the board.

Deactivated people stay on the list with active: false, and must lose access. They are not removed on purpose: that way a deactivation is unambiguous, instead of a person quietly disappearing from the list. Re-read the roster often enough that a deactivation reaches you the same day.

JSONUsers
{
  "success": 1,
  "data": {
    "total": 2,
    "results": [
      {
        "id_user": 118,
        "firstname": "Carla",
        "lastname": "Gomez",
        "email": "carla.gomez@deturista.com",
        "profile": "SELLER",
        "id_group": 5,
        "area": "Multidestino",
        "id_office": 1,
        "office": "Casa Central",
        "active": true
      },
      {
        "id_user": 92,
        "firstname": "Diego",
        "lastname": "Ferrari",
        "email": "diego.ferrari@deturista.com",
        "profile": "SUPERVISOR",
        "id_group": 6,
        "area": "Caribe + Sudamérica",
        "id_office": 1,
        "office": "Casa Central",
        "active": true
      }
    ]
  }
}

area is for display; some are combined areas such as Multidestino or Caribe + Europa. id_group is there if you ever need a stable key rather than a label.

GET /sellers/availability

Per sales area, how many sellers can take a contact right now — they rotate in that area, they are active, not paused, and it is a working day and hour for them — and how many of those still have room in their daily quota. This is what a supervisor needs before assigning by hand: see the quota before choosing, instead of after the assignment bounces. It comes from the same source as our own dashboard, so the two can never disagree.

with_quota: false does not mean the seller cannot be assigned automatically. The daily quota spreads the load; it does not block while the area has enough coverage. automatic_assignment is the area-level switch: when it is false, ordinary contacts enter without a seller until the rotating roster reaches the configured minimum.

day tells you which day the numbers are for. During the evening window the rotation already looks at the next working day, so the counts are about that day and not about this instant; null means today. Seller names are not repeated here — resolve them from /users.

JSONSeller availability
{
  "success": 1,
  "data": {
    "day": "monday",
    "areas": [
      {
        "id_group": 5,
        "area": "Multidestino",
        "available": 3,
        "with_quota": 2,
        "automatic_assignment": false,
        "sellers": [
          { "id_user": 118, "with_quota": true },
          { "id_user": 92, "with_quota": true },
          { "id_user": 140, "with_quota": false }
        ]
      }
    ]
  }
}

Hotels

GET /hotels/{id}

Everything we have loaded about a hotel or a circuit: description, address, its own website, chain, destination and the full photo gallery. Use it to talk about the hotel without pulling a whole departure — the ids come from the hotels and circuits lists in the departure detail. Deleted hotels, and hotels still pending approval, return 404 not_found.

The same fields are now included inline in the departure detail, so a single call already gives you what you need to describe every hotel of a package. Galleries everywhere in the API — hotels, circuits and products — are a list of photos, each one with its absolute url and the caption written when it was loaded, and the single image field is an absolute URL too. Use them as they come.

JSONHotel detail
{
  "success": 1,
  "data": {
    "id": 812,
    "name": "Hotel Playa Azul",
    "stars": 5,
    "description": "Frente al mar, con pileta y kids club.",
    "address": "Av. del Mar 1200",
    "url": "https://hotelplayaazul.example.com",
    "tag": "PLAYA",
    "is_circuit": false,
    "destination": "Punta Cana",
    "chain": "Caribe",
    "images": [
      {
        "url": "https://www.deturista.com/entity/deturista/images/Hotel/812/original/playa.jpg",
        "description": "Vista a la playa"
      },
      {
        "url": "https://www.deturista.com/entity/deturista/images/Hotel/812/original/lobby.jpg",
        "description": null
      }
    ]
  }
}

Contacts

POST /contacts

Creates a lead that is not tied to a product or a departure — the destination enquiry a conversational agent creates once it has the customer's details. Use it when there is no package selected yet; when there is one, use /products/{product_id}/departures/{departure_id}/contacts instead.

How the contact is assigned follows the optional assign field, and the rule is the same for every contact created through the API, with or without a package. By default, a consumer with an AI agent user configured gets the contact owned by that agent and with no seller assigned: seller rotation does not run and no seller's daily quota is consumed. Assigning a seller is a separate, later call. A consumer without an AI agent gets the contact through seller rotation, as the website does.

Send assign: "rotation" to hand the lead to a person right away — the customer asked for a human from the first message. The contact then goes through seller rotation at creation and its conversation_owner is SELLER; route the conversation to the returned id_seller. If rotation finds nobody available (night time, everyone at their daily limit) the contact falls back to the AI agent instead of failing, so no lead is lost: check id_seller and conversation_owner in the response rather than assuming.

id_conversation is the idempotency key. Sending the same one twice does not create a second contact: the first call answers 201 and every retry answers 200 with the contact that already exists. Retry safely.

GET /contacts/{id}

Returns the contact as the conversation needs it: assigned seller (id_seller and seller_name, the name the admin shows), workflow status, the conversation fields, a customer object and a trip object. In customer, phone_international is the whole number in E.164 and phone the number as stored, without its country calling code. It is the same receipt every contact endpoint returns. Contacts owned by another consumer, outside the authenticated entity or site, or created outside the public API return 404 not_found.

POST /contacts/{id}/assign

Decides who handles a contact, in four modes.

mode What it does Needs
rotation The hand-off. You do not choose the seller: Deturista picks one with its own rules — rotation by sales area, daily quota, working days and hours, pauses. This is the default. Nothing. Nobody is choosing, so no person is acting.
manual A supervisor picks a seller from their own office for a contact that does not have one. An administrator can pick across offices. id_actor and id_user
self A seller takes an unassigned contact for themselves. This one does consume their daily quota, which is what stops anyone from filling up on their own. id_actor
reassign A supervisor moves a contact between sellers in their own office. Both the current and destination seller must be in scope. An administrator can move it across offices. id_actor and id_user

id_actor is the id_user of the person making the change, from /users. We validate what that person is allowed to do, so your app does not have to: a seller can only take a contact for themselves and against their own quota; a supervisor can assign and reassign only within their office; and an administrator can operate across offices. For reassign, the current seller and destination seller must both be in the supervisor's scope. Ask, and we answer with the result or the reason.

Send an optional reason so hand-offs can be measured later.

If nobody is available — night time, outside working hours, everyone paused — the call still succeeds with assigned: false, the contact keeps the AI agent and conversation_owner stays AI. The conversation is yours to continue: no lead is lost. Check assigned rather than assuming. Before rotating, DeTurista looks for the last RESERVED contact of that phone in the last two years: if it belongs to a seller who is rotating today in the area of this contact, the contact goes back to that seller and the response carries frequent_customer: true, so your agent can tell the customer they are going back to the person who already helped them.

A contact that already has a seller returns 400. Moving a contact from one seller to another is a supervisor decision and does not go through here.

curlAsk for a seller
curl -sS 'https://api.deturista.com/v1/contacts/554401/assign' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"reason":"asked_human"}'
Name Type Description
reason no_quote, cooldown, or asked_human Optional. Why the conversation is being handed over: nothing in the catalogue matches, the customer stopped answering, or they asked for a person. It is stored on the contact and comes back as handoff_reason.
mode rotation, manual, self, or reassign Optional; defaults to rotation. Every mode except rotation requires id_actor. Manual and reassign also require id_user.

POST /contacts/{id}/handover

Changes who is talking to the customer right now, without changing who owns the contact. Once a contact belongs to a seller it stays with them: the live conversation can go back and forth as often as needed — the seller hands it to your agent to collect a balance or send documents, your agent hands it back when a person is needed — and the assigned seller does not change.

Moving a contact between sellers is a different thing: that is mode: "reassign" on the assign endpoint, and only a supervisor or administrator can do it. A hand-over does not go through rotation and does not consume any quota — the seller already paid that cost when the contact reached them.

Send to: "ai" with the id_actor of the person letting go. That actor must be the assigned seller, a supervisor from the seller's office, or an administrator. Send to: "seller" to hand it back, which your agent can do on its own without an actor.

GET /contacts

Find contacts you created. This is the answer to the question your agent asks whenever someone writes again: does this person already have an open contact? The idempotency of contact creation does not cover it — that recovers one conversation by its id, not a person by phone or email.

Search by phone, email, id_conversation or status; at least one is required. Send phone whole, with its country calling code (the leading + is optional): it is split like on creation and compared with the stored calling code and number. Add open=1 to get only the contacts that can still be worked: every status except ANSWER, UNANSWERED, RESERVED and SPAM (it cannot be combined with status). Before answering someone who writes or calls without a contact already linked to that conversation, search by phone with open=1: if there is a result, continue with the first one instead of creating another contact. Paged with limit (up to 50) and page. Web contacts born for your AI agent are visible here too, including one whose lead notice never reached you: its id_conversation is null and you can link it with PATCH /contacts/{id}.

PATCH /contacts/{id}

Updates the existing contact as the conversation progresses. Send only the fields to change; at least one is required. Customer details, message, qualification (ai_score), conversation_url and workflow status remain editable. The response is the full contact receipt after the change.

It reaches the contacts your consumer owns, including web contacts assigned to it, and the active contact of the person, whatever its channel (the one GET /contacts?open=1 returns). Any other contact returns 403. Setting id_conversation on a contact of another channel makes it yours: its notices reach you from then on.

A status change follows the same path as the admin: it is written to the contact history with your AI agent user as the author, and it feeds the sales statistics like any seller's change. It does not send reminder emails to the customer nor create seller alerts — the conversation is yours. Three rules: RESERVED cannot be set here, it is set by the reservation; SPAM unassigns the contact; and an archived contact (ANSWER, UNANSWERED or RESERVED) cannot be reopened: moving it to any other status than those three or SPAM returns 400 invalid_request without saving changes. When the person comes back, create a new contact with POST /contacts.

The request does not change here: destination, dates, passengers and package. Send what the customer asks for later to POST /contacts/{id}/interests, and the contact will show it; correct the latest request with PATCH /contacts/{id}/interests/{id_interest}. message stays the conversation summary. The reservation carries the final choice, whatever product it is for, and is linked to this same contact through id_contact.

For compatibility with CRM follow-ups, resent id_destination, start_date, end_date and pax_count are ignored without blocking the other updates. Normal field-format validation still applies. id_product, id_departure, id_price, rooms, coupon_code and query are not accepted: they return 400 invalid_request without saving changes. A package goes to POST /contacts/{id}/interests.

A typical follow-up sends only editable fields, for example {"status":"QUOTE","ai_score":60,"message":"Quote sent; the customer will confirm on Friday."}. The receipt returns the contact's trip unchanged.

Name Type Description
firstname, lastname, email, phone string Customer details. Send phone whole, with its country calling code: it replaces the calling code and the number.
id_destination integer Deprecated and ignored: a new destination goes to POST /contacts/{id}/interests. Accepted, with its format validated, so a follow-up that resends it is not blocked.
start_date / end_date string Deprecated and ignored: new dates go to POST /contacts/{id}/interests with the destination. Accepted as YYYY-MM-DD so a follow-up that resends them is not blocked.
pax_count integer Deprecated and ignored: a new passenger count goes to POST /contacts/{id}/interests with the destination. Accepted, at least 1, so a follow-up that resends it is not blocked.
message string Conversation summary. Remains editable. A later destination or package the customer asks for goes to POST /contacts/{id}/interests.
status string Any workflow status except RESERVED: PENDING, RECONTACT, INITIAL_CALL, QUOTE, FOLLOW, FOLLOW_2, FOLLOW_3, ANSWER, UNANSWERED, SPAM. What each one means is in the workflow statuses below.
ai_score integer Conversation qualification, 0 to 100.
conversation_url string Deep link to the conversation.
id_conversation string Your conversation id, to link a contact that has none (a web contact whose lead notice never reached you, found through GET /contacts by phone). It can be written once: resending the same value does nothing, and a different value, or one that belongs to another contact, returns 400 without saving the rest of the request.

POST /contacts/{id}/interests

When the customer asks for something else (another destination, another package, another departure), add it to the same contact as an interest instead of opening another contact. The DeTurista staff sees it as a subconsulta in the contact. Say with type what it is: return when the person came in again (wrote again in a new conversation, from an ad, through another channel) and change when they changed their mind in the same conversation; without it, change. Marketing counts the returns as new leads.

Each call adds one interest, and the contact shows the latest one: its area, destination or package, departure, price, rooms, passengers, dates and coupon become the interest's, and a destination interest leaves it without the package of the previous one. Its seller, status, message, channel and UTM fields do not change, and what the contact came in with stays first in interests as type: original. To correct the latest request without adding another one (another departure, price, rooms or coupon of the same package; other dates or passengers of the same destination), use PATCH /contacts/{id}/interests/{id_interest}. Keep using PATCH /contacts/{id} with message for the conversation summary.

Send either id_destination, or a whole package: id_product, id_departure, id_price and rooms, validated like /products/{product_id}/departures/{departure_id}/contacts (a public product, a selectable departure, an available price of that departure, rooms that price offers). Half a package returns 400 naming the missing field, and both a destination and a package return 400. A destination interest takes the start_date, end_date and pax_count the customer gave, validated like on POST /contacts; without them the contact is left without dates or passengers, because it shows the latest interest. A package interest stores the departure dates, passengers and price of that selection the same way a package contact does, with coupon_code applied and validated like on creation.

It requires the contacts:update scope and reaches the same contacts as PATCH /contacts/{id}: yours and the active contact of the person; any other returns 403. It answers 201 with the contact receipt, whose interests list ends with the new one. If anything fails, nothing is saved.

curlAdd a package interest
curl -sS 'https://api.deturista.com/v1/contacts/554401/interests' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"channel":"whatsapp","id_product":1234,"id_departure":5678,"id_price":4567,"rooms":{"double":1},"message":"Le interesa esta salida, sólo ellos dos"}'
Name Type Description
channel whatsapp, call, instagram, facebook, email, or web Required. Channel of the conversation where the customer asked for it, with the values of POST /contacts. Returned as origin.
type return or change Optional; defaults to change. return when the person came in again (wrote again in a new conversation, from an ad, through another channel): marketing counts it as a lead. change when they changed their mind in the same conversation.
id_destination integer A destination interest, from /destinations. Unknown ids return 404 destination not found.
start_date / end_date string Tentative trip dates of a destination interest, YYYY-MM-DD, as in POST /contacts. The contact shows them. Not used with a package, which takes the departure dates.
pax_count integer Passengers of a destination interest, at least 1. Not used with a package, which takes them from rooms.
id_product integer A package interest: the public product, from /products.
id_departure integer A selectable departure of id_product, from /products/{id}/departures.
id_price integer An available price of id_departure, from the departure detail prices list. A price of another departure, unavailable or unknown, returns 404 price not found.
rooms object Room selection as a map of room type to the number of rooms of that type, as in /products/{product_id}/departures/{departure_id}/contacts, for example {"double": 1}. Every selected room type must appear in the selected price's amounts list.
coupon_code string Only for a package interest: applied and validated like on /products/{product_id}/departures/{departure_id}/contacts. A coupon that does not apply returns 400 coupon code is not available for this product; with a destination it returns 400.
message string What the customer said about this interest (dates, who travels, budget). Kept with the interest; the contact's own message does not change.
utm_source, utm_medium, utm_campaign, utm_content, utm_term string Optional, up to 255 characters.

Every contact receipt, GET /contacts/{id} included, carries interests: every request of the contact, oldest first. The first one is what the contact came in with (type: original, source: null, message: null); the last one is the request the contact shows: trip and the package fields of the receipt are that one, and it is the only one the PATCH below corrects. A destination interest has id_destination, destination and the dates and passengers sent with it, if any; a package interest has id_product, id_departure, id_price, rooms, the departure dates, passengers and list price.

JSONinterests in the contact receipt
"interests": [
  {
    "id_interest": 11,
    "creation_date": "2026-11-01 09:30:00",
    "source": null,
    "type": "original",
    "origin": "WHATSAPP",
    "id_destination": 42, "destination": "Punta Cana",
    "id_product": null, "id_departure": null, "id_price": null,
    "rooms": null,
    "start_date": null, "end_date": null, "pax_count": null,
    "currency": null, "amount": null, "usd_cash": null,
    "coupon_code": null,
    "message": null
  },
  {
    "id_interest": 12,
    "creation_date": "2026-11-03 14:05:00",
    "source": "AI",
    "type": "change",
    "origin": "WHATSAPP",
    "id_destination": null, "destination": null,
    "id_product": 1234, "id_departure": 5678, "id_price": 4567,
    "rooms": { "double": 1 },
    "start_date": "2027-07-12", "end_date": "2027-07-19",
    "pax_count": 2,
    "currency": "ARS", "amount": 2345600, "usd_cash": 1980,
    "coupon_code": null,
    "message": "Le interesa esta salida, sólo ellos dos"
  }
]
Field What it means
id_interest The id to send to PATCH /contacts/{id}/interests/{id_interest}.
source Who added it: AI (your agent, through this API) or WEB (the customer came back through the site). null for the original, which says how the contact came in with origin.
type original, what the contact came in with, always the first one; return, the person came in again (through the site, a new conversation, an ad), which marketing counts as a lead; change, they changed their mind in the same conversation. Interests added through the site, or by creating a contact for a person who already had an active one, are always return. It never changes.
origin Medium of that entry, with the contact's values: WHATSAPP, PHONE, INSTAGRAM, FACEBOOK, EMAIL and WEB for the channels of POST /contacts; the site's buttons may add PROMO_WHATSAPP, PROMO_CALL and PROMO_CONTACT.
amount / currency Price of the whole selection when the interest was added, with its coupon if it came with one, and its currency (ARS or USD). null for a destination interest.
usd_cash The selection's price in cash dollars, when the package has one.
coupon_code The coupon applied to the package interest, as it was sent.

PATCH /contacts/{id}/interests/{id_interest}

Corrects the request the contact shows, the last one of its interests, without adding another one: when the customer keeps the same package but changes the departure, the price, the rooms or the coupon, or keeps the same destination but changes the dates or the passengers. id_interest is the one the receipt lists in interests. It requires the contacts:update scope.

Send only what changes; what is not sent stays as it was. The result is validated whole, like POST /contacts/{id}/interests: for a package, a public product, a selectable departure of it, an available price of that departure and rooms that price offers. When id_departure changes, send its id_price too. Without rooms the request keeps its own. The package request is calculated again like on creation (departure dates, passengers, price) and the contact shows it.

Coupon: coupon_code adds or changes it. Without coupon_code the request keeps its coupon, validated again with the new selection; when it no longer applies the answer is 400 coupon code is not available for this product. An empty coupon_code ("" or null) removes the coupon, which also unblocks a request whose coupon stopped applying.

curlChange the departure of the latest request
curl -sS 'https://api.deturista.com/v1/contacts/554401/interests/12' \
  -X PATCH \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"id_departure":5701,"id_price":4602}'

To change the rooms and remove the coupon of the same request, send {"rooms":{"double":1,"single":1},"coupon_code":""}.

Request Fields it takes
Package id_departure (another departure of the same package; send id_price with it), id_price, rooms (rooms of the price, as on POST /contacts/{id}/interests) and coupon_code.
Destination start_date, end_date (YYYY-MM-DD) and pax_count (at least 1).
Rule What it means for your agent
Another package, destination or type is another request id_product, id_destination and type cannot change: sent with other values they return 400, because another package or destination is another request for POST /contacts/{id}/interests; with the same values they change nothing.
Each request takes its own fields start_date, end_date or pax_count on a package request return 400: they come from the departure and the rooms. coupon_code, id_departure, id_price or rooms on a destination request return 400.
Only the latest request of a contact that is not archived An older request, or a contact in ANSWER, UNANSWERED or RESERVED, returns 400. A contact in SPAM is corrected.
The same contacts as POST /contacts/{id}/interests Yours and the active contact of the person; any other returns 403, and an id_interest of another contact returns 404.
A correction, not a new request The seller is not notified and the contact does not go up in the seller's list. It answers 200 with the contact receipt.

The contact workflow statuses

The status is the sales funnel a seller works in the admin, so it answers "how far did this conversation get". The middle column is the label the seller reads on screen, in Spanish, exactly as it appears there. A status your agent sets looks the same to the seller as one a colleague set.

Status Admin label What it means
PENDING Sin Atender Every contact is created with this one, yours included: nobody has worked it yet. It is also the status that takes a contact back out of SPAM.
RECONTACT Recontactar The customer went quiet and will be chased again. This is how you park a conversation without closing it.
INITIAL_CALL Contacto Inic. First real contact with the customer happened.
QUOTE Cotización A quote or a price reached the customer.
FOLLOW Seguimiento The conversation was picked up again after the quote.
FOLLOW_2 Seguimiento 2 Second follow-up.
FOLLOW_3 Seguimiento 3 Third and last follow-up.
ANSWER Respondido Closed without a sale. Archives the contact.
RESERVED Reservado The reservation was confirmed and paid. Archives the contact. DeTurista sets this one, never you: the PATCH answers 400. You see it on the receipt.
UNANSWERED No atendida The customer never answered at all. Archives the contact. DeTurista already sets it on its own after 60 days in PENDING (the window is configurable), so you rarely need to send it.
SPAM SPAM Junk. Unassigns the seller and blocks that email address.
Rule What it means for your agent
The order is not validated You can go straight from PENDING to FOLLOW_3, or back, without passing through the ones in between. Send the status that matches the conversation instead of walking the funnel step by step.
Sending the status it already has does nothing No history row and no change to the statistics, and the response is still the contact receipt. Retrying is safe.
Three statuses archive the contact ANSWER, UNANSWERED and RESERVED mark the contact as handled and move it to the admin's archive. An archived contact cannot be reopened: any other status than those three or SPAM returns 400 without saving changes, so do not use them to park a conversation — that is what RECONTACT is for. When the person comes back, create a new contact with POST /contacts.
Filtering is not restricted GET /contacts?status= accepts RESERVED too: the restriction is on setting it, not on searching for it.

Reservations

GET /reservations/{id}

Where the reservation stands money-wise, for the moment the customer says "I already paid" or before you re-send a payment link:

payment_status What it means
unpaid Nothing paid yet.
deposit_paid Some money in, a balance still outstanding — typically the deposit. Ask for owed, not for the total.
paid Nothing owed. payed_date tells you when.
cancelled Cancelled, or the customer is not travelling. Stop selling, whatever the balance says.

Alongside it come total, paid and owed, as the usual ARS/USD pairs. This is a snapshot: it tells you how things stand right now, which is what you need mid-conversation. Knowing when a payment lands is what the notification is for.

Reservation receipt lookup returns persisted receipt fields for reservations created by the authenticated API consumer. Reservations owned by another consumer, outside the authenticated entity or site, or created outside the public API return 404 not_found. The read-back receipt does not include id_price or payment_option; those fields are returned only by the creation response.

Destinations

GET /destinations

Destinations return the active destination tree visible to the authenticated API consumer. Use GET /destinations/{id} to retrieve one destination. Its data.videos contains the destination’s own active videos, or the country’s active videos when the destination has none. These lists are never merged. Country detail returns its own active country videos; if no applicable videos are active, the array is []. See video fields and selection guidance.

Categories

GET /categories

Categories return the website-visible category tree for the authenticated API consumer. Use GET /categories/{id} to retrieve one category.

Product groups

GET /product-groups

Product groups return destination or category group totals with drill-down URLs for /products. Set include_top_ranked=1 to include compact previews from real public products.

Home

GET /home

Home returns one section of the home for the authenticated consumer's site. type is required and selects the section: slider, list and popup return the ordered blocks of that type; banners returns the banners strip (up to two). There is no combined response — call once per section. Each block references a product, category, or destination; product resources and LIST products are compact home cards (not the full product row — load /products/{id} for full detail). At most one popup block is returned (the first by order), matching the single popup shown on the home page.

Site

GET /site

Site returns public site and footer metadata for the authenticated consumer's site: contact fields, social links, legal text, and the public office list. Analytics, pixel, and verification keys are not exposed.

Newsletter

POST /newsletter

Newsletter creates an email subscription for the authenticated consumer's site. It reuses the public newsletter validation and duplicate-email guard, so an invalid field or an already-subscribed email returns 400 invalid_request and a new subscription returns 201 with a receipt.

Success 200 OK
Create success 201 Created
Auth failure 401 Unauthorized
Missing scope 403 Forbidden
Invalid request 400 Bad Request
Not found 404 Not Found
Rate limit 429 Too Many Requests
The default rate limit is 10 requests per second per API consumer. A configured consumer limit overrides that default.

TC trips and hotel shopping

Use this flow when a traveller wants to search hotels and keep a choice for later. You create a trip, add a hotel stay, request live availability from Travel Compositor, then save the chosen room option. The trip keeps that work between requests so you can reopen it without starting the search again.

What is a trip?

A trip is a saved itinerary in DeTurista. It starts as an empty draft and can hold several services, such as a hotel stay in Madrid followed by another stay in Barcelona. Creating it does not create a TC booking, a CRM contact or a reservation. This public workflow adds hotel services and saves selections; it does not book them.

ConceptWhat it representsID to keep
TripThe saved itinerary containing your services.data.trip.id
Hotel serviceOne stay's destination or hotel target, dates, room occupants, saved results and selected option.data.service.id
Hotel resultOne quoted TC hotel, with its available room combinations.results[].result_id
Room optionOne complete combination covering every requested room for the whole stay, with its price and conditions.results[].options[].option_id

Trip and service IDs identify saved records. Result and option IDs belong to the current quote; a new Quote replaces them. Keep the current revision alongside each saved record so an older request cannot overwrite a newer choice.

TC hotel-shopping quick start

Send your private API key in X-API-Key from your backend. The full flow needs tc-trips:create, tc-trips:hotels:search and tc-trips:hotels:select. These grants are independent; a basic trip-read grant does not include shopping. Paths below are relative to https://api.deturista.com/v1; locally use the existing app's /api/v1 base.

The IDs and revision numbers below are illustrative. Copy the values returned by your own requests. Response excerpts omit unrelated fields; the API reference and Swagger describe the complete responses. Availability, prices and conditions always come from your live quote.
  1. 1
    Find a TC destination or hotel code GET /tc-trip/catalog/destinations?q=Madrid

    Use local catalog lookup when you do not already know the TC code. Hotel lookup also accepts an exact destination code:

    curlLocal hotel lookup
    curl -sS 'https://api.deturista.com/v1/tc-trip/catalog/hotels?destination_code=MAD&limit=25' \
      -H 'X-API-Key: YOUR_API_KEY'
    Keep
    The destination code or hotel accommodation_code.
    Next
    Use a destination code to compare hotels, or a hotel code to target a specific property. These are TC codes, not local database IDs. Lookup is local and has no availability or prices. An empty local catalog does not prove TC has no availability.
  2. 2
    Create the tripPOST /tc-trip
    JSONRequest body
    { "title": "Madrid hotel stay" }

    HTTP 201 returns the saved trip:

    JSONResponse excerpt
    { "success": 1, "data": { "trip": { "id": 123, "revision": 1, "state": "DRAFT" } } }
    Keep
    Trip ID 123 and its current revision.
    Next
    Add a hotel service to this trip. The API assigns the trip to your consumer. Do not send a seller, consumer or account override. Creation has no idempotency key, so do not automatically repeat it after an uncertain response.
  3. 3
    Save the hotel's search detailsPOST /tc-trip/123/services

    Use the trip revision from step 2. This example requests one room for two people:

    JSONRequest body
    {
      "revision": 1,
      "intent": {
        "checkIn": "2027-06-10",
        "checkOut": "2027-06-13",
        "tripType": "ONLY_HOTEL",
        "destinationId": "MAD",
        "distributions": [{ "persons": [{ "age": 35 }, { "age": 8 }] }],
        "filter": { "bestCombinations": true, "maxCombinations": 3 },
        "language": "EN",
        "sourceMarket": "AR"
      }
    }

    HTTP 201 returns a saved hotel service; this step does not contact TC:

    JSONResponse excerpt
    { "success": 1, "data": {
      "trip": { "id": 123, "revision": 2 },
      "service": { "id": 456, "revision": 1, "service_type": "ACCOMMODATION", "state": "DRAFT" }
    } }
    Keep
    Service ID 456, its revision and the latest trip revision.
    Next
    Quote this service. Occupancy requires actual ages grouped by room; adult/child counts are insufficient. Choose exactly one target: intent.destinationId, intent.accommodations, or outer accommodation_id. See the reference for the single-hotel filter shape.
  4. 4
    Request live hotel availabilityPOST /tc-trip/123/services/456/quote
    JSONRequest body
    { "revision": 1 }

    The response contains the current service revision and a first page of quoted hotels:

    JSONResponse excerpt
    { "success": 1, "data": {
      "service": { "id": 456, "revision": 4, "state": "QUOTED" },
      "results": [{
        "result_id": "7c4f2e8a1b9d4a50b6350fd87a6e29c3",
        "quote_single_needed": 1,
        "options": [{ "option_id": "4a91dc7f0b2e453a89f6d31c5e8072ab" }]
      }]
    } }
    Keep
    The latest service revision, the chosen hotel's result_id, and available option IDs.
    Next
    Display the returned hotel content, rooms, price.amount, currency and cancellation terms. Price is a decimal-string total for every requested room and the whole stay; do not multiply it by passengers or nights. Every Quote calls TC for current availability and rates. Empty availability is a successful response with no selectable options.
  5. 5
    Filter saved results and expand a hotel's options GET /tc-trip/123/services/456?stars=4,5&meals=BED_AND_BREAKFAST&sort=price_asc

    Filtering and pagination read the saved quote without another TC call. Use page and limit for more hotels. If a hotel needs expansion, or you want its current room combinations, use POST /tc-trip/123/services/456/quote-single:

    JSONQuote Single request body
    { "revision": 4, "result_id": "7c4f2e8a1b9d4a50b6350fd87a6e29c3" }
    JSONResponse excerpt
    { "success": 1, "data": {
      "service": { "id": 456, "revision": 5 },
      "results": [{
        "result_id": "7c4f2e8a1b9d4a50b6350fd87a6e29c3",
        "options": [{ "option_id": "b6e02d9a7f1348c0a592ed34fc6817ba" }]
      }]
    } }
    Keep
    The latest service revision and option IDs from expansion.
    Next
    Quote Single calls TC, retains the hotel's result ID, replaces its option IDs and clears any existing selection. Choose from the new options. If expansion is unnecessary, select using the current revision and option IDs from Quote instead.
  6. 6
    Save the chosen complete room optionPOST /tc-trip/123/services/456/select

    This example follows step 5's expansion:

    JSONRequest body
    {
      "revision": 5,
      "result_id": "7c4f2e8a1b9d4a50b6350fd87a6e29c3",
      "option_id": "b6e02d9a7f1348c0a592ed34fc6817ba"
    }
    JSONResponse excerpt
    { "success": 1, "data": {
      "service": { "id": 456, "revision": 6, "state": "SELECTED",
        "selection": { "result_id": "7c4f2e8a1b9d4a50b6350fd87a6e29c3",
                       "option_id": "b6e02d9a7f1348c0a592ed34fc6817ba" }
      }
    } }
    Keep
    The saved trip/service IDs, current revisions and selected customer terms.
    Next
    Show the saved choice. Selection saves the exact complete room combination locally; it does not Confirm, Prebook or Book. A selection-only grant returns the selected terms, not a page of other options.
  7. 7
    Reopen or edit the stayGET /tc-trip/123/services/456

    Reopening returns the saved intent, descriptions, options, selection and current revisions. It does not contact TC or renew expired offers. Later catalog edits do not replace descriptions in this saved quote. To change dates, rooms or target, send the service revision and full replacement intent to POST /tc-trip/123/services/456, then Quote again. Editing clears earlier results, selection and flow.

    Keep
    The returned current revisions and service.requote_required.
    Next
    Use the trip revision only when adding another service. Use the service revision for edits, Quote, Quote Single and selection. Basic saved trip list/detail use separate read grants and keep a smaller response; hotel shopping details come from this service endpoint.
  8. 8
    Re-quote when the choice expires or becomes stalePOST /tc-trip/123/services/456/quote
    JSONRequest body after step 6
    { "revision": 6 }

    A fresh Quote calls TC again, replaces old result/option IDs and clears the prior selection. Ask the traveller to choose again using the new IDs and current terms. Never carry an old choice forward automatically.

    On error
    HTTP 409 conflict with code: STALE_REVISION: reload the service before deciding what to change. HTTP 409 conflict with code: REQUOTE_REQUIRED: reload and explicitly Quote before another expansion or selection. A known provider failure returns 502 provider_error; reload saved state, because a failed fresh Quote cannot restore an old selection.
    Remember
    Descriptions use the matching local database source first, so a stored description prevents a TC datasheet request for that hotel. Quote and Quote Single still call TC for availability, prices and conditions. Saved filtering and reopening reuse that snapshot without provider calls.

See the hotel-shopping API reference for every field, scope and filter, or open the TC operations in Swagger. Offers/PDFs, passenger personal data and booking actions are outside this public workflow.

Saved trip reads

Use GET /tc-trip and GET /tc-trip/{id} to read saved local trips. tc-trips:read shows trips assigned to your consumer. The independent tc-trips:read:any grant also shows other consumers' trips and unassigned trips. Both grants stay within your configured entity and site and expose the same fields. They do not grant contact or reservation access. Existing keys need an explicit TC grant before using these endpoints.

All saved states are readable, including drafts and trips requiring review. These calls do not contact TC, refresh provider data or repair saved state. These basic read grants do not authorize shopping, offers or document downloads. An inaccessible or missing trip returns 404 not_found with message: "trip not found". Missing operation permission returns 403 forbidden before looking up the trip.

List and filters

The list accepts only page, limit and optional exact trip_state. Defaults are page 1 and limit 25. Both must be positive integers; limit above 100 is rejected. States are DRAFT, IN_PROGRESS, PARTIALLY_BOOKED, BOOKED, PARTIALLY_CANCELED, CANCELED and REQUIRES_REVIEW. Unsupported fields, including consumer or site overrides, return 400 invalid_request. Detail accepts no query fields and requires a positive integer ID.

curlSaved bookings
curl -sS 'https://api.deturista.com/v1/tc-trip?trip_state=BOOKED&page=1&limit=25' \
  -H 'X-API-Key: YOUR_API_KEY'

The response is { "success": 1, "data": { "page": 1, "limit": 25, "total": 1, "results": [...] } }. Visibility and state filtering apply before count and pagination. Results sort by saved update time descending, then ID descending for ties. Each result has exactly these summary fields:

JSONTrip summary
{
  "id": 123,
  "revision": 4,
  "title": "Winter trip",
  "state": "BOOKED",
  "creation_date": "2026-10-25 09:00:00",
  "update_date": "2026-10-26 10:30:00",
  "service_counts": { "total": 2, "booked": 2, "canceled": 0 },
  "totals": { "plan": { "USD": "150.75", "EUR": "20" }, "plan_complete": 1 }
}

TC trip money uses decimal strings grouped by currency, unlike catalog integer price fields. Current-plan totals exclude canceled and removed services. Manual selling amounts are whole-line amounts, even when quantity exceeds one. An incomplete price sets numeric plan_complete to 0 while retaining known amounts. An empty plan has plan: {} and completeness 0. Counts include canceled services, exclude removed services and count a service as booked only when its saved state is BOOKED. Dates and times use saved application-local values, with no timezone suffix.

Detail and included services

GET /tc-trip/123 returns { "success": 1, "data": { "trip": { ... } } }. The trip has the same summary fields plus services, ordered by local service ID ascending. Included canceled services remain visible; removed services are omitted.

Service kindExact public fields
TC servicesid, service_type, state, booking_reference, external_reference, provider_booking_status, provider_service_status, details_retrieved_at. Types are ACCOMMODATION, FLIGHT, TRANSFER, TICKET and CLOSED_TOUR.
Manual servicesid, service_type: "AD_HOC", state and ad_hoc. That object contains title, description, start_date, end_date, qty, decimal-string selling amount, currency and voucher_reference. Dates use YYYY-MM-DD.

Optional unrecorded values are JSON null. Responses exclude passenger and customer personal data, internal notes, supplier costs, raw provider payloads, protected tokens, audit diagnostics, document locations and staff actions. The broader read grant changes which trips you can see, not which fields you receive.

Hotel-shopping API reference

Each operation below shows its full path. Replace {id} with the saved trip ID and {service_id} with the saved hotel service ID. Neither is a TC hotel code. Production paths start at https://api.deturista.com/v1; local paths start at http://localhost:8181/api/v1.

Send X-API-Key on every request and Content-Type: application/json with JSON bodies. Use IDs and revisions from your responses. The examples below use illustrative values. Your key needs the scope listed for each operation, and shopping is limited to trips assigned to your consumer.

Find destination codes

GET /tc-trip/catalog/destinations

Scope: tc-trips:hotels:search

Search the locally saved destination names or TC codes. This does not check hotel availability.

curlFind Madrid
curl -sS 'https://api.deturista.com/v1/tc-trip/catalog/destinations?q=Madrid&page=1&limit=25' \
  -H 'X-API-Key: YOUR_API_KEY'

Optional query fields: q, page and limit. The default page is 1 and limit is 25; the maximum limit is 100.

Response: HTTP 200 with data.page, data.limit, data.total and data.results. Each result has a TC code and name. Keep the chosen code for intent.destinationId.

Find hotel codes and descriptions

GET /tc-trip/catalog/hotels

Scope: tc-trips:hotels:search

Look up locally saved hotels by name or exact TC code. Add destination_code to restrict the lookup to a TC destination.

curlHotels in Madrid
curl -sS 'https://api.deturista.com/v1/tc-trip/catalog/hotels?destination_code=MAD&limit=25' \
  -H 'X-API-Key: YOUR_API_KEY'

Optional query fields: q, destination_code, page and limit. Pagination uses the same defaults and limit as destination lookup.

Response: HTTP 200 with a paginated data.results list. Each result contains accommodation_code and the available public hotel content. These are descriptions, not live prices or availability. Local lookup may be incomplete; known TC codes can still be used for a live quote.

Create a trip

POST /tc-trip

Scope: tc-trips:create

Start an empty saved itinerary. The only accepted body field is optional title, up to 255 characters.

JSONRequest body
{ "title": "Madrid hotel stay" }

Response: HTTP 201 with data.trip. Keep its id and revision before adding a hotel stay. This does not create a booking, contact or reservation. Creation has no idempotency key, so do not automatically repeat an uncertain request.

Add a hotel stay to the trip

POST /tc-trip/{id}/services

Scope: tc-trips:hotels:search

Send the current trip revision and the hotel search details in intent. For example:

JSONRequest body
{
  "revision": 1,
  "intent": {
    "checkIn": "2027-06-10",
    "checkOut": "2027-06-13",
    "tripType": "ONLY_HOTEL",
    "destinationId": "MAD",
    "distributions": [{ "persons": [{ "age": 35 }, { "age": 8 }] }],
    "filter": { "bestCombinations": true, "maxCombinations": 3 },
    "language": "EN",
    "sourceMarket": "AR"
  }
}

Response: HTTP 201 with data.trip, data.service and an empty saved result page. Keep the hotel service's id and revision. Saving the search is local; use the Quote operation to request availability.

Hotel search fields

Use this body format when adding a hotel stay or replacing its search. Unknown fields are rejected.

FieldRequired?Accepted value
revisionYesPositive integer. Use the trip revision when adding a stay, or the service revision when replacing its search.
intent.checkIn, intent.checkOutYesDates in YYYY-MM-DD format. Checkout must follow check-in.
intent.tripTypeYesA supported TC trip type. Use ONLY_HOTEL for this hotel-only flow; Swagger lists the accepted values.
intent.distributionsYesOne to four rooms. Each contains a persons array of one to six people with integer age values from 0 to 120. Counts of adults/children cannot replace ages.
intent.languageNoA supported TC language code, such as EN or ES. Swagger lists the accepted values.
intent.sourceMarketNoA two-letter country code, such as AR.
intent.timeoutNoAn integer timeout of at least 3000 milliseconds.

Choose exactly one of these targets:

Search forTarget fieldFilter
Hotels at a destinationintent.destinationId: one TC destination code.intent.filter is required. It accepts optional boolean bestCombinations, boolean includeOnRequestOptions and integer maxCombinations from 1 to 60.
A list of known hotelsintent.accommodations: 1–3000 unique TC hotel code strings.The same required filter object as a destination search.
One known hotelaccommodation_id: one TC hotel code, outside intent. Omit destinationId and accommodations.Optional intent.filter, containing only boolean includeOnRequestOptions.

Do not send a currency selector. The quote response provides the actual selling currency and price.

Replace a stay's search details

POST /tc-trip/{id}/services/{service_id}

Scope: tc-trips:hotels:search

Send the current service revision and a complete replacement intent, using the hotel search fields above. This is a full replacement, not a partial patch. Omitting outer accommodation_id clears its previous value.

Response: HTTP 200 with the updated trip/service and saved result page. Editing clears the old results, selection and shopping flow. Run Quote again before choosing a room option.

Quote live hotel availability

POST /tc-trip/{id}/services/{service_id}/quote

Scope: tc-trips:hotels:search

Request current availability, room combinations, selling prices and conditions for the service's saved search. The body accepts only the current service revision:

JSONRequest body
{ "revision": 1 }

Response: HTTP 200 with the current trip/service and the first page of up to 25 hotel results. Each hotel has a local result_id, and each complete room combination has an option_id. Keep these IDs and the returned service revision. Empty availability is still HTTP 200, with no options and an UNAVAILABLE service.

Every explicit Quote calls TC and starts a new generation. It replaces old result/option IDs and clears any selection. A failed fresh Quote cannot restore the old choice.

Browse or filter the saved quote

GET /tc-trip/{id}/services/{service_id}

Scope: tc-trips:hotels:search

Reopen the saved search, results and selection. Filtering and pagination do not call TC, refresh descriptions or renew an expired offer.

curlSaved breakfast options
curl -sS 'https://api.deturista.com/v1/tc-trip/123/services/456?meals=BED_AND_BREAKFAST&sort=price_asc&limit=25' \
  -H 'X-API-Key: YOUR_API_KEY'
Query fieldMeaning
qCase-insensitive saved hotel-name search.
starsComma-separated star categories, for example 4,5.
mealsComma-separated TC meal codes: ROOM_ONLY, BED_AND_BREAKFAST, HALF_BOARD, FULL_BOARD or ALL_INCLUSIVE.
max_priceA non-negative decimal maximum for every room and the whole stay.
free_cancellation0 or 1. A value of 1 requires unambiguous saved evidence that cancellation is currently free; it is not a guarantee about later conditions.
sortrecommended, price_asc, price_desc or score. Default: recommended, preserving provider order. Missing scores remain missing.
page, limitPositive integers. Defaults: page 1, limit 25. Maximum limit: 100.

Filters intersect on the same complete room option. A cheap room-only rate cannot borrow breakfast or cancellation terms from another rate. Price filtering/sorting rejects a saved result set with mixed currencies.

Response: HTTP 200 with data.trip, data.service and data.page, data.limit, data.total and data.results. The total counts matching hotels before pagination. The service includes saved intent, selection, current revision, expiry and numeric requote_required.

Hotel results contain public hotel content, available options and nullable minimum_price. Options contain rooms, meal plan, decimal-string selling price/currency, on-request status, cancellation conditions and customer remarks. Prices cover every requested room for the whole stay. Missing optional content stays null. Swagger lists the exact nested fields.

Expand the room options for one quoted hotel

POST /tc-trip/{id}/services/{service_id}/quote-single

Scope: tc-trips:hotels:search

Use this after Quote to get current room combinations for one hotel. Send its local result_id from the saved results, not its TC accommodation code, together with the current service revision:

JSONRequest body
{
  "revision": 4,
  "result_id": "7c4f2e8a1b9d4a50b6350fd87a6e29c3"
}

Response: HTTP 200 with the current trip/service and default first page of saved hotel results. The hotel's result ID stays the same, but its option IDs are replaced and any saved selection is cleared. Use the new option IDs and service revision. If the hotel is outside the default page, browse the saved results again with pagination or name filtering.

This operation calls TC. It reuses the description saved with the quote and does not fetch that description again.

Save a room selection

POST /tc-trip/{id}/services/{service_id}/select

Scope: tc-trips:hotels:select

Send the current service revision and the result/option IDs from the same current quote:

JSONRequest body
{
  "revision": 5,
  "result_id": "7c4f2e8a1b9d4a50b6350fd87a6e29c3",
  "option_id": "b6e02d9a7f1348c0a592ed34fc6817ba"
}

Response: HTTP 200 with data.trip and data.service, including the selected customer terms and state SELECTED. There is no page of other hotel options in this receipt. Selection saves the complete room combination locally; it does not book it or refresh an expired price.

Descriptions, prices and saved results

Catalog lookup and hotel descriptions use saved local data. When a matching local description exists, TC is not called for that hotel's datasheet, even if the content is old, another field is missing or the requested language differs. description_language reports the stored language. Only description misses may use datasheet fallback. Quote and Quote Single still call TC for current availability, prices and conditions. Once quoted, filtering and reopening use the saved description snapshot, even after the catalog changes.

The local destination list is derived from saved hotels, not a complete TC destination directory. Missing local metadata does not prevent a live quote with a known valid TC code.

Errors and what to do next

ResponseWhat to do
400 invalid_requestCorrect the request fields, types, IDs or filters. Unknown fields and old-generation choices are rejected.
401 unauthorizedCheck the API key and whether it is enabled for this environment.
403 forbiddenThe key lacks the operation's required scope.
404 not_foundThe trip or hotel service is missing or unavailable to this API consumer. Check the saved IDs and assignment.
409 conflict, code: STALE_REVISIONReload the hotel service, use its current revision and decide whether the intended action is still appropriate.
409 conflict, code: REQUOTE_REQUIREDReload the service and explicitly Quote before another expansion or selection.
429 rate_limitedWait before retrying. Use saved result browsing rather than repeatedly quoting for filter changes.
502 provider_errorReload saved state. A provider request failed; do not treat it as empty availability or assume an old selection survived.

Errors use { "success": 0, "error": "...", "message": "..." }. Conflicts also include code. Server configuration or unexpected internal failures remain server errors.

Integration setup: optional default seller

DeTurista can configure public_api/consumers/<name>/id_seller as the default association for new trips. If omitted, seller is null. This is independent of ai_agent_user_id and does not change the API consumer's identity. It is a server setting, not a field to send when creating a trip.

Parameters

Product listing parameters

GET accepts these as query parameters. POST accepts the same keys in a JSON object.

Name Type Description
limit integer Maximum number of products to return. Defaults to 100 and is capped at 100.
page integer 1-based page number. page=1 is the first page.
id_products array Exact product ids. Use a comma-separated value in GET or an array in POST. The request fails if any id is not visible to the API consumer. Do not combine with listing filters or pagination.
id_destination integer Destination id. Child destinations are included by default.
destination_scope tree or linked Use tree to include child destinations, or linked to match only direct product-destination links.
id_category integer Category id. Child categories are included by default.
category_scope tree or linked Use tree to include child categories, or linked to match only direct product-category links.
month integer Departure month from 1 to 12. Returns only the products with at least one departure on sale that starts in that month, the ones /products/{id}/departures returns for the same month and year. Each row's price_from_*, deposit_from_*, num_of_payments, card_installment_amount and ars_installments are then the ones of the cheapest departure of that month, so the price can be quoted for that month. Without month they are the lowest of the whole product, on any date. Without year it means the next time that month comes.
year integer Departure year. Use it together with month to select a specific month in a specific year.

Product currency is the package base currency used by its prices, not a display conversion hint.

Product departure parameters

Name Type Description
limit integer Maximum number of departures to return. Defaults to 100 and is capped at 100.
page integer 1-based page number. page=1 is the first page.
month integer Month from 1 to 12. Returns only the departures whose start_date falls in that month; a trip that starts the month before and ends in it is not included. Without year it means the next time that month comes.
year integer Year of the requested month. Use it together with month; the values of filters.months (YYYY-MM) are the pairs that return departures.

Product departure filters describe every active departure of the product, whatever month was requested. total counts the departures that match month and year, and results is one page of them.

Product departure detail uses product_id and departure_id from the path. data.currency uses the product package base currency. The selected departure must belong to the product and must be active, future-visible, web-visible, and priced like rows returned by the departure listing endpoint.

Product departure price preview uses product_id and departure_id from the path and requires this JSON body:

Name Type Description
id_price integer Selected price id from the departure detail prices list.
rooms object Room selection as a map of room type to room count, for example {"double": 1}. Valid room types are the same as contact and reservation creation.
coupon_code string Optional alphanumeric coupon code to apply to the selected product. An unavailable or inapplicable code returns 400 invalid_request with coupon code is not available for this product. Internal eligibility reasons are not exposed.

The preview response always includes total_ars / total_usd, new_total_ars / new_total_usd, pax_count, num_of_payments, deposit_ars / deposit_usd, card_installments, card_installment_amount, ars_installments, deposit_usd_installments, payment_methods, applied, and default_code. The totals and the installment are for every passenger of the selected rooms; num_of_payments and deposit_* are null when the package has no payment plan, and card_installments and card_installment_amount are null unless the package is priced in ARS with card installments. ars_installments is null unless the package has 12 cuotas en pesos, and deposit_usd_installments unless payment_methods has DEPOSIT_USD_INSTALLMENTS. payment_methods lists the ways to pay this selection with its amounts, and is empty outside site 1. The suggested default_code is only returned when no coupon was applied in the request; when a coupon is applied it is null and the response also includes code, original_total_*, amount_discount_*, discount_type, and configured_value.

Product departure contacts use product_id and departure_id from the path and require this JSON body:

Ownership is always taken from the authenticated API key. A request body field named consumer is ignored.

Name Type Description
firstname string Lead first name.
lastname string Optional lead last name.
email string Lead email address.
phone string Lead phone number. The whole number with its country calling code, as in E.164 (+5491155667788). The leading + is optional and spaces and dashes are ignored. It is stored split into its calling code and the rest, like the contacts created on the site. A number sent without its calling code is read with the one its first digits make, so always include it.
origin contact, whatsapp, or call Selected contact action. The lead is stored using the matching legacy promo origin, which is what advertising attribution reads. Without an AI agent, whatsapp and call require current advisor availability and minimum rotation coverage — otherwise send contact. With an AI agent configured neither restriction applies: the lead falls back to the agent.
assign ai_agent or rotation Optional. Same rule as /contacts: defaults to ai_agent when your consumer has an AI agent, otherwise to rotation.
id_conversation string Optional conversation id from your CRM, up to 80 characters. When sent it is the idempotency key: a retry returns the contact created the first time with 200.
conversation_url string Optional deep link to the conversation, so a seller can open it from the contact.
id_price integer Selected price id from the departure detail prices list.
rooms object Room selection as a map of room type to room count, for example {"double": 1}. Valid room types: single, double, double_shared, triple, quadruple, sextuple, family_1, family_2. At least one count must be greater than zero, and each selected room type must appear in the selected price's amounts[].room_type.
coupon_code string Optional alphanumeric coupon code to apply to the selected product. An unavailable or inapplicable code returns 400 invalid_request with coupon code is not available for this product.

Room selection pax counts

The rooms object always counts rooms. The API expands those rooms into passenger slots for reservations.

Room key Passengers per room Example
single 1 {"single": 1} requires 1 passenger.
double 2 {"double": 1} requires 2 passengers.
double_shared 1 {"double_shared": 1} requires 1 passenger.
triple 3 {"triple": 1} requires 3 passengers.
quadruple 4 {"quadruple": 1} requires 4 passengers.
sextuple 6 {"sextuple": 1} requires 6 passengers.
family_1 3 {"family_1": 1} requires 2 passengers who have reached kids_age and 1 passenger under kids_age at trip end.
family_2 4 {"family_2": 1} requires 2 passengers who have reached kids_age and 2 passengers under kids_age at trip end.

kids_age is an exclusive child-age limit evaluated at the end of the trip: when it is 12, a child slot requires a passenger who is still under 12 on that date. Infants under 2 also use a child room slot. The price preview is the fixed total for the selected room occupancy and is not recalculated from passenger birth dates. Infant airline handling or charges are finalized by the sales team and are not added by price-preview.

Product departure reservations use the same path ids and require a structured reservation body:

Ownership is always taken from the authenticated API key. A request body field named consumer is ignored. Passenger count must match the pax count implied by rooms.

Name Type Description
id_price integer Selected price id from the departure detail prices list.
payment_option buy or deposit Selected payment option. The product must support the requested option.
payment_selection TRANSFER, ONLINE or MERCADOPAGO Optional; defaults to TRANSFER. Send ONLINE to create the reservation pending payment and receive the redirect URL of the gateway that charges reservations. MERCADOPAGO works only while that gateway is MercadoPago.
rooms object Room selection as a map of room type to room count, for example {"double": 1}. Valid room types match the contact endpoint, and every selected room type must be priced by the selected id_price.
customer object Required customer fields: firstname, lastname, email, phone, and cuil. The CUIL is used to resolve or create the linked client.
passengers array One object per room occupant. Each passenger requires firstname, lastname, document, nationality, and birth_date in YYYY-MM-DD format. Passengers are assigned in the room-key order shown above; within each family room send the two adult-age passengers first, followed by its one or two child-age passengers. Invalid family composition returns 400 invalid_request.
coupon_code string Optional alphanumeric coupon code to apply to the selected product. An unavailable or inapplicable code returns 400 invalid_request with coupon code is not available for this product.
viajeflex_id integer Optional ViajeFlex service id for flexible confirmed-flight departures. The id must be available for the selected price and room selection.
id_contact integer Required for a consumer with an AI agent: without it the reservation is not created and the response is 400 invalid_request. Optional for other consumers. The contact this reservation closes (from POST /contacts or GET /contacts/{id}), even when the reservation is for another product or destination: the contact keeps what the customer first asked for and the reservation carries the final choice. It must belong to your consumer, otherwise 404 not_found. Linking it moves the contact to RESERVED and lets us report the sale and its payments to your CRM. It also decides who owns the reservation: when the contact is already assigned to a seller, the reservation is created for that seller, even if your AI agent closes the sale; a contact still assigned to your AI agent keeps the default owner. Do not request a seller before reserving just to have an owner.

Destination listing parameters

Name Type Description
name string Optional partial-name filter. Matching is case-insensitive and accent-insensitive, so ?name=brasil can be used directly from free-text search. Filtering is applied before total and pagination.
limit integer Maximum number of destinations to return. Defaults to 100 and is capped at 100.
page integer 1-based page number. page=1 is the first page.

The destination detail endpoint uses the destination id from the path and has no query parameters.

Category listing parameters

Name Type Description
limit integer Maximum number of categories to return. Defaults to 100 and is capped at 100.
page integer 1-based page number. page=1 is the first page.

The category detail endpoint uses the category id from the path and has no query parameters.

Product group parameters

Name Type Description
by destination or category Required grouping dimension.
group string For destinations use continent, country, or linked. For categories use root, linked_parent, or linked. Category linked_parent means exactly one level above the exact linked category; if the linked category is four levels deep, the product groups under the category at level three. For linked_parent the group total is exact, but the products_url drill-down uses tree scope on that parent and may include more deeply nested products than the count.
include_top_ranked 0 or 1 Optional. Set to 1 to include compact top-ranked product previews for each group.
top_ranked_limit integer Optional. Maximum previews per group when top-ranked products are included. Defaults to 3; values above 3 are rejected.

Responses

Successful listing responses include success: 1 and a data object with total and results. Catalog detail responses include the resource object directly under data. Saved TC trip detail uses data.trip, as shown in Saved TC trips.

JSONSuccess shape
{
  "success": 1,
  "data": {
    "total": 69,
    "results": [
      {
        "id": 2158,
        "title": "Mundial de Futbol 2026 - 16avos de Final",
        "subtitle": "03 noches de alojamiento...",
        "summary": "03 noches de alojamiento...",
        "url": "2158/mundial_de_futbol_2026.html",
        "image": "img_web-03.jpg",
        "discount": 20,
        "transport": "Aereo",
        "meal_plan": "Desayuno",
        "currency": "ARS",
        "price_from_ars": 5835,
        "price_from_usd": 990,
        "deposit_from_ars": 1200,
        "deposit_from_usd": 190,
        "num_of_payments": 5,
        "card_installments": null,
        "card_installment_amount": null,
        "ars_installments": null,
        "nights": [3, 4],
        "has_hotel": false,
        "can_deposit": true,
        "can_buy": false,
        "destinations": [
          { "id": 410, "name": "Miami" }
        ],
        "categories": [
          { "id": 154, "name": "Mundial" }
        ]
      }
    ]
  }
}

Product detail responses return the product object directly under data and replace the listing image field with the ordered images gallery plus the base package description and public payment summaries. A departure can carry a different description. After a departure is selected, treat the departure detail description as authoritative: it replaces the base package description completely and the two values must not be merged.

JSONProduct detail
{
  "success": 1,
  "data": {
    "id": 2158,
    "title": "Mundial de Futbol 2026 - 16avos de Final",
    "subtitle": "03 noches de alojamiento...",
    "summary": "03 noches de alojamiento...",
    "description": "Día 1: llegada a Miami.\r\nDía 2: excursión incluida...",
    "url": "2158/mundial_de_futbol_2026.html",
    "discount": 20,
    "transport": "Aereo",
    "meal_plan": "Desayuno",
    "currency": "ARS",
    "price_from_ars": 5835,
    "price_from_usd": 990,
    "deposit_from_ars": 1200,
    "deposit_from_usd": 190,
    "num_of_payments": 5,
    "card_installments": null,
    "card_installment_amount": null,
    "ars_installments": null,
    "nights": [3],
    "has_hotel": false,
    "can_deposit": true,
    "can_buy": false,
    "destinations": [
      { "id": 410, "name": "Miami" }
    ],
    "categories": [
      { "id": 154, "name": "Mundial" }
    ],
    "images": [
      {
        "name": "img_web-03.jpg"
      }
    ],
    "payment_options": {
      "deposit": [
        {
          "name": "Tarjeta Visa",
          "title": "Reserva con tarjeta",
          "description": "Pago de seña en cuotas disponibles.",
          "payment_selection": "TRANSFER"
        },
        {
          "name": "MercadoPago",
          "title": "Pagá con Mercado Pago",
          "description": "Checkout Pro para pagar online.",
          "payment_selection": "MERCADOPAGO"
        }
      ]
    }
  }
}

When the authenticated API consumer is scoped to a USD display site, the same money fields use only the USD suffix.

JSONUSD display site product
{
  "success": 1,
  "data": {
    "id": 3150,
    "title": "Caribe 2026",
    "description": "Día 1: llegada a Punta Cana...",
    "url": "3150/caribe_2026.html",
    "discount": null,
    "transport": "Aereo",
    "meal_plan": "All inclusive",
    "currency": "USD",
    "price_from_ars": null,
    "price_from_usd": 990,
    "deposit_from_ars": null,
    "deposit_from_usd": null,
    "num_of_payments": null,
    "card_installments": null,
    "card_installment_amount": null,
    "ars_installments": null,
    "nights": [7],
    "can_deposit": false,
    "can_buy": true,
    "destinations": [
      { "id": 410, "name": "Miami" }
    ],
    "categories": [
      { "id": 154, "name": "Caribe" }
    ],
    "images": [],
    "payment_options": {
      "buy": []
    }
  }
}
Field Rule for the next API call
description This is the base package description, including its itinerary and excursion details. Once the user chooses a departure, use the departure detail description instead: a departure-specific value replaces this text completely rather than extending it. Never concatenate or merge the two descriptions.
currency This is the package base currency (ARS or USD) used by the product prices. Money field suffixes identify the currency of each numeric amount.
discount Informational package-level percentage, useful for badges or commercial labels. It does not replace departure or price calculations.
transport / meal_plan Public labels from the package configuration. Internal ids are not exposed.
can_buy When true, the product can support a reservation with payment_option=buy. Still fetch departure detail and choose a valid id_price before creating the reservation. It is false on packages with ars_installments.
can_deposit When true, the product can support a reservation with payment_option=deposit. This is effective availability, so it is false when installments do not apply even if deposit is configured internally, and on packages with ars_installments. The selected departure price must also return deposit values.
nights Display these numeric durations as nights available for the product.
images Only present on product detail. Items are ordered by the admin gallery order, and each one carries its absolute url and its caption.
payment_options Only present on product detail. Includes buy and/or deposit arrays when those actions are available. Send the chosen option's payment_selection; the gateway account is resolved internally by Deturista. The online option is named MercadoPago or Getnet after the gateway that charges reservations, and its payment_selection is MERCADOPAGO while MercadoPago charges them and ONLINE once Getnet does. The keys depend on your consumer. For a regular consumer they follow can_buy and effective can_deposit, the web checkout flags. A consumer with an AI agent must ignore those flags: it always gets buy, and deposit on every international package with installments, provided the selected departure price has num_of_payments. A missing key means that option is rejected. TRANSFER is always accepted, even when the list is empty; online payment only when the list carries an online method. On packages with ars_installments a regular consumer gets no key, like the website; a consumer with an AI agent keeps both.
card_installments Present on products, departures, departure amounts and price preview next to num_of_payments. Number of interest-free card installments of a package priced in ARS, or null. They are paid on the full amount (payment_option=buy with payment_selection=MERCADOPAGO), with no surcharge and no deposit: these packages never have num_of_payments and reject payment_option=deposit. Quote each installment from card_installment_amount.
card_installment_amount Present wherever card_installments is, right after it. Each card installment in ARS, as a rounded integer: the price of that row divided by card_installments, the same number the website shows. On products and departures it is over price_from_ars, on departure amounts over the per-passenger price_ars, and on price preview over new_total_ars, the whole selection after the coupon. Quote it as is; do not compute it yourself. It is null exactly when card_installments is null.
ars_installments Present on products, home product cards, departures, departure amounts and price preview, next to card_installments. It is null, or {"installments": 12, "amount": 1257511} on a package priced in dollars that the site sells as 12 cuotas en pesos. The customer pays it by buying giftcards in 12 installments, so amount is each installment in ARS: the cash-dollar price of that row at today's MEP rate plus the giftcard 12-installment surcharge, divided by 12 and rounded, the same number the website shows. It moves with the rate, so quote it as an estimate. It is an extra option: the package keeps its USD payment plan (num_of_payments, deposit_*) and its payment_options, so a consumer with an AI agent still sells the deposit and the USD installments as always. Only a consumer without an AI agent loses them, like the website, which does not sell these packages online (can_buy and can_deposit are false). There is no payment_option for it: the customer pays it by buying giftcards for that amount in 12 installments. It is also null when that row has no cash-dollar price.

Product departure responses include active future departures for one product. The root product_description carries the base package itinerary once. Each row's description_override is either a complete replacement for that departure or null when the row inherits the base description, so clients can compare activities without loading every departure detail. The root currency is the package base currency for the product. The filters object is calculated from every active departure of the product, even when results is paginated or narrowed with month and year; total counts the departures of the requested month. filters.months lists departure start months; each row omits month summaries because start_date and end_date carry the date range. Inventory fields distinguish confirmed flight seats from regular flight departures and no-flight departures.

JSONProduct departures
{
  "success": 1,
  "data": {
    "product_id": 2158,
    "product_description": "Day 1: city tour. Day 2: free day.",
    "currency": "USD",
    "total": 24,
    "filters": {
      "price": { "min": 5835, "max": 8720 },
      "stars": [5, 4],
      "months": ["2026-06", "2026-07"],
      "itinerary_days": [5, 8],
      "luggage": ["Carry on"],
      "route": ["DIRECT"]
    },
    "results": [
      {
        "id": 9876,
        "start_date": "2026-06-12",
        "end_date": "2026-06-16",
        "nights": 4,
        "itinerary_days": 5,
        "assistance_days": 5,
        "last_seats": 6,
        "available_prices_count": 1,
        "inventory_type": "allotment",
        "available_seats": 6,
        "price_from_ars": null,
        "price_from_usd": 5835,
        "deposit_from_ars": null,
        "deposit_from_usd": null,
        "num_of_payments": null,
        "final_payment_days_before": null,
        "card_installments": null,
        "card_installment_amount": null,
        "ars_installments": null,
        "city_from": "Buenos Aires",
        "is_hook": false,
        "hotel_stars": [5, 4],
        "luggage": ["Carry on"],
        "route": "DIRECT",
        "flexible": false,
        "description_override": "Day 1: city tour. Day 2: full-day excursion."
      }
    ]
  }
}
Field Why it matters to the end user
start_date, end_date, nights, itinerary_days These are raw travel dates and the passenger-facing trip duration. Format dates on the client side.
product_description, description_override Show the row's override when it is not null; otherwise show the root product description. The override replaces the base itinerary completely rather than adding to it. The base text is returned once to keep packages with many departures compact.
assistance_days This is the coverage duration used for assistance and ViajeFlex calculations, which can differ from passenger-facing itinerary days.
last_seats Use it to show remaining availability pressure when present. It is only populated for confirmed flights with few seats left.
available_prices_count, inventory_type, available_seats available_prices_count counts enabled price rows for the departure. inventory_type is allotment, regular, or no_flight. available_seats is the real remaining seat count only for confirmed flight inventory, otherwise null.
price_from_ars, price_from_usd, deposit_from_ars, deposit_from_usd These rounded integer money fields are always present; currencies that do not apply are null. The deposit_from_* fields are previews derived from the departure's lowest available price. Use departure detail to select the actual id_price.
final_payment_days_before How many days before start_date the whole trip must be paid when the customer pays in USD installments: equal ones (num_of_payments) or a deposit and the balance in installments (deposit_usd_installments of departure detail). It is already resolved: the departure's own deadline, or the package's. Quote it as is. It is null when the departure has neither plan, because then the full amount is paid up front; packages priced in ARS always return null. Departure detail returns the same value.
city_from, route, luggage, hotel_stars These fields help users compare departure quality before choosing one. hotel_stars is an aggregate of all available price options; precise hotel stars remain in detail at prices[].hotels[].stars.

Product departure detail responses include the selected departure price payload. The root currency is the package base currency for the product. description is authoritative for the selected departure: a departure-specific description replaces the base package description completely; only when the departure has no description does this field fall back to the product description. Never merge both descriptions. Deposit values are exposed on each selectable prices[].amounts[] row, not at the detail root. Use flight for flight details; it is null when the departure has no flight.

JSONProduct departure detail
{
  "success": 1,
  "data": {
    "product_id": 2158,
    "currency": "USD",
    "description": "<p>Departure details...</p>",
    "prices": [
      {
        "id": 4321,
        "amounts": [
          {
            "price_ars": null,
            "name": "amount_double",
            "price_usd": 990,
            "deposit_ars": null,
            "deposit_usd": 190,
            "num_of_payments": 5,
            "card_installments": null,
            "card_installment_amount": null,
            "ars_installments": null,
            "deposit_usd_installments": null,
            "max_pax": 12,
            "room_type": "DOUBLE",
            "has_kids": false
          }
        ],
        "payment_methods": [
          {
            "code": "TRANSFER",
            "title": "Transferencia o Depósito",
            "detail": "Podés abonarlo con transferencia o depósito bancario en pesos o dólares, a las cuentas disponibles de Deturista.\n\nEn el caso del pago en pesos, debe abonarse al tipo de cambio MEP del día de pago.",
            "handled_by": "agent"
          },
          {
            "code": "USD_INSTALLMENTS",
            "title": "Cuotas en dólares",
            "detail": "Abonalo en hasta 5 cuotas fijas de USD 190 por persona. La primera cuota corresponde a la seña y podés abonarla en dólares o en pesos a través de las plataformas de cobro disponibles.\n\nEl total deberá quedar abonado 30 días antes de la salida.",
            "handled_by": "agent"
          },
          {
            "code": "CASH",
            "title": "Pago Efectivo",
            "detail": "Podrás abonarlo en efectivo en cualquiera de nuestras sucursales.",
            "handled_by": "agent"
          }
        ],
        "hotels": [
          {
            "id": 10,
            "name": "Hotel Plaza",
            "stars": 4,
            "destination": "Madrid",
            "image": "hotel.jpg",
            "nights": 3,
            "check_in_date": "2026-06-13",
            "check_out_date": "2026-06-16",
            "meal_plan": {
              "code": "BREAKFAST",
              "label": "Con Desayuno"
            }
          }
        ],
        "circuits": [
          {
            "id": 20,
            "name": "Circuito Andalucia",
            "nights": 4,
            "start_date": "2026-06-16",
            "end_date": "2026-06-20",
            "meal_plan": null
          }
        ],
        "transfers": [
          {
            "id": 30,
            "name": "Traslado aeropuerto-hotel"
          }
        ],
        "count": 1,
        "price_from_ars": null,
        "price_from_usd": 990,
        "price_from_type": "DOUBLE"
      }
    ],
    "has_amounts": true,
    "price_from_type": "DOUBLE",
    "discount": null,
    "kids_age": 12,
    "start_date": "2026-06-12",
    "end_date": "2026-06-20",
    "nights": 7,
    "itinerary_days": 8,
    "assistance_days": 9,
    "final_payment_days_before": 30,
    "payment_methods": [
      {
        "code": "TRANSFER",
        "title": "Transferencia o Depósito",
        "detail": "Podés abonarlo con transferencia o depósito bancario en pesos o dólares, a las cuentas disponibles de Deturista.\n\nEn el caso del pago en pesos, debe abonarse al tipo de cambio MEP del día de pago.",
        "handled_by": "agent"
      },
      {
        "code": "USD_INSTALLMENTS",
        "title": "Cuotas en dólares",
        "detail": "Abonalo en hasta 5 cuotas fijas en dólares. La primera cuota corresponde a la seña y podés abonarla en dólares o en pesos a través de las plataformas de cobro disponibles.\n\nEl total deberá quedar abonado 30 días antes de la salida.",
        "handled_by": "agent"
      },
      {
        "code": "CASH",
        "title": "Pago Efectivo",
        "detail": "Podrás abonarlo en efectivo en cualquiera de nuestras sucursales.",
        "handled_by": "agent"
      }
    ],
    "flight": {
      "inventory_type": "ALLOTMENT",
      "available_seats": 6,
      "last_seats": 6,
      "route": "DIRECT",
      "luggage": ["CARRY_ON", "CHECKED"],
      "departure_date": "2026-06-12",
      "arrival_date": "2026-06-20",
      "flight_legs": [
        {
          "leg_type": "OUTBOUND",
          "airport_from": "EZE",
          "city_from": "Buenos Aires",
          "airport_to": "MIA",
          "city_to": "Miami",
          "airline_name": "Aerolinea",
          "flight_number": "AR1302",
          "departure_date": "2026-06-12 22:00:00",
          "arrival_date": "2026-06-13 06:00:00"
        }
      ]
    },
    "id_departure": 9876
  }
}
Field Rule for contact or reservation creation
prices[].id Send one of these values as id_price. Do not reuse a price id from another departure.
amounts[].room_type, prices[].price_from_type, price_from_type Room types use SINGLE, DOUBLE, DOUBLE_SHARED, TRIPLE, QUADRUPLE, SEXTUPLE, FAMILY_1, or FAMILY_2. Lowercase an amounts[].room_type to obtain the matching rooms key: DOUBLE maps to {"double":1} and FAMILY_1 maps to {"family_1":1}.
amounts[].price_ars, amounts[].price_usd Price per passenger in that room type, not the price of the room. A DOUBLE holds two passengers, so one double room costs twice this value; SINGLE and DOUBLE_SHARED hold one, TRIPLE and FAMILY_1 three, QUADRUPLE and FAMILY_2 four, SEXTUPLE six. Call price-preview to get the total of a room selection instead of multiplying.
amounts[].deposit_ars, amounts[].deposit_usd, amounts[].num_of_payments These are the calculated per-passenger installment and installment count for the selected room and departure date: the plan is num_of_payments equal installments of deposit_*, the first one being the deposit. Use the suffix that matches the selected money field. For the installment of a whole room selection call price-preview. Do not send deposit totals in the reservation request; the reservation endpoint recomputes and persists them server-side.
final_payment_days_before How many days before start_date the whole trip must be paid when the customer pays in USD installments, equal ones (num_of_payments) or a deposit and the balance in installments (deposit_usd_installments): the departure's own deadline, or the package's. Quote it as is. It is null when no amounts[] has num_of_payments and payment_methods has no DEPOSIT_USD_INSTALLMENTS, because then the full amount is paid up front; packages priced in ARS always return null.
payment_methods, prices[].payment_methods The "Formas de pago" lists the website shows, in Spanish: code, title, detail (the text for the customer, values included, in plain text with its paragraphs separated by a blank line: \n\n) and handled_by. The root list is the departure summary, without amounts: which ways to pay apply, how many installments and how many days before departure to pay them off. Each prices[].payment_methods repeats it with that option's amounts per passenger, for the room in prices[].price_from_type, the one the website highlights (almost always DOUBLE): name that room when you quote them, as the website does ("Valores por persona en base doble"). For the rooms the customer chose, quote payment_methods from price-preview instead. When handled_by is advisor, explain the option with its text and hand the conversation over to an advisor. Payment texts inside description are not authoritative. The lists are empty outside site 1, and an option with no amounts has none.
amounts[].deposit_usd_installments When this object is not null (DEPOSIT_USD_INSTALLMENTS in payment_methods), the customer pays a deposit first (deposit, in USD per passenger) and the rest in installments equal installments of installment_amount: the same numbers prices[].payment_methods quotes. It replaces the equal installments, so num_of_payments and deposit_* are null. An advisor closes it; reservations do not build this plan.
has_amounts true when at least one price has a selectable room in amounts. When it is false, every prices[].amounts list is empty.
amounts[].has_kids Read this as a JSON boolean. For family rooms, use the product's kids_age as the exclusive child-age limit at trip end.
flight, prices[].hotels, prices[].circuits, prices[].transfers, viajeflex_options Present these before checkout so the user confirms the operational travel details and included services attached to the selected departure.
flight.flight_legs[].departure_date, flight.flight_legs[].arrival_date Each leg includes its own departure and arrival date and time in one value: YYYY-MM-DD HH:MM:SS, with no UTC offset. Use these values for flight schedules, including connections and overnight arrivals. The summary fields flight.departure_date and flight.arrival_date remain plain dates (YYYY-MM-DD).
product.has_assistance Use this product boolean to tell the user whether the package includes travel assistance. Assistance selection and accounting details are internal and are not exposed by departure prices.

Product departure contact responses return the same contact receipt as /contacts. The persisted contact uses the submitted product, departure, price, and room selection, with channel set to PUBLIC_API. The id_seller field is the assigned seller id, or null when the lead is not assigned.

JSONProduct departure contact
{
  "success": 1,
  "data": {
    "id_contact": 654321,
    "id_seller": 103,
    "creation_date": "2026-06-03 18:59:05",
    "status": "PENDING",
    "channel": "PUBLIC_API",
    "id_product": 2158,
    "id_departure": 9876,
    "id_price": 4321
  }
}

Product departure reservation responses return a reservation receipt. The persisted reservation uses authenticated API ownership, with channel and origin set to PUBLIC_API. Transfer reservations are created in the regular reservation flow. MercadoPago reservations are created as PENDING_PAYMENT and include a Checkout Pro redirect under payment.url.

JSONProduct departure reservation
{
  "success": 1,
  "data": {
    "id_reservation": 765432,
    "creation_date": "2026-06-03 19:11:02",
    "status": "IN_PROCESS",
    "channel": "PUBLIC_API",
    "origin": "PUBLIC_API",
    "id_product": 2158,
    "id_departure": 9876,
    "id_price": 4321,
    "payment_option": "buy",
    "payment_selection": "TRANSFER",
    "price_usd": 200,
    "deposit_usd": null,
    "currency_to": "USD"
  }
}
JSONOnline payment reservation (MercadoPago)
{
  "success": 1,
  "data": {
    "id_reservation": 765433,
    "creation_date": "2026-06-03 19:14:10",
    "status": "PENDING_PAYMENT",
    "channel": "PUBLIC_API",
    "origin": "PUBLIC_API",
    "id_product": 2158,
    "id_departure": 9876,
    "id_price": 4321,
    "payment_option": "deposit",
    "payment_selection": "MERCADOPAGO",
    "price_usd": 200,
    "deposit_usd": 50,
    "currency_to": "USD",
    "payment": {
      "type": "redirect",
      "provider": "MERCADOPAGO",
      "url": "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=123"
    }
  }
}

With Getnet charging reservations, payment_selection and payment.provider are GETNET and payment.url is the Getnet checkout.

Contact receipt lookup returns the same data fields for contacts owned by the authenticated API consumer. The status field is the current contact workflow status and can change after creation.

JSONContact receipt
{
  "success": 1,
  "data": {
    "id_contact": 654321,
    "id_seller": 103,
    "creation_date": "2026-06-03 18:59:05",
    "status": "FOLLOW",
    "channel": "PUBLIC_API",
    "id_product": 2158,
    "id_departure": 9876,
    "id_price": 4321
  }
}

Reservation receipt lookup returns the persisted reservation receipt for reservations owned by the authenticated API consumer. The status field is the current reservation workflow status and can change after creation.

JSONReservation receipt
{
  "success": 1,
  "data": {
    "id_reservation": 765432,
    "creation_date": "2026-06-03 19:11:02",
    "status": "DEBT",
    "channel": "PUBLIC_API",
    "origin": "PUBLIC_API",
    "id_product": 2158,
    "id_departure": 9876,
    "payment_selection": "TRANSFER",
    "price_usd": 200,
    "deposit_usd": null,
    "currency_to": "USD"
  }
}

Destination listing responses include active destinations, their parent summary, and direct child summaries. Destination detail responses use the same fields, but data is the destination object directly.

JSONDestinations
{
  "success": 1,
  "data": {
    "total": 3,
    "results": [
      {
        "id": 410,
        "name": "Miami",
        "description": "Miami",
        "parent_id": 23,
        "url": "america-10/estados_unidos-23/miami-410",
        "image": "miami.jpg",
        "active": true,
        "parent": { "id": 23, "name": "Estados Unidos" },
        "children": []
      }
    ]
  }
}

Category listing responses include website-visible categories, their parent summary, and direct child summaries. The active field describes menu visibility state; disabled categories and categories under disabled parents are not returned. Category detail responses use the same fields, but data is the category object directly.

JSONCategories
{
  "success": 1,
  "data": {
    "total": 3,
    "results": [
      {
        "id": 154,
        "name": "Mundial",
        "parent_id": null,
        "url": "mundial-154",
        "image": "mundial.jpg",
        "active": true,
        "parent": null,
        "children": [
          { "id": 155, "name": "Final" }
        ]
      }
    ]
  }
}

Product group responses include the selected dimension, grouping level, product totals, drill-down URLs, and optional compact top-ranked products. In each top-ranked product, currency is the package base currency.

JSONProduct groups
{
  "success": 1,
  "data": {
    "by": "destination",
    "group": "continent",
    "groups": [
      {
        "id": 10,
        "name": "Europe",
        "parent_id": null,
        "url": "europe-10",
        "total": 42,
        "products_url": "/api/v1/products?id_destination=10",
        "top_ranked": [
          {
            "id": 1001,
            "title": "Madrid escapada",
            "url": "1001/madrid_escapada.html",
            "image": "madrid.jpg",
            "price_from_usd": 1200,
            "currency": "USD"
          }
        ]
      }
    ]
  }
}

Home responses return one section. A block section (slider/list/popup) returns blocks; ?type=banners returns banners. Each block carries its display metadata plus a resource; product resources and LIST products are compact home cards using the same money/flag conventions as /products, while category and destination resources are compact tile summaries. The example below shows a type=list response.

JSONHome list section (type=list)
{
  "success": 1,
  "data": {
    "blocks": [
      {
        "id": 1356,
        "type": "list",
        "order": 1,
        "link": true,
        "show_text": true,
        "image_name": null,
        "resource_type": "category",
        "resource": {
          "id": 126,
          "name": "Playas",
          "url": "playas-126",
          "image": "playas.jpg"
        },
        "products": [
          {
            "id": 1684,
            "title": "Paquete a Playas de Rep. Dominicana",
            "url": "1684/paquete_rep_dominicana.html",
            "image": "punta_cana.jpg",
            "discount": 20,
            "transport": "Aéreos",
            "meal_plan": "All Inclusive",
            "nights": [7, 8, 9, 10],
            "has_hotel": true,
            "has_transfer": false,
            "has_assistance": false,
            "has_tour": false,
            "price_from_ars": 1903828,
            "price_from_usd": 1200,
            "deposit_from_ars": null,
            "deposit_from_usd": null,
            "num_of_payments": null
          }
        ]
      }
    ]
  }
}

The ?type=banners section returns the banner strip under banners instead of blocks.

JSONHome banners section (type=banners)
{
  "success": 1,
  "data": {
    "banners": [
      {
        "name": "banner_mundial.png",
        "description": "Mundial",
        "link": "https://www.deturista.com/categoria/mundial-154"
      }
    ]
  }
}

Site responses return the site display fields, social links, legal text, and the public office list. Most fields are nullable, so null-check before rendering.

JSONSite metadata
{
  "success": 1,
  "data": {
    "id_site": 1,
    "name": "deTurista.com",
    "site_name": "Argentina",
    "phone": "0810-333-8748",
    "email": "info@deturista.com",
    "working_hours": "Lunes a Viernes de 9 a 18 hs",
    "address": "Av. Córdoba 1801/07, CABA, Buenos Aires",
    "address_link": "https://maps.app.goo.gl/example",
    "address2": null,
    "address2_link": null,
    "legal_info": "Razon social: deTurista.com de Aterrizando S.A - Leg. 16952",
    "social": {
      "facebook": "https://facebook.com/deturista",
      "twitter": null,
      "instagram": "https://instagram.com/deturista"
    },
    "offices": [
      {
        "id": 1,
        "label": "Pilar",
        "address": "Colectora Panamericana (Km 42.5) y Las Camelias, Pilar",
        "working_hours": null,
        "map_url": "https://www.google.com/maps/place/deTurista.com",
        "image": "pilar_.png"
      }
    ]
  }
}

Newsletter creation returns 201 with the subscription receipt.

JSONNewsletter receipt
{
  "success": 1,
  "data": {
    "id_newsletter": 472238,
    "email": "ana@example.com",
    "destination": "EUROPA",
    "creation_date": "2026-07-21 11:42:29"
  }
}

Working with the data

These conventions hold across every response, so handle them once instead of per endpoint.

Topic What to do
Dates and times Timestamps such as creation_date and flight leg departure_date / arrival_date are in the API account's timezone, formatted YYYY-MM-DD HH:MM:SS. They are not ISO-8601 and carry no UTC offset, so do not assume one when parsing. Plain dates such as start_date, end_date, hotel check-in/check-out dates, and passenger birth_date are YYYY-MM-DD.
Nullable fields Many non-money fields can be null or absent — for example subtitle, image, discount, route and luggage, and parent objects. Public money pairs are always present, with the unavailable currency set to null.
Money numbers Catalog and reservation money fields (price_from_ars, price_from_usd, deposit_from_ars, deposit_from_usd, price_ars, price_usd, deposit_ars, deposit_usd) are emitted as ARS/USD pairs. Values are rounded integer JSON numbers without currency symbols, thousands separators, or decimals; an unavailable currency is null. ars_installments.amount and card_installment_amount follow the same number rules but only exist in pesos, so they have no pair; the amounts of deposit_usd_installments only exist in USD, so they have none either. Product currency and departure data.currency name the package base currency. Saved TC trips instead use decimal strings in totals.plan currency maps and manual selling amount. Format amounts for display on the client side.
Per passenger vs. total Prices and deposits on products, departures and departure detail (price_from_*, deposit_from_*, amounts[].price_*, amounts[].deposit_*) are per passenger. Totals for the selected rooms come only from price-preview (total_*, new_total_*, deposit_*) and from reservation receipts.
Deposit amounts Preview fields named deposit_from_* are derived from the lowest available child price. The selectable per-passenger deposit values are amounts[].deposit_ars and amounts[].deposit_usd in departure detail; the deposit of a whole room selection is deposit_ars / deposit_usd in price-preview. num_of_payments is calculated server-side from the departure payment plan, including payment visibility, departure date, final-payment deadline, and increase rules. The create reservation endpoint never accepts client-supplied deposit totals; it recomputes them from the selected price and rooms.
Booleans drive the UI Flags such as can_buy, effective can_deposit, and the product has_* flags are real JSON booleans (true / false), not 0 / 1. Gate buttons on them; do not infer them.

Error shape

JSONForbidden
{
  "success": 0,
  "error": "forbidden",
  "message": "insufficient scope"
}
JSONUnauthorized
{
  "success": 0,
  "error": "unauthorized",
  "message": "missing or invalid API key"
}
JSONInvalid request
{
  "success": 0,
  "error": "invalid_request",
  "message": "month must be between 1 and 12"
}
JSONNot found
{
  "success": 0,
  "error": "not_found",
  "message": "price not found"
}

For contact and reservation receipt lookup, 404 not_found also means the id is not owned by the authenticated API consumer.

JSONRate limited
{
  "success": 0,
  "error": "rate_limited",
  "message": "rate limit exceeded"
}
Requests are rate limited per API key. When you exceed your limit you get HTTP 429 with error: "rate_limited"; pause and retry with exponential backoff instead of hammering the endpoint. The exact limit is configured per consumer, so design clients to react to a 429 rather than assuming a fixed rate.

Every error carries a message. For invalid_request and not_found it is specific so you can fix the call (it names the offending field or the missing resource). For unauthorized, forbidden and rate_limited it is intentionally generic and does not reveal configured credentials or grants. Do not parse it; switch on the error code instead.

Swagger and OpenAPI schema

Swagger UI is bundled with this beta documentation and loads the local openapi.json schema. Use the Swagger Authorize button to enter your own X-API-Key; the docs do not include prototype credentials.

Beta and versioning

The contract is versioned under /v1. During beta it evolves additively: new fields and endpoints can appear without notice, but an existing /v1 field will not be removed or repurposed without a new version. Build defensively — read the fields you need and ignore any you do not recognise — so an additive change never breaks your integration.

During beta, treat Swagger as a reference and call the API from your server. Swagger's Try it out runs from your browser against the selected server; when this page is opened from test.deturista.com, Swagger selects the development host by default. On a local development machine, Swagger selects the current app that served the page. Check the selected server before executing a request. Browser calls may still be blocked unless the selected API host allows your environment. The curl examples in this guide always work from a terminal or backend.