Skip to main content

Partner API

Send transfers to an operator by API or from your AI agent

One REST API and one MCP server to quote, book, modify and cancel transfers with a TransfersManager operator. The prices are the operator's own, and the operator issues your key from its panel.

What it is

The Partner API lets an OTA, a travel agency or a booking engine send work straight to a transport operator that runs on TransfersManager. You do not integrate with the platform in general: you integrate with one operator, with its vehicles, its zones and its price list.

  • You get a quote with a signed price. The booking is charged exactly that price.
  • You create, read, modify and cancel bookings, and you follow them with webhooks.
  • The same key works over REST and over MCP, so an AI agent can book on your behalf.
  • Nothing is charged through TransfersManager. You settle with the operator under your own agreement.

Get your key

The operator creates a partner for you in its panel (Settings, Partners API) and gives you two things: your API key (starts with tmp_) and, if you want events, a webhook secret (starts with whsec_). Both are shown only once. If you lose one, ask the operator to rotate it.

Authentication and limits

Send your key in the X-API-Key header, or as a Bearer token. The key is scoped to one operator and to you: it can only see and change your own bookings.

  • Rate limit per key: 120 requests per minute by default. The operator can set it between 10 and 600.
  • Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
  • When you go over, you get a 429 with a Retry-After header. Wait that many seconds before retrying.
curl https://api.transfersmanager.ai/api/v1/partner/me \
  -H "X-API-Key: tmp_your_key_here"

Base URL and OpenAPI

All calls go to https://api.transfersmanager.ai and the machine-readable specification is published as OpenAPI 3:

Endpoints

All paths are relative to the base URL.

GET/api/v1/partner/meYour partner profile and the operator you work with.
GET/api/v1/partner/vehicle-classesThe vehicle classes the operator offers, with passenger and luggage capacity.
POST/api/v1/partner/quotesGet priced options for a trip. Each option carries a signed quote_id.
POST/api/v1/partner/bookingsCreate a booking from a quote_id.
GET/api/v1/partner/bookings?external_ref=&limit=&offset=List your bookings. Filter by external_ref, and page with limit and offset.
GET/api/v1/partner/bookings/{ref}Read one booking. The reference can be the booking_id, the confirmation number (TM26-XXXXXX) or your own external_ref.
PATCH/api/v1/partner/bookings/{ref}Modify a booking until the driver leaves.
GET/api/v1/partner/bookings/{ref}/cancellationCheck what cancelling right now would cost, without cancelling.
POST/api/v1/partner/bookings/{ref}/cancelCancel a booking.

Quote a trip

Send the pickup, the drop-off, the time and the party size. Latitude and longitude are optional: if you leave them out, we geocode the address.

  • pickup_time is ISO 8601 with a UTC offset. Without an offset, send also "timezone": "Europe/Madrid" (or the right zone). Anything else returns 422.
  • The pickup must be at least 30 minutes ahead.
  • Each option has a quote_id that is signed. The booking is charged exactly that price, and the quote is valid for 24 hours.
  • price_is_approximate tells you when the price depends on something the operator will confirm.
Request
POST /api/v1/partner/quotes
{
  "pickup": { "address": "Málaga Airport (AGP)", "lat": 36.6749, "lng": -4.4991 },
  "dropoff": { "address": "Hotel Puente Romano, Marbella" },
  "pickup_time": "2026-11-20T10:30:00+01:00",
  "passengers": 3,
  "luggage": 3,
  "child_seats": 1
}
Response
{
  "pickup": { "address": "Málaga Airport (AGP)", "lat": 36.6749, "lng": -4.4991 },
  "dropoff": { "address": "Hotel Puente Romano, Marbella", "lat": 36.5016, "lng": -4.9456 },
  "pickup_time": "2026-11-20T10:30:00+01:00",
  "passengers": 3,
  "luggage": 3,
  "distance_km": 54.2,
  "options": [
    {
      "quote_id": "qt_...",
      "vehicle_class": { "code": "minivan", "name": "Minivan", "max_passengers": 6, "max_luggage": 6 },
      "price": 78.0,
      "currency": "EUR",
      "price_is_approximate": false,
      "expires_at": "2026-11-19T10:30:00+01:00"
    }
  ]
}

