Versão do contrato: 1.0.0. Atualizado em 10/10/2026.
Integre o sistema da agência para cadastrar viajantes, criar viagens com roteiro, associar pessoas e enviar documentos. Nesta versão, a integração está disponível somente em produção; não há ambiente público de testes. As operações de escrita criam ou alteram registros na conta da agência e podem enviar convites reais.
| Ambiente | URL base, sem /api/v1 | Uso |
|---|---|---|
| Produção | https://api.tripmap.com.br | Operação da agência quando a integração estiver disponível no painel |
As rotas abaixo já incluem /api/v1. Exemplo: URL base + /api/v1/viajantes = https://api.tripmap.com.br/api/v1/viajantes. Não acrescente /api/v1 à variável da base. Se a aba API não estiver disponível no painel de produção, acione o suporte antes de iniciar.
A chave de integração fica no servidor da agência. O viajante entra no app por e-mail e código, usando a autenticação própria do app.
Acesso e chaves
Todos os planos com assinatura ativa e trials dentro da validade permitem integração. Assinatura ausente, trial vencido, inadimplência, cancelamento e agência suspensa impedem o acesso.
- Entre no painel de produção como proprietário ativo da agência.
- Abra Configurações → API, na seção Chaves da API.
- Informe um nome para identificar o sistema, por exemplo
Meu CRM, e clique em Criar chave. O nome aceita de 1 a 40 caracteres. - Em Guarde esta chave, clique em Copiar chave e salve o segredo em uma variável de ambiente no servidor integrador. A chave completa aparece somente nessa criação; se perder a cópia, crie outra chave.
- Valide a autenticação com uma consulta de leitura usando a URL base de produção e
Authorization: Bearer <SUA_CHAVE>.
O integrador usa somente a chave tm_live_...; não precisa receber a senha nem a sessão do painel do proprietário. Somente o proprietário ativo cria, lista e revoga chaves. Há no máximo cinco chaves ativas por agência. O segredo nunca aparece na listagem. Desativar o criador ou movê-lo para outra agência revoga suas chaves permanentemente; reativar o membro não reativa chaves. Excluir o criador invalida o acesso.
Nesta versão, a chave permite todas as rotas deste contrato na própria agência e não expira automaticamente. Para rotacionar, crie uma chave nova, troque-a no servidor integrador, confirme a consulta de teste e clique em Revogar na chave anterior. A revogação impede novas chamadas imediatamente. Não coloque o segredo na URL, no app, no código executado no navegador, nos logs nem no Git.
O guia da API fica em https://api.tripmap.com.br/api/docs/agency, com download em https://api.tripmap.com.br/api/docs/agency.md; ambos são públicos. Eles apresentam o mesmo contrato da documentação da landing. Leia Limites e erros se a primeira consulta retornar 401, 403 ou 429.
Rotas
GET individual e recibos de POST/PATCH de viajante incluem também phone_alt, nationality, cpf, passport_number, passport_expiry, birth_date, seat_preference, dietary_restrictions e notes. Viagem inclui description, notes, pax_count, cover_image_url, tags e status_manual. Listas permanecem resumidas. O status retornado é efetivo na data de Brasília, preservando estados manuais/cancelados.
PATCH de viagem conserva a escolha de status manual feita no dashboard. Alterar datas valida dias e intervalos dos itens e rejeita atomicamente um roteiro incompatível; não encurta hospedagens ou transportes automaticamente.
DELETE de viajante associado por grupo retorna 409 group_membership_conflict, sem alterar os vínculos. Ajuste o grupo no dashboard antes de repetir a desvinculação. Convites reconhecem passageiros vinculados por grupo; convite explícito para cliente que não pertence à viagem retorna 404.
:id e :travelerId aceitam o UUID retornado pelo TripMap ou o external_id. Um external ID com aparência de UUID também funciona: a busca usa o UUID primeiro e, se não existir na agência, busca o external ID. :documentoId é sempre o UUID do documento. Ao montar URLs, use encodeURIComponent(id) para cada ID e URLSearchParams para filtros; caracteres como /, espaço, ? e # não devem ser concatenados diretamente.
| Método | Rota | Resultado |
|---|---|---|
| POST | /api/v1/viajantes | Cria ou atualiza o viajante pelo e-mail/external ID |
| GET | /api/v1/viajantes | Lista; filtros email, external_id |
| GET / PATCH | /api/v1/viajantes/:id | Consulta / atualização parcial |
| POST | /api/v1/viagens | Cria viagem, viajante opcional e roteiro em uma transação |
| GET | /api/v1/viagens | Lista; filtro external_id |
| GET / PATCH | /api/v1/viagens/:id | Consulta com roteiro / atualização de dados da viagem |
| POST | /api/v1/viagens/:id/viajantes/:travelerId | Vincula um viajante existente |
| DELETE | /api/v1/viagens/:id/viajantes/:travelerId | Desvincula; 204 |
| POST | /api/v1/viagens/:id/viajantes/:travelerId/convite | Tenta enviar um convite pendente |
| POST / GET | /api/v1/viagens/:id/documentos | Upload / lista |
| POST / GET | /api/v1/viajantes/:id/documentos | Upload / lista |
| GET | /api/v1/viagens/:id/documentos/:documentoId/download | URL assinada, válida por 300 segundos |
| GET | /api/v1/viajantes/:id/documentos/:documentoId/download | URL assinada, válida por 300 segundos |
| DELETE | /api/v1/viagens/:id/documentos/:documentoId | Exclusão lógica; 204 |
| DELETE | /api/v1/viajantes/:id/documentos/:documentoId | Exclusão lógica; 204 |
Todas as listas aceitam limit entre 1 e 100 (padrão 100) e offset inteiro entre 0 e 1.000.000 (padrão 0). A resposta inclui pagination: {limit, offset, next_offset}; next_offset: null indica o fim. Ordenação por criação e UUID, ambos descendentes. Alterações durante a paginação podem deslocar resultados; deduplique pelo UUID retornado. Não há filtro incremental por data nem webhook de alterações nesta versão.
Escritas, identidade e datas
JSON deve ser um objeto, nunca null ou um array. Datas precisam existir no calendário e usar YYYY-MM-DD. Horários precisam de fuso explícito, por exemplo 2099-11-02T10:00:00-03:00. Strings acima do limite são rejeitadas, nunca truncadas.
external_id tem até 120 caracteres e é único por agência em cada tipo de recurso. Repetir POST da viagem existente retorna 200 com a viagem e o viajante persistidos, sem substituir o roteiro. Criação retorna 201. Chamadas simultâneas com o mesmo external ID criam somente uma viagem.
Viajante: name (até 200) e email (até 320) são obrigatórios no POST. O e-mail é normalizado para minúsculas e os textos têm espaços externos removidos. Repetir o e-mail na mesma agência atualiza apenas campos enviados. Omissão preserva dados; null limpa os campos pessoais opcionais listados abaixo, com exceção de external_id. Nome/e-mail não podem ser apagados. Identificadores que apontem para pessoas diferentes retornam 409 identity_conflict. Use PATCH para trocar o e-mail do mesmo viajante. Um external ID atribuído não pode ser trocado nem apagado com null; guarde a relação com o ID do seu sistema.
POST/PATCH de viajante aceitam name, email, phone, phone_alt, birth_date, nationality, cpf, passport_number, passport_expiry, seat_preference, dietary_restrictions, notes e external_id. Telefones até 40 caracteres; CPF até 20; nacionalidade até 80; passaporte/preferência até 40; restrições até 200; notas até 2000.
Trocar o e-mail remove o vínculo da conta anterior e reavalia o acesso pela conta do novo e-mail. Os convites ficam pendentes para o novo endereço. Uma troca durante envio ainda sem confirmação retorna 409 invite_in_progress; consulte a seção Convite e acesso ao app.
Viagem: external_id, title (até 200), start_date e end_date são obrigatórios no POST. O fim não pode anteceder o início. Campos opcionais: description e notes (até 4000), pax_count (inteiro de 1 a 10000, padrão 1), cover_image_url (HTTPS, até 500), tags (até 20 strings de 80 caracteres), traveler e days.
PATCH da viagem aceita somente title, start_date, end_date, description, notes, pax_count, cover_image_url e tags. O corpo deve conter ao menos um campo permitido; campo desconhecido é rejeitado. Omissão preserva o campo. description, notes e cover_image_url aceitam null para limpar. Para limpar tags, use tags: []; tags: null, título/datas nulos e pax_count: null são rejeitados. Não substitui roteiro; não aceita days. Datas novas devem continuar abrangendo todos os dias e intervalos dos itens já gravados. No POST, traveler, days, tags e items devem ser omitidos quando ausentes, e não enviados como null. Campos desconhecidos no POST e no POST/PATCH de viajante não são gravados; use apenas os campos deste contrato.
O status é calculado pelas datas em Brasília: futura confirmed, nas datas da viagem ongoing, depois completed. A integração não congela o status manualmente. Uma operação que coloque o viajante em duas viagens em andamento, diretamente ou por grupo, causa 409 traveler_already_on_ongoing_trip, inclusive sob concorrência com o dashboard.
Também são rejeitados períodos automáticos sobrepostos do mesmo viajante ativo, de hoje em diante: 409 traveler_trip_period_conflict. Viagens de viajantes distintos podem ter as mesmas datas. Início e fim são inclusivos; a próxima viagem do mesmo viajante deve começar depois do último dia da anterior. A regra considera o principal e os vínculos individuais ou por grupo, inclusive em alterações feitas pelo dashboard. Viagens futuras com períodos distintos continuam permitidas enquanto o viajante está em uma viagem atual. Viagens canceladas ou com status manual confirmed, completed ou draft não reservam período. Status manual ongoing reserva de hoje em diante sem fim automático; encerre esse status no dashboard antes de associar outra viagem futura. Sobreposições históricas não são apagadas nem canceladas automaticamente; alterações descritivas continuam possíveis, mas novos vínculos ou mudanças de período precisam respeitar a regra.
GET da viagem inclui o viajante principal em travelers, mesmo sem vínculo individual separado, e evita duplicá-lo quando esse vínculo também existe. Referências a grupos permanecem no formato group_id. Os dias vêm por day_number e UUID crescentes. Dentro de cada dia, os itens vêm por sort_order e UUID crescentes, refletindo a ordem editada no dashboard.
Roteiro e voos
days aceita até 366 dias em ordem crescente, sem datas repetidas e dentro do intervalo da viagem. Cada dia aceita date, title (200), summary (1000) e items. Máximo de 80 itens por viagem.
Tipos: flight, accommodation, activity, car_rental, transfer, land_transport, sea_transport, insurance, restaurant e note. Todo item exige title (200), exceto voo. Ordem é a ordem do array; os itens são gravados confirmados e em BRL.
Campos de itens comuns: start_at, end_at, address (300), origin (300), destination (300), notes (2000), confirmation_number (80). Horários devem estar dentro do intervalo da viagem. Na hospedagem são check-in/check-out; no transporte, partida/chegada; no aluguel, retirada/devolução. O texto local do ISO é usado nos cartões; os instantes persistidos preservam UTC.
Atividades com start_at devem começar na data local de days[].date. O fim pode atravessar a meia-noite, desde que continue dentro do período da viagem. Hospedagens podem atravessar dias e não exigem check-in no dia do bloco. Para voos, a partida e a chegada locais retornadas pelo provedor também precisam estar dentro do período; uma chegada fora dele retorna 400 antes de gravar a viagem.
Voo exige flight_number (2 a 10 letras/números, exemplo G31442), date (padrão: dia do roteiro) e origin opcional (IATA, três letras). A consulta ao provedor preenche aeroportos, companhia, horários, terminal e portão. 404 flight_not_found indica consulta sem resultados ou 404 do provedor; 409 flight_ambiguous: reenvie com o aeroporto de origem correto. Falhas de rede, timeout de 12 segundos, resposta inválida, quota ou erro do provedor retornam 503 flight_lookup_unavailable, com Retry-After: 30 e retry_after: 30. Espere e repita com o mesmo external ID; não troque o número do voo só por esse erro. Falha de busca não deixa viagem, dia ou viajante parcialmente gravados.
passengers aceita até 200 objetos: name (obrigatório, 200), client_id (UUID do TripMap opcional), email (320), seat (10), cabin (40), baggage (80), pnr (20), ticket_number (40). Um client_id deve pertencer a um viajante ativo da agência; se também enviar e-mail, ambos devem apontar para a mesma pessoa. Sem ID, o e-mail resolve qualquer viajante ativo da própria agência, mesmo antes de vinculá-lo à viagem. Se ainda não estiver cadastrado, os dados do passageiro serão associados quando a pessoa for cadastrada e vinculada, individualmente ou por grupo. Atribuição ao voo não cria vínculo nem convite automaticamente. Sem e-mail/ID, só o nome do principal explicitamente informado na criação pode ser associado; outros homônimos não são inferidos. O app usa os dados para assento, cabine, bagagem e bilhete. Se um passageiro antigo estiver associado incorretamente, ajuste-o explicitamente no dashboard.
Exemplo:
{
"external_id": "crm-8841",
"title": "Lisboa com a Ana",
"start_date": "2099-11-02",
"end_date": "2099-11-08",
"traveler": {"external_id": "cli-19", "name": "Ana Souza", "email": "ana@example.com"},
"days": [{
"date": "2099-11-02",
"title": "Chegada",
"items": [{"type": "transfer", "title": "Aeroporto ao hotel", "origin": "Aeroporto", "destination": "Hotel", "start_at": "2099-11-02T15:00:00+00:00"}]
}]
}Exemplo completo de integração
Use Node.js 20 ou superior no servidor da agência. Configure TRIPMAP_API_BASE=https://api.tripmap.com.br, sem /api/v1, e TRIPMAP_API_KEY com uma chave válida de produção. O exemplo é um módulo ES (.mjs). Configure TRIPMAP_TIMEOUT_MS se precisar de mais tempo para roteiros com várias consultas de voo. Os dados abaixo são fictícios; use um PDF válido e substitua o e-mail por um endereço controlado. Executar o exemplo cria registros reais na agência; a criação com viajante pode tentar enviar convite.
import { readFile } from "node:fs/promises";
const base = process.env.TRIPMAP_API_BASE?.replace(/\/$/, "");
const key = process.env.TRIPMAP_API_KEY;
if (!base || !key) throw new Error("Configure a base e a chave no servidor.");
if (base !== "https://api.tripmap.com.br") {
throw new Error("A base deve ser a origem HTTPS de produção, sem /api/v1.");
}
const timeout = Number(process.env.TRIPMAP_TIMEOUT_MS ?? 120000);
if (!Number.isSafeInteger(timeout) || timeout <= 0) throw new Error("Timeout inválido.");
async function call(path, { method = "GET", json, form } = {}) {
const response = await fetch(base + path, {
method,
signal: AbortSignal.timeout(timeout),
headers: {
Authorization: "Bearer " + key,
...(json === undefined ? {} : { "Content-Type": "application/json" })
},
body: form ?? (json === undefined ? undefined : JSON.stringify(json))
});
const raw = await response.text();
let data = null;
if (raw) {
try { data = JSON.parse(raw); }
catch { throw new Error("Resposta inesperada; confirme o ambiente e acione o suporte."); }
}
if (!response.ok) {
const error = new Error(data?.error ?? "request_failed");
error.status = response.status;
error.field = data?.field;
error.retryAfter = response.headers.get("Retry-After") ?? data?.retry_after;
throw error; // Trate os erros; não registre chave nem dados pessoais.
}
return { status: response.status, data };
}
// Primeira consulta: confirma a chave e o ambiente antes de qualquer escrita.
await call("/api/v1/viajantes?limit=1");
// 201 na primeira criação; 200 quando o e-mail/external ID já existe.
const saved = await call("/api/v1/viajantes", {
method: "POST",
json: { external_id: "cli-19", name: "Ana Souza", email: "ana@example.com" }
});
const traveler = saved.data.traveler;
await call("/api/v1/viajantes/" + encodeURIComponent(traveler.id), {
method: "PATCH", json: { phone: "+55 11 90000-0000" }
});
// 201 na primeira criação; replay retorna 200 sem substituir o roteiro.
const created = await call("/api/v1/viagens", {
method: "POST",
json: {
external_id: "crm-8841",
title: "Lisboa com a Ana",
start_date: "2099-11-02",
end_date: "2099-11-08",
traveler: { external_id: "cli-19", name: "Ana Souza", email: "ana@example.com" },
days: [{
date: "2099-11-02",
title: "Chegada",
items: [{
type: "activity", title: "Passeio",
start_at: "2099-11-02T15:00:00+00:00"
}]
}]
}
});
const trip = created.data.trip;
// Confira created.data.traveler.invite: a gravação não garante entrega do e-mail.
const detail = await call("/api/v1/viagens/" + encodeURIComponent(trip.id));
// detail.data.trip.days[].items[] usa start_datetime/end_datetime no retorno,
// enquanto a escrita usa start_at/end_at. Metadata preserva a data/hora local.
const file = new FormData();
file.set("external_id", "voucher-19");
file.set("type", "tour_voucher");
file.set("title", "Voucher do passeio");
file.set("file", new Blob([await readFile("./voucher.pdf")], {
type: "application/pdf"
}), "voucher.pdf");
// Não defina Content-Type manualmente: fetch inclui o boundary multipart.
const uploaded = await call("/api/v1/viagens/" + encodeURIComponent(trip.id) + "/documentos", {
method: "POST", form: file
});
const document = uploaded.data.document;
const downloaded = await call(
"/api/v1/viagens/" + encodeURIComponent(trip.id) + "/documentos/" + encodeURIComponent(document.id) + "/download"
);
// downloaded.data = { url, expires_in }; use a URL imediatamente e em privado.
// Paginação: travelers/trips/documents são as chaves de coleção de cada rota.
let offset = 0;
do {
const query = new URLSearchParams({ limit: "100", offset: String(offset) });
const result = await call("/api/v1/viajantes?" + query);
for (const person of result.data.travelers) {
// Integre pelo person.id sem registrar os dados pessoais em logs.
}
offset = result.data.pagination.next_offset;
} while (offset !== null);Exemplo de resposta de POST de viajante, HTTP 201 (ou 200 no replay):
{
"traveler": {
"id": "11111111-1111-4111-8111-111111111111",
"external_id": "cli-19",
"name": "Ana Souza",
"email": "ana@example.com",
"phone": null,
"phone_alt": null,
"nationality": null,
"cpf": null,
"passport_number": null,
"seat_preference": null,
"dietary_restrictions": null,
"notes": null,
"birth_date": null,
"passport_expiry": null
}
}Respostas e campos retornados
| Operação | Envelope | HTTP de sucesso |
|---|---|---|
| POST de viajante | {traveler} | 201 novo; 200 existente |
| GET/PATCH de viajante | {traveler} | 200 |
| POST de viagem | {trip, traveler} | 201 nova; 200 replay |
| GET/PATCH de viagem | {trip} | 200 |
| POST de vínculo | {linked: true} | 200 |
| POST de convite | {invite, error?, retry_after?} | 200 concluído; 202 pendente |
| POST de documento | {document} | 201 novo; 200 replay |
| Listas | {travelers, pagination}, {trips, pagination} ou {documents, pagination} | 200 |
| Download | {url, expires_in} | 200 |
| DELETE de vínculo/documento | Sem corpo; não tente ler JSON | 204 |
Viajante e viagem
O objeto traveler do exemplo anterior é o formato de detalhe e dos recibos POST/PATCH. A lista traz apenas id, external_id, name, email e phone; consulte GET individual para obter os demais campos. Os campos pessoais opcionais retornam string ou null; birth_date e passport_expiry, quando preenchidos, usam YYYY-MM-DD. Registros legados podem ter nome/e-mail nulos. id é UUID; external_id é string ou null para registros criados sem esse identificador.
| Campo da viagem | Tipo/observação |
|---|---|
id, external_id | UUID; identificador externo string ou null |
title | String; registros legados podem retornar null |
start_date, end_date | YYYY-MM-DD; registros legados podem retornar null |
status | draft, confirmed, ongoing, completed ou cancelled; efetivo em Brasília |
status_manual | Booleano; presente nos detalhes/recibos, indica escolha pelo dashboard |
description, notes, cover_image_url | String ou null, presentes nos detalhes/recibos |
pax_count | Inteiro, ou null em registro legado; não substitui vínculos de viajantes |
tags | Array de strings, ou null em registro legado |
days, travelers | Arrays presentes somente no GET individual, não no POST/PATCH/lista |
A lista de viagens traz somente id, external_id, title, start_date, end_date e status. No POST, traveler fora do objeto trip é o principal informado/persistido, com o estado de convite. Se não houver principal, é null; não representa o roster completo. No GET, trip.travelers contém referências {client_id, group_id}, com UUID ou null em cada campo. A referência individual/principal usa client_id; a referência de grupo usa group_id. Membros do grupo não são expandidos nem gerenciados por esta API; consulte-os no dashboard. Um mesmo vínculo individual/principal aparece uma única vez.
Exemplo fictício de GET individual de viagem:
{
"trip": {
"id": "22222222-2222-4222-8222-222222222222",
"external_id": "crm-8841",
"title": "Lisboa com a Ana",
"start_date": "2099-11-02",
"end_date": "2099-11-08",
"status": "confirmed",
"status_manual": false,
"description": null,
"notes": null,
"pax_count": 1,
"cover_image_url": null,
"tags": [],
"days": [{
"id": "33333333-3333-4333-8333-333333333333",
"day_number": 1,
"date": "2099-11-02",
"title": "Chegada",
"summary": null,
"items": [{
"id": "44444444-4444-4444-8444-444444444444",
"type": "activity",
"title": "Passeio",
"start_datetime": "2099-11-02T15:00:00+00:00",
"end_datetime": null,
"metadata": {"api_start_local_date": "2099-11-02"},
"sort_order": 0
}]
}],
"travelers": [{
"client_id": "11111111-1111-4111-8111-111111111111",
"group_id": null
}]
}
}Dias, itens e normalização
O dia contém id UUID, day_number inteiro, date data, title/summary string ou null e items array. items contém id, type, title, start_datetime, end_datetime, metadata e sort_order. title e horários podem ser nulos; horários preenchidos são ISO com fuso e representam o instante, sem garantir o mesmo offset textual enviado. sort_order define a ordem. Notas e confirmação não vêm em campos separados no GET: ficam em metadata.notas e metadata.conf, quando preenchidas.
Na escrita, use start_at/end_at. Na leitura, use start_datetime/end_datetime. metadata é um objeto de dados auxiliares ou null; só use as propriedades descritas neste contrato. Outras propriedades podem ser acrescentadas. O objeto não é um campo gravável da integração.
| Tipo enviado | Tipo retornado | Metadados relevantes |
|---|---|---|
transfer | transfer | subtype: transfer, origem, destino, partida |
land_transport | transfer | subtype: coletivo, origem, destino, partida |
sea_transport | transfer | subtype: aquatico, porto_embarque, porto_desembarque, partida |
car_rental | transfer | subtype: rental, retirada_local, devolucao_local, retirada_datetime, devolucao_datetime |
accommodation | accommodation | nome, checkin, checkout, endereco, address |
flight | flight | numero, cia, origem, destino, partida_local, chegada_local, terminal, gate, passengers_json |
| Demais tipos aceitos | Mesmo tipo enviado | address, quando informado; horários nos campos do item |
Valores opcionais podem estar ausentes ou vazios. api_start_local_date/api_end_local_date conservam as datas locais de itens escritos com horários. Em voos, passengers_json é uma string JSON; faça parse para obter um array, nunca execute o conteúdo. Os campos de leitura de passageiro são clientId, name, classe, seat, bagagens, pnr e numero_bilhete; clientId pode estar vazio enquanto não houver associação. O GET não devolve o e-mail de resolução do passageiro. Não confunda atribuição ao voo com vínculo à viagem.
Documentos
Recibo e listagem usam o mesmo objeto, sem o conteúdo binário nem URL permanente do arquivo:
{
"document": {
"id": "55555555-5555-4555-8555-555555555555",
"external_id": "voucher-19",
"type": "tour_voucher",
"title": "Voucher do passeio",
"file_name": "voucher.pdf",
"byte_size": 12000
}
}id é UUID; external_id é string ou null. type segue a enumeração de Documentos. title/file_name são strings; registros legados podem retornar null. byte_size é inteiro de bytes do arquivo gravado, ou null em legado; imagens podem ficar menores após processamento. Documentos pessoais acrescentam document_number string ou null e expiry_date data ou null. Para abrir o arquivo, solicite a rota /download.
Exemplo de listagem paginada vazia: {"documents":[],"pagination":{"limit":100,"offset":0,"next_offset":null}}. POST de vínculo retorna exatamente {"linked":true}; DELETE bem-sucedido retorna HTTP 204 sem corpo.
Convite e acesso ao app
A criação com traveler vincula a pessoa e tenta convidá-la. A resposta traz traveler.invite: sent, already_active ou pending. Sem viajante, traveler é null.
pending significa que a viagem foi gravada e o convite precisa de atenção. invite_error explica a falha e retry_after, quando presente, informa a espera em segundos. Repita o POST da mesma viagem ou use a rota de convite após corrigir uma falha conhecida. A rota de convite responde 202 enquanto pendente e 200 quando concluído.
Um convite confirmado não é reenviado por repetir a viagem. Não presuma que 201 significa e-mail entregue. sent significa aceito para envio, não confirmação de entrega ou leitura; already_active indica uma conta já associada para esse viajante.
| Estado/código do convite pendente | Próxima ação |
|---|---|
rate_limited | Aguarde retry_after segundos e repita a rota de convite; não crie outra viagem |
invite_rejected | Confira e-mail, vínculo e situação do viajante no painel; se persistir, acione suporte |
invite_unavailable | O convite não foi concluído. Acione suporte se persistir; após resolução, repita a mesma rota |
invite_in_progress | Outro envio ainda não terminou. Consulte novamente o recurso; não force reenvio ou troca de e-mail. Se persistir, suporte |
invite_confirmation_required | Resultado do envio sem confirmação. Acione suporte antes de repetir; não faça reenvio automático |
Exemplo de resposta HTTP 202 da rota de convite: {"invite":"pending","error":"rate_limited","retry_after":90}. Quando concluída, responde HTTP 200 com {"invite":"sent"} ou {"invite":"already_active"}. Na criação da viagem, as mesmas informações ficam em traveler.invite, traveler.invite_error e traveler.retry_after; a viagem continua gravada mesmo com convite pendente. O campo error de uma resposta 202 é um estado de convite, não falha na gravação da viagem.
O vínculo de uma pessoa existente é uma operação separada do convite. Se o viajante não vê a viagem no app:
- Confira se está vinculado à viagem, como principal, individualmente ou por grupo.
- Confira se usa a conta do mesmo e-mail cadastrado e se o acesso está autorizado pela agência.
- Confira o status no painel. Rascunho manual e rascunho futuro automático ficam ocultos. Rascunho automático que já passou para em andamento/concluído pelas datas pode aparecer. Outros status não são ocultados apenas por essa regra de rascunho.
- Se a viagem tiver uma proposta comercial no dashboard, ela só aparece no app após o aceite; sem proposta, essa condição não se aplica. Propostas não são gerenciadas por esta API.
A chave da agência não autentica o viajante, e sent não garante acesso ao app.
Suporte da integração
Escreva para plutotecnologia@gmail.com, canal de contato do TripMap, com assunto Integração API — agência — ambiente. Informe ambiente, horário com fuso, método/rota, HTTP, código público do erro e IDs da viagem/viajante/documento envolvidos. Não envie chave, senha, sessão do painel, documentos pessoais ou payload completo. Para envio sem confirmação, informe também o estado invite/invite_error; aguarde a orientação antes de reenviar.
Documentos
O download de arquivo hospedado no TripMap retorna URL temporária com expires_in: 300, em segundos. Documentos existentes no painel que usem links externos HTTP/HTTPS retornam o link cadastrado com expires_in: null; isso significa que o TripMap não define a validade, não que o link seja garantidamente permanente. Somente documentos ativos acessíveis à própria agência são retornados.
POST usa multipart/form-data: type, title (200) e um único file obrigatórios; external_id (120) opcional. O nome do arquivo aceita até 180 caracteres. Arquivos: PDF válido (.pdf), JPEG (.jpg/.jpeg), PNG (.png) ou WebP (.webp), até 25 MiB; extensão deve corresponder ao conteúdo. O conteúdo é validado, não apenas extensão/MIME. Imagens acima de 40 milhões de pixels são rejeitadas mesmo se tiverem menos de 25 MiB. Imagens são reduzidas a até 1920 × 1920 e gravadas em JPEG; a compressão pode alterar a qualidade e o nome passa a terminar em .jpg. PDFs seguem íntegros. PDFs ilegíveis ou protegidos que não possam ser validados são rejeitados.
Tipos da viagem: travel_insurance, passport, visa, hotel_voucher, flight_ticket, tour_voucher, transfer, contract, other. Tipos do viajante: rg, cpf, cnh, passport, visa, travel_insurance, other. Documentos pessoais aceitam também document_number (80) e expiry_date.
A primeira gravação responde 201. O mesmo external ID no mesmo pai responde 200, mantendo o documento existente, sem substituir o arquivo ou seus metadados; em outra viagem/pessoa responde 409 document_parent_conflict. Chamadas simultâneas não duplicam o documento. Use external_id também nos uploads para repetir uma tentativa sem criar cópias. Sem esse identificador, uma repetição pode criar outro documento.
GET lista metadados. A rota /download retorna {"url":"...","expires_in":300}; trate a URL assinada como temporária e privada. DELETE retira o documento da visualização por exclusão lógica.
Limites e erros
Autenticação e quotas são verificadas antes de ler o corpo. O limite de dez tentativas de upload/minuto por chave inclui requisições inválidas e repetições com external ID; cada POST conta uma vez, mesmo quando não cria outro arquivo. Após 429, respeite Retry-After e repita com o mesmo external ID para manter a idempotência.
| Limite | Valor |
|---|---|
| Requisições | 60/minuto/chave |
| Tentativas de upload | 10/minuto/chave |
| Tentativas de convite | 20/hora/agência, somando todas as chaves |
| JSON | 1.000.000 bytes reais, incluindo requisição sem Content-Length |
| Multipart completo | 26 MiB |
| Arquivo | 25 MiB |
Quota direta retorna 429 com Retry-After. Quota de convite na criação deixa invite: pending, preservando a resposta da viagem já gravada. Repetições continuam contando como requisições; convites já concluídos não consomem nova tentativa.
| HTTP | error |
|---|---|
| 400 | invalid_json, validation_error (pode incluir field), invalid_file, file_too_large, too_many_items, invalid_document_url |
| 401 | unauthorized |
| 403 | agency_suspended, subscription_required |
| 404 | not_found, flight_not_found |
| 409 | identity_conflict, document_parent_conflict, traveler_already_on_ongoing_trip, traveler_trip_period_conflict, flight_ambiguous, group_membership_conflict, invite_in_progress |
| 413 | payload_too_large |
| 429 | rate_limited |
| 500 | internal_error |
| 503 | flight_lookup_unavailable, storage_unavailable |
Como recuperar uma chamada
| Situação | Ação |
|---|---|
| 400/413 | Corrija JSON/campo/formato/tamanho antes de repetir; use field quando houver |
| 401 | Confira chave, cabeçalho e ambiente; se revogada, crie outra no painel |
| 403 | Confira assinatura, validade do trial e suspensão com o proprietário; repetir não resolve |
| 404 | Confira ambiente, ID e recurso ativo; recurso de outra agência também responde 404 |
| 409 | Corrija identidade, vínculo, grupo ou datas conforme o código; não crie outra identidade para contornar o conflito |
| 429 | Espere Retry-After segundos, com poucas tentativas e limite; preserve o external ID |
| 503 | Aguarde a orientação de espera quando presente; mantenha o external ID e acione suporte se persistir |
| 500 ou falha de rede/timeout | A escrita pode ter sido concluída. Consulte o recurso pelo external ID ou repita o POST com o mesmo ID; nunca suponha que houve rollback |
| 202 de convite | A viagem já está gravada. Siga a tabela de convites; não trate como falha de criação |
Formato de erro HTTP: {"error":"validation_error","field":"start_date"}; field é opcional. Falha temporária de consulta de voo usa {"error":"flight_lookup_unavailable","retry_after":30} com HTTP 503 e Retry-After: 30. Não existe um campo message obrigatório: tome decisões pelo HTTP e pelo código error. Códigos de convite pendente são descritos na seção própria e podem aparecer em HTTP 202 ou dentro do recibo de criação.
Repetir POST de viajante pode atualizar campos enviados; não é uma consulta sem efeitos. Prefira GET individual ou lista com filtro para conferir o que foi gravado. GET não expõe o histórico de entrega de convites; depois de um envio sem confirmação, a consulta da viagem confirma a gravação, mas a orientação sobre o convite continua sendo do suporte.
As quotas valem para tentativas, inclusive erros e replays. Uma carga de mil viajantes exige ao menos cerca de 17 minutos com uma única chave a 60 requisições/minuto, antes de viagens, documentos, convites e tempo de processamento. Use fila com controle de ritmo; não crie chaves adicionais para contornar a quota de convites por agência. Não há escrita em lote neste contrato.
Dados de outra agência retornam 404. Guarde chaves e URLs temporárias em privado e evite logs com dados pessoais. O guia contém exemplos fictícios; antes de executar operações de escrita, confirme os dados que serão cadastrados na conta da agência.
Fora deste contrato: exclusão de viagens/viajantes, lixeira, substituição de roteiro via PATCH, grupos, cobrança, equipe, mensagens e proposta comercial.