Install the tag
Drop the loader into the head of every page. It is asynchronous, under 9 KB gzipped, and makes no network calls until consent is granted.
<script>
(function (t, s) {
t.TravelSpy = t.TravelSpy || function () { (t.TravelSpy.q = t.TravelSpy.q || []).push(arguments); };
var e = s.createElement('script'); e.async = 1;
e.src = 'https://cdn.travelspy.io/ts.js';
s.head.appendChild(e);
})(window, document);
TravelSpy('init', { workspace: 'ws_live_xxxxxxxx', region: 'eu' });
</script>
Sending events
Call track for anything meaningful. Standard travel events are recognised by the model automatically; anything custom becomes a feature you can segment on.
TravelSpy('track', 'destination_viewed', {
destination: 'JP',
region: 'Kansai',
trip_type: 'culture',
nights: 11,
party: { adults: 2, children: 0 }
});
TravelSpy('track', 'search_performed', {
origin: 'MAN', destination: 'KIX',
depart: '2026-04-11', return: '2026-04-22',
cabin: 'economy', results: 38
});
Server-side ingestion is available on Navigate and Atlas via POST /v1/events with the same payload shape.
Profile API
Read the current modelled profile for a traveller. Median latency is under 200 ms at the edge.
GET https://api.travelspy.io/v1/profiles/{traveller_id}
Authorization: Bearer sk_live_xxxxxxxx
200 OK
{
"traveller_id": "trv_8813af",
"confidence": "high",
"observed_signals": 214,
"destination_affinity": [
{ "code": "JP", "score": 0.91 },
{ "code": "KR", "score": 0.44 }
],
"trip_type": "culture_and_food",
"party": { "adults": 2, "children": 0, "confidence": 0.86 },
"spend_band": { "band": 6, "range_gbp": [6400, 8200], "price_sensitivity": "low" },
"booking_window": { "opens_in_days": 9, "closes_in_days": 21 },
"consent": { "analytics": true, "personalisation": true, "advertising": false }
}
Package API
Ask the orchestration engine for the assembled trip. Guardrails you configure in the portal are applied before the response is returned.
POST https://api.travelspy.io/v1/packages
{
"traveller_id": "trv_8813af",
"currency": "GBP",
"components": ["flight", "accommodation", "transfer", "experience", "insurance"],
"margin_floor_pct": 12
}
Webhooks
Subscribe to model changes rather than polling. Payloads are signed with an HMAC header you should always verify.
POST /your-endpoint
X-TravelSpy-Signature: t=1770000000,v1=8f1c...
{
"event": "profile.booking_window_opened",
"traveller_id": "trv_8813af",
"occurred_at": "2026-03-02T09:14:22Z"
}
Available events include profile.created, profile.segment_changed, profile.booking_window_opened, package.assembled and consent.withdrawn.
Audience export
Segments can be pushed to ad platforms, your CRM or your warehouse. Consent state is enforced at the point of export, so an advertising-suppressed traveller is never included in an ad audience regardless of segment membership.
POST https://api.travelspy.io/v1/audiences/{audience_id}/sync
{ "destination": "meta_ads", "account_id": "act_123456789" }
Consent
TravelSpy will not store or score a traveller until a consent decision is recorded. Integrate with your CMP or set it directly.
TravelSpy('consent', {
analytics: true,
personalisation: true,
advertising: false
});
Calling TravelSpy('forget') or DELETE /v1/profiles/{traveller_id} erases the profile and propagates the deletion to every connected destination.
Rate limits & errors
Default limits are 1,000 requests per minute for read endpoints and 200 per minute for write endpoints, raised on request. Errors use standard HTTP codes with a machine-readable body.
429 Too Many Requests
{ "error": "rate_limited", "retry_after_ms": 1400 }
Want a sandbox key?
We will set up a sandbox workspace with synthetic traveller data so your engineers can build against the real API before any commercial conversation.