Create a booking

Post the quote_id with your own reference and the passenger. Add an Idempotency-Key header (any UUID) so that a retry after a timeout never creates a second booking.

Request
POST /api/v1/partner/bookings
Idempotency-Key: 6f1c2f0e-5b1e-4d0e-9a3b-0c1f7a2d9e11

{
  "quote_id": "qt_...",
  "external_ref": "OTA-123456",
  "passenger": { "name": "Ana García", "phone": "+34600000000", "email": "[email protected]" },
  "pickup_address": "Arrivals hall, Terminal 3",
  "dropoff_address": "Hotel Puente Romano",
  "flight_number": "FR1234",
  "meet_board_name": "GARCÍA",
  "notes": "One child seat needed"
}

What you can get back

201
Booking created.
200
The external_ref (or Idempotency-Key) was already used with the same body. You get the original booking with "duplicate": true.
409
The external_ref or the Idempotency-Key was reused with a different body (external_ref_conflict, idempotency_key_conflict), or the first request is still running (booking_in_progress).
422
invalid_quote, quote_expired, passengers_exceed_quote or pickup_too_soon.
429
operator_quota_exceeded: the operator has reached its quota for Partner API bookings.

Payment: nothing is charged through TransfersManager. You settle each booking with the operator.

The booking object

Every booking endpoint returns the same shape.

Example
{
  "booking_id": "...",
  "confirmation_number": "TM26-XXXXXX",
  "external_ref": "OTA-123456",
  "status": "confirmed",
  "pickup": { "address": "...", "lat": 36.6749, "lng": -4.4991 },
  "dropoff": { "address": "...", "lat": 36.5016, "lng": -4.9456 },
  "pickup_time": "2026-11-20T10:30:00+01:00",
  "passengers": 3,
  "luggage": 3,
  "child_seats": 1,
  "baby_seats": 0,
  "vehicle_class": { "code": "minivan", "name": "Minivan" },
  "price": 78.0,
  "currency": "EUR",
  "flight_number": "FR1234",
  "passenger": { "name": "Ana García", "phone": "+34600000000", "email": "[email protected]" },
  "driver": { "first_name": "Luis", "phone": "+34611111111" },
  "vehicle": { "brand": "Mercedes", "model": "V-Class", "color": "black", "license_plate": "1234ABC" },
  "cancellation": null,
  "created_at": "2026-11-01T09:12:44Z",
  "duplicate": false
}

Statuses

  • pending_confirmation
  • confirmed
  • driver_assigned
  • driver_en_route
  • driver_arrived
  • picked_up
  • completed
  • no_show
  • cancelled
  • rejected

Modify a booking

PATCH changes the details that do not change the price. While the driver has not left, you can freely change:

  • passenger, flight_number, meet_board_name and notes,
  • pickup_address and dropoff_address (as text, for the driver).

Changing time, passengers, luggage or seats

Quote again with the same pickup and drop-off and send the new quote_id in the PATCH. The booking takes that new price.

  • A pickup or drop-off more than 1 km away is a different trip: you get 422 route_changed. Create a new booking and cancel the old one.
  • Once the driver has left, modifying returns 409 booking_already_started.

Cancel a booking

Cancelling uses the operator's cancellation policy as it was frozen when you booked. Call GET /cancellation first to show your customer what it costs, then POST /cancel.

  • The response includes a cancellation object with hours_of_notice, refund_pct, fee_pct, fee_amount, currency, policy_source and cancelled_by.
  • fee_amount is what the operator may invoice you.
  • Repeating the cancel returns the same result.
  • Once the passenger is on board, cancelling returns 409.
POST /api/v1/partner/bookings/{ref}/cancel
{ "reason": "Customer changed plans" }

