Connect an OpenAI-based client to travel.tech's MCP server
Wire travel.tech's MCP tools into an OpenAI Assistants/Apps SDK integration or a function-calling client.
travel.tech exposes its travel-search, booking, and trip tools as an
MCP server. Two HTTP transports share the
same assistant tool surface:
POST /v1/mcp— JSON-RPC in a single HTTP request/response. Use this from
a function-calling bridge you run yourself (you sign or attach
credentials on every call).POST /v1/mcp/stream— Streamable HTTP (MCP spec revisions2026-07-28,
2025-11-25,2025-06-18, and2025-03-26).
Use this from OpenAI's hosted MCP connector, which calls travel.tech
directly over Streamable HTTP.
/v1/mcpand/v1/mcp/streamare not on the API reference. OpenAPI
cannot model a JSON-RPC envelope (a singlePOSTwhose behavior branches
on themethodfield in the body) as a set of typed operations, so this
guide — not the reference's schema browser — is the source of truth for
the tool names, inputs, and error codes below.
Authentication
POST /v1/mcp and POST /v1/mcp/stream accept the same credential model.
Exactly one mode applies per request — present whichever matches your
integration, never more than one credential type at once. A request with no
credential is rejected (HTTP 401).
- Traveler JWT — an
Authorization: Bearerheader carrying a token
obtained through BizAway sign-in (see
Authentication § Obtaining a token). This
unlocks every tool scoped to that traveler —listTrips,
searchFlights,getInvoiceProfiles, and so on. Most integrations
acting on behalf of a signed-in traveler want this mode. Hosted
connectors that send a single staticAuthorizationheader should use
this token againstPOST /v1/mcp/stream. - MCP OAuth 2.1 access token — an
Authorization: Bearerheader
carrying an access token issued by travel.tech's MCP authorization
server. Discovery, authorization, and token endpoints are all live (see
OAuth 2.1), including the browser
account-linking flow/oauth/authorizehands off to. Completing that
flow links the token to a specific traveler's BizAway account, so a
linked access token unlocks the same traveler-scoped tool surface a
traveler JWT does — mutating tools included. OpenAI's hosted connector,
which can open a system browser, completes the full first-time login
this way. If you already hold a linked access token, send it directly;
the connector only needs to open the browser flow once per traveler. - Consumer HMAC — no bearer token, but the
TravelTech-Consumer/
TravelTech-Timestamp/TravelTech-Signatureheaders that identify your
registered integration and are recomputed per request from a per-consumer
secret. Full algorithm and worked examples:
Authentication § Consumer HMAC. This is a
conduit call with no traveler identity attached — it only reaches
non-mutating, non-traveler-scoped tools; mutating tools (bookings) and
every traveler-scoped tool or resource stay withheld on this path. Your
integration must be registered as a consumer with travel.tech before this
works — contact your BizAway integration contact to provision a slug and
secret. Because the signature covers a hash of the exact request body and
a per-request timestamp, it cannot be entered as a static header in
OpenAI's hosted connector; use a function-calling bridge for HMAC.
A credential that is presented but fails its own verification (an
expired/malformed JWT or MCP OAuth token, or a consumer-HMAC signature that
doesn't match) is rejected outright — it never falls back to a weaker mode.
Every authentication failure is an HTTP status with travel.tech's standard
REST error envelope, not a JSON-RPC error body:
- A rejected bearer (an expired, malformed, or revoked traveler JWT or MCP
OAuth access token) returns HTTP 401 with
WWW-Authenticate: Bearer error="invalid_token", error_description="…".
When MCP OAuth is enabled the challenge also carries
resource_metadata="…/.well-known/oauth-protected-resource/v1/mcp", so a
connector refreshes its access token (or restarts sign-in) on its own. - A bad consumer signature returns
CONSUMER_AUTH_REQUIRED(HTTP 401). - When the credential cannot be checked right now (for example the signing
keys or BizAway identity service are briefly unreachable) the response is
HTTP 503 withRetry-After. Keep the token and retry after that many
seconds; it has not been rejected.
Connecting with a function-calling bridge
Since your own server holds the credentials and makes the HTTP call, this
path uses POST /v1/mcp and does not require Streamable HTTP. The shape is:
- Call
tools/listonce (cache the result) to get each tool'sname,
description, and JSON-SchemainputSchema. - Map each entry to an OpenAI function/tool definition — the schemas are
already JSON Schema, so this is close to a direct pass-through. - Pass those definitions as
toolson your Responses (or Chat Completions)
API call. - When the model emits a function/tool call, forward it to travel.tech's
tools/calland return the result as the tool output.
import hashlib
import hmac
import json
import time
import uuid
import httpx
from openai import OpenAI
TRAVELTECH_BASE_URL = "https://api.travel.tech"
CONSUMER = "acme-travel"
CONSUMER_SECRET = "<your consumer secret>"
traveler_jwt = "<traveler-jwt>" # issued by BizAway sign-in; omit for conduit-only mode
openai_client = OpenAI()
travel_tech = httpx.Client(base_url=TRAVELTECH_BASE_URL, timeout=15.0)
def _signed_headers(method: str, path: str, body: bytes) -> dict[str, str]:
timestamp = str(int(time.time()))
body_hash = hashlib.sha256(body).hexdigest()
canonical = f"{method}\n{path}\n{timestamp}\n{body_hash}"
signature = hmac.new(CONSUMER_SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
headers = {
"TravelTech-Consumer": CONSUMER,
"TravelTech-Timestamp": timestamp,
"TravelTech-Signature": signature,
"Content-Type": "application/json",
}
# A traveler JWT takes priority over the consumer HMAC above when both are
# present -- send it only when acting on behalf of a signed-in traveler.
if traveler_jwt:
headers["Authorization"] = f"Bearer {traveler_jwt}"
return headers
def call_travel_tech_mcp(method: str, params: dict) -> dict:
body = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method, "params": params}
payload = json.dumps(body).encode()
response = travel_tech.post(
"/v1/mcp", content=payload, headers=_signed_headers("POST", "/v1/mcp", payload)
)
data = response.json()
if "error" in data:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
return data["result"]
def travel_tech_tools_as_openai_functions() -> list[dict]:
tools = call_travel_tech_mcp("tools/list", {})["tools"]
return [
{
"type": "function",
"name": t["name"],
"description": t["description"],
"parameters": t["inputSchema"],
}
for t in tools
]Worked example: listTrips
listTripsGive the model the mapped tool list and let it decide when to call one:
response = openai_client.responses.create(
model="gpt-4.1",
input="What's my next trip?",
tools=travel_tech_tools_as_openai_functions(),
)
for item in response.output:
if item.type == "function_call" and item.name == "listTrips":
tool_result = call_travel_tech_mcp(
"tools/call", {"name": "listTrips", "input": {}}
)
# tool_result == {"result": {"past": [...], "current": [...],
# "upcoming": [...], "incomplete": [...], "totalCount": N}}
# Feed tool_result back as the function_call_output for this call_id,
# then call responses.create again to get the model's final reply.The signed HTTP request your bridge sends for that call looks exactly like
the one on the Claude integration guide — same JSON-RPC
envelope, same signed headers, same response shape — because both platforms
are calling the same /v1/mcp endpoint; only the client-side tool-call
plumbing differs.
Look up a place, then search
Travelers name places loosely: "a hotel in Milan near the city centre",
"Milan to Paris", "Gare de Lyon", "Mailand". travel.tech resolves those words
with one policy for stays, flights, trains and cars, in English, Italian,
French, Spanish and German, so your integration never needs place ids or
airport codes.
There are two ways to use it:
- Search with the traveler's words.
searchStays(destination_query),
searchFlights(origin_query,destination_query),searchTrains
(origin_query,destination_query) andsearchCars(pickup_query)
resolve the words themselves. When the place was a confident guess, the
result carrieslocationAssumptions. Show that line to the traveler (for
example "Searched near Milano Centrale, Piazza Duca d'Aosta, Milan, …") so
they can correct it in one turn. - Look the place up first with
findPlaceswhen you want to confirm it
or reuse it across searches. It returns rankedcandidates(each with
placeId,kind,label,city,countryandwhy), the
recommendedone, andneedsConfirmation.
Ask the traveler only when needsConfirmation is true (words that truly
name several places, such as "Springfield"), offering the candidates'
labels, at most once. Otherwise search with recommended.placeId:
| Vertical | Pass placeId as |
|---|---|
stay | searchStays.place_id |
flight | searchFlights.origin or destination |
train | searchTrains.origin_id or destination_id |
car | searchCars.pickup_id, with pickup_type set to carLocationType |
When a search tool can't pick a place safely on its own, it returns the same
labelled candidates in its error (ambiguous_destination, ambiguous_station
or ambiguous_location) with needsConfirmation: true.
Example exchange
The traveler asks: "Find me a hotel near Centrale in Milan for 18 to 20
November." The model looks the place up:
{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"findPlaces","input":{"query":"Milan near Centrale","vertical":"stay"}}}travel.tech recommends the station, and says what it assumed:
{
"jsonrpc": "2.0",
"id": "2",
"result": {
"result": {
"vertical": "stay",
"rule": "landmark_in_city",
"confidence": "good",
"needsConfirmation": false,
"recommended": {
"placeId": "ChIJ…",
"kind": "station",
"name": "Milano Centrale, Piazza Duca d'Aosta, Milan, Italy",
"label": "Milano Centrale, Piazza Duca d'Aosta, Milan, Italy (station)",
"assumption": "Searched near Milano Centrale, Piazza Duca d'Aosta, Milan, Italy"
},
"candidates": ["…"],
"message": "Search with recommended.placeId without asking, and tell the traveler what was assumed (recommended.assumption) so they can correct it."
}
}
}The model then calls searchStays with place_id set to the recommended
placeId and the dates, and answers with the stays and the assumption:
"These are near Milano Centrale; tell me if you meant somewhere else."
Results come ranked, with a reason
searchStays, searchFlights, searchTrains and searchCars, and the tools
that narrow or page them (refineStaySearch, refineFlightSearch,
refineTrainSearch, refineCarSearch, getMoreResults), list options best
first. Each option carries three fields to quote to the traveler:
rank: 1 is the best match among the options that passed the traveler's
constraints. The constraints you set (stars, price, direct only) are never
traded away for a higher score; ranking only orders what they kept.rankReason: a short phrase for the rank, such as "Lowest price; Best value
for the price", "Your preferred stay chain", or "Outside your travel policy"
with the first reason. It is English text.isPolicyCompliant: whether the traveler's travel policy clears the option.
An option the policy does not clear ranks below every option it does clear.
nullmeans no policy verdict is available.
A flight search keeps the order the traveler asked for when it sets sort
(for example price-asc), and a train search lists its cheapest and fastest
direct picks first; rank still tells you where the scorer placed each one.
Present the options in the order returned instead of sorting them again.
Connecting with OpenAI's hosted MCP connector
OpenAI's hosted MCP tool (Responses API type: "mcp", and the Apps SDK
built on it) calls travel.tech directly over Streamable HTTP. Point it
at POST /v1/mcp/stream, not at POST /v1/mcp. The connector negotiates
the protocol revision itself (the initialize handshake for 2025-03-26
through 2025-11-25, per-request MCP-Protocol-Version: 2026-07-28
otherwise). On this URL, tools/call takes the spec's params.arguments
and returns a standard CallToolResult (content, plus structuredContent
from 2025-06-18 on). The params.input / {"result": ...} shape shown
for POST /v1/mcp applies only to that endpoint.
With a traveler JWT or a linked MCP OAuth access token on a
handshake revision (2025-03-26 through
2025-11-25), initialize returns an Mcp-Session-Id response header
naming the traveler's open assistant session. If no session can be opened
at that moment, initialize still succeeds without the header and the
connection is stateless. The connector echoes it on
every later request, which is what lets refineFlightSearch,
getMoreResults, getResultDetails, and presentResults build on a search
from an earlier call. An Mcp-Session-Id the bearer does not own, or one
that has expired, returns HTTP 404; start a new session with initialize.
Sessions cannot be ended by the client (DELETE returns HTTP 405).
Consumer HMAC connections and 2026-07-28 requests are stateless: no
Mcp-Session-Id is issued and each tool call stands alone.
Use a traveler JWT as authorization when the model must call
traveler-scoped tools (listTrips, bookings, and so on). A linked MCP
OAuth 2.1 access token is also a valid bearer on this URL and acts for
the traveler it was linked to, exactly like a traveler JWT. Consumer
HMAC cannot be expressed as a single static header; keep using the function-calling bridge
for HMAC.
response = openai_client.responses.create(
model="gpt-4.1",
input="What's my next trip?",
tools=[
{
"type": "mcp",
"server_label": "travel-tech",
"server_url": "https://api.travel.tech/v1/mcp/stream",
"authorization": "<traveler-jwt-or-mcp-oauth-access-token>",
}
],
)OpenAI's hosted connector completes a first-time in-browser OAuth login
against travel.tech: /oauth/authorize hands off to a browser
account-linking flow (sign-in, account confirmation, consent) that
finishes with a redirect back to the connector carrying the authorization
code. If you already hold a linked MCP OAuth access token, pass it as
authorization directly; otherwise let the connector open the browser
flow, or use a traveler JWT or the function-calling bridge.
OAuth 2.1
When MCP OAuth is enabled on the deployment, travel.tech publishes:
| Purpose | Method and path |
|---|---|
| Authorization server metadata (RFC 8414) | GET /.well-known/oauth-authorization-server |
| Protected resource metadata (RFC 9728) | GET /.well-known/oauth-protected-resource and GET /.well-known/oauth-protected-resource/v1/mcp |
| JWKS | GET /.well-known/jwks.json |
| Authorization | GET /oauth/authorize |
| Token | POST /oauth/token |
The advertised resource identifier is {issuer}/v1/mcp. Supported grant
types are authorization code (PKCE S256) and refresh token. The MCP
scope is bizaway.mcp. Access tokens are sent as
Authorization: Bearer on POST /v1/mcp and POST /v1/mcp/stream.
Clients identify themselves with an OAuth Client ID Metadata Document;
dynamic client registration is not exposed.
GET /oauth/authorize validates the client, parks the request, and
redirects to a browser account-linking flow (sign-in, account
confirmation, consent) that completes at /auth/complete, redirecting
back to the client's redirect_uri with the authorization code. A hosted
connector that can open a system browser completes a brand-new login this
way. Token refresh and bearer use of an already-issued access token are
live.
Troubleshooting
- HTTP 401 with
WWW-Authenticate: Bearer error="invalid_token"— the
traveler JWT or MCP OAuth access token is malformed, expired, or revoked.
A connector refreshes its token on this response; a bridge should re-mint
it and retry. - HTTP 503 with
Retry-After— the credential could not be checked right
now. Retry after the given number of seconds with the same token. -32010(auth error) — BizAway rejected the traveler's token while a
tool call was already running (the token expired mid-call). Refresh or
re-mint the token and retry the call.-32001(tool not found) — the tool name is misspelled, or the
account calling in does not have that tool's module/feature flag enabled.
Calltools/listand use the returned names verbatim.-32002(permission denied) — the credential used cannot reach that
tool (for example, a mutating or traveler-scoped tool called via the
consumer-HMAC conduit or an MCP OAuth access token, instead of a traveler
JWT).-32012(rate limited) — back off for theretryAfterseconds in the
error'sdatafield before retrying.CONSUMER_AUTH_REQUIRED(HTTP 401, before any JSON-RPC body is read) —
the consumer HMAC headers are missing or the signature does not match; see
Authentication § Troubleshooting.
Reporting issues
This page is maintained by the BizAway integration team. To request changes or
report an error, contact your BizAway integration contact. Edits made directly
in the ReadMe dashboard are overwritten on the next publish.
Updated 2 days ago