// Response: the booking, now with
"status": "cancelled",
"cancellation": {
  "hours_of_notice": 30.5,
  "refund_pct": 100,
  "fee_pct": 0,
  "fee_amount": 0.0,
  "currency": "EUR",
  "policy_source": "operator",
  "cancelled_by": "partner"
}

Webhooks

The operator sets your webhook URL in its panel (public https) and gives you the webhook secret. We then call you for each change in your bookings. Webhooks never include passenger data.

Events

booking.confirmed
The operator confirmed the booking.
booking.driver_assigned
A driver was assigned. Includes driver (first_name, phone) and vehicle.
booking.driver_unassigned
The driver was removed; a new one will follow.
booking.driver_en_route
The driver is on the way.
booking.driver_arrived
The driver is at the pickup point.
booking.picked_up
The passenger is on board.
booking.completed
The trip is finished.
booking.no_show
The passenger did not show up.
booking.cancelled
Cancelled. Includes the cancellation object and cancelled_by (partner or operator).
ping
Test event sent from the operator's panel.

Payload

{
  "id": "whd_...",
  "event": "booking.driver_assigned",
  "created_at": "2026-11-20T08:02:11Z",
  "data": {
    "booking_id": "...",
    "confirmation_number": "TM26-XXXXXX",
    "external_ref": "OTA-123456",
    "status": "driver_assigned",
    "occurred_at": "2026-11-20T08:02:10Z",
    "driver": { "first_name": "Luis", "phone": "+34611111111" },
    "vehicle": { "brand": "Mercedes", "model": "V-Class", "color": "black", "license_plate": "1234ABC" }
  }
}

Headers

  • X-TM-Event: the event name.
  • X-TM-Delivery-Id: unique per delivery. Use it to deduplicate.
  • X-TM-Signature: t=<unix timestamp>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" with your webhook secret.

Verify every delivery

  1. Read the raw body, before any JSON parsing.
  2. Compute the HMAC-SHA256 of "<t>.<raw body>" and compare it with v1 in constant time.
  3. Reject the request if t is older than 5 minutes.
  4. Deduplicate by X-TM-Delivery-Id and answer with any 2xx.
Node.js
import crypto from 'node:crypto';

// rawBody: the request body as a string or Buffer, BEFORE JSON.parse.
export function verifyTmSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hashlib
import hmac
import time


def verify_tm_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > 300:  # older than 5 minutes
        return False

    expected = hmac.new(
        secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

If you do not answer with a 2xx, we retry with exponential backoff: from 30 seconds up to 1 hour, then daily. The events of one booking are delivered in order.

From your AI agent (MCP)

The same operator, the same prices and the same key are available to AI agents through a remote MCP server (Streamable HTTP) at https://mcp.transfersmanager.ai/mcp. Use your key as a Bearer token.

Claude Code
claude mcp add --transport http transfersmanager https://mcp.transfersmanager.ai/mcp \
  --header "Authorization: Bearer tmp_your_key_here"

In any other client, add a remote MCP server with that URL and the header Authorization: Bearer tmp_...

https://mcp.transfersmanager.ai/mcp

Tools

quote_transfer
Price a trip and get options.
check_availability
Check that the operator can serve a trip.
create_booking
Book an option. Takes the option_id from quote_transfer, your client_reference (used for idempotency) and passenger_name.
get_booking
Read the state of a booking.
cancel_booking
With confirm=false it shows the fee first. With confirm=true it cancels.
list_vehicle_classes
The operator's vehicle classes.

Errors

Errors share one format, with a stable code you can switch on and a message for people:

{ "detail": { "code": "quote_expired", "message": "This quote has expired. Request a new one." } }
401 invalid_key
The key is missing, wrong or revoked.
402 subscription_required
The operator's plan does not include the Partner API.
404
Not found. It is also what you get for a booking that is not yours.
409 / 422 / 429
See each endpoint above.

Are you an operator?

Open your panel, go to Settings, Partners API, and issue a key to the first OTA that sends you work.

Start with TransfersManager