Orizn Visa API · v1.1

API Documentation

Production-ready visa intelligence — 47,362 passport/destination pairs, 30 data points each, 15 languages. 28 REST endpoints across 8 product surfaces.

OpenAPI 3.0.3 spec
Auto-import into Postman, Insomnia, RapidAPI or generate a client SDK in any language.
get up and running in 60 seconds

Quickstart

Every Orizn account ships with a personal API key. Pass it via the x-api-key header (or?api_key= query param) and you're live.

Working in a specific stack? The integration tutorials take you from an empty folder to a running call — MCP, REST, JavaScript, Python, Rust, LangChain, React and Next.js. Every code block on those pages was executed before publishing.

# Free key: evaluation only. Shipping to users? -> commercial license required.
curl "https://visa.orizn.app/api/v1/visa?passport=FRA&destination=JPN&lang=en" \
  -H "x-api-key: YOUR_API_KEY"
GET /api/v1/visa/check?passport=FRA&destination=JPN

No key needed — run this demo call live from your browser. You get a real 200 response from the API, same-origin.

Base URL: https://visa.orizn.app · JSON in/out · UTF-8 · CORS open on all public endpoints.

1. Authentication

There are three ways to authenticate, depending on the endpoint:

1 · API key

x-api-key header

Default for product code. Also accepted as ?api_key= query param.

2 · Public

no credentials

Stats, score, live, register, affiliate flows. /visa/check is also keyless when called from visa.orizn.app, localhost, or a Chrome extension.

3 · Session

orizn_token cookie

Dashboard endpoints (key rotation, Stripe checkout/portal, analytics). Set automatically when you log in on the dashboard.

# 1) API key — recommended
curl "https://visa.orizn.app/api/v1/visa?passport=FRA&destination=JPN" \
  -H "x-api-key: YOUR_API_KEY"

# 2) API key via query string (only when headers can't be set)
curl "https://visa.orizn.app/api/v1/visa?passport=FRA&destination=JPN&api_key=YOUR_API_KEY"

# 3) Session cookie (dashboard endpoints)
curl "https://visa.orizn.app/api/v1/visa/auth/ensure-key" \
  --cookie "orizn_token=YOUR_SESSION"

Never expose your key in client-side code shipped to untrusted users — proxy through your backend. Keys can be rotated at any time from the dashboard.

the things every endpoint shares

Conventions

ISO 3166-1 alpha-3
All passport and destination codes are 3-letter ISO codes (FRA, USA, JPN). 199 countries supported — the canonical list lives at /api/v1/visa/stats.
Versioning
/v1/ is stable and stays reachable. Additive changes ship without notice — new fields, new enum members, new endpoints. Breaking changes (a removed or renamed field, a narrowed type, a removed endpoint) never land in v1: they ship as /v2, and v1 keeps answering. Pin nothing; parse defensively and ignore unknown fields.
Deprecation
Six months of notice before anything in v1 stops working, counted from the dated changelog entry. During that window every affected response carries Deprecation and Sunset headers (RFC 9745 / RFC 8594) plus a Link header to the migration note, so your monitoring sees it before your users do. No silent removals, no shortened windows.
CORS
Every public endpoint sends Access-Control-Allow-Origin: *. OPTIONS preflight is implemented on all data endpoints.
Content negotiation
The /visa and /visa/check endpoints respect Accept: text/markdown and return a human-readable Markdown rendering instead of JSON — ideal for chat tools.
Idempotency
POST /register, POST /affiliate/register, POST /affiliate/ios-purchase, and POST /devices are idempotent on their natural key (email, transaction_id, device_token).
Time
All timestamps are ISO 8601 with UTC suffix. Monthly quotas reset on the 1st of each month, UTC.
28 endpoints · 8 surfaces

2. Endpoints

All endpoints live under https://visa.orizn.app. The badges under each path tell you the auth mode and minimum plan. Public endpoints have a Run button you can fire right here.

Visa data

The endpoints you'll actually call from product code.

GET/api/v1/visaapi key · any plan

Full visa intelligence

The flagship endpoint. Returns 30 data points: documents, process, fees, embassies, transit, vaccinations, safety advisories, overstay penalties, and more — in 15 languages. Plan gating: free returns the core fields plus upgrade stubs; starter unlocks every extended field except remote-work-visa and reciprocity history; pro+ returns everything including bidirectional embassy info.

Parameters
ParameterTypeRequiredDescription
passportstringYesISO 3166-1 alpha-3 (e.g. FRA).
destinationstringYesISO 3166-1 alpha-3 (e.g. JPN).
langstringNoOne of 15 supported codes (see Languages). Free plan is English-only — other languages require Starter+.
example
curl "https://visa.orizn.app/api/v1/visa?passport=FRA&destination=JPN" \
  -H "x-api-key: YOUR_API_KEY"
Response · 200 OK
{
  "data": {
    "passport": "FRA",
    "destination": "JPN",
    "requirement": "visa_free",
    "visa_free_days": 90,
    "visa_required": false,
    "description": "French citizens can enter Japan visa-free for up to 90 days.",
    "documents_required": ["Valid passport (3 months)", "Return or onward ticket"],
    "process": ["No prior formalities", "Immigration form on arrival"],
    "tips": ["Carry proof of sufficient funds"],
    "country_info": { "currency": "JPY", "language": "Japanese", "timezone": "UTC+9", "capital": "Tokyo" },
    "verified": true,
    "transit_visa": { "hubs": [{ "airport": "NRT", "city": "Tokyo", "transit_visa_required": false, "transit_free_hours": 24 }] },
    "passport_validity_months": 3,
    "visa_fee": { "single_entry": { "amount": 0, "currency": "JPY" } },
    "processing_days": { "standard": null, "express": null },
    "photo_specs": { "width_mm": 35, "height_mm": 45, "background": "white" },
    "vaccinations_required": [],
    "insurance_required": { "required": false },
    "overstay_penalty": { "fine_per_day": "Variable + deportation", "ban_days": 365, "criminal": true },
    "entry_by_mode": { "air": 90, "land": 90, "sea": 90 },
    "safety": { "level": 1, "advisory": "Exercise normal precautions", "source": "diplomatie.gouv.fr" },
    "health_requirements": { "covid_test": false, "quarantine_days": 0 },
    "embassy": {
      "your_embassy_at_destination": { "name": "Ambassade de France au Japon", "city": "Tokyo", "phone": "+81 3 5798 6000" },
      "visa_application_embassy": { "name": "Ambassade du Japon en France", "city": "Paris" }
    }
  },
  "meta": { "lang": "en", "api_version": "1.1", "coverage": "199 passports x 238 destinations", "languages": 15, "data_points": 30 }
}
Error Codes
400missing or non-ISO3 passport/destination, unsupported lang401missing api key403invalid/inactive key, or non-en on free plan404no data for this pair429monthly quota exceeded
noteSet Accept: text/markdown to receive a human-friendly Markdown rendering instead of JSON. Increments requests_month and requests_total.
GET/api/v1/visa/checkapi key · any plan

Quick visa check

Lightweight yes/no — returns just the requirement type and allowed stay. The embedded _upgrade_preview field counts what the full /visa endpoint would have returned, so you can drive upsell UI without a second call.

Parameters
ParameterTypeRequiredDescription
passportstringYesISO 3166-1 alpha-3.
destinationstringYesISO 3166-1 alpha-3.
example
curl "https://visa.orizn.app/api/v1/visa/check?passport=FRA&destination=JPN" \
  -H "x-api-key: YOUR_API_KEY"

Response · 200 OK
{
  "passport": "FRA",
  "destination": "JPN",
  "requirement": "visa_free",
  "visa_free_days": 90,
  "visa_required": false,
  "_hint": "Upgrade to get documents, process, embassies, photo specs and 28 more fields.",
  "_upgrade_preview": {
    "documents_required": 4,
    "process_steps": 3,
    "embassy_info": true,
    "transit_visa": true,
    "visa_fees": false,
    "vaccinations": 0,
    "safety_advisory": "level 1",
    "languages": 15,
    "upgrade_url": "https://visa.orizn.app/visa-api/pricing"
  }
}
Error Codes
400missing or non-ISO3 params401no api key and not called from a whitelisted Referer/Origin403invalid api key404pair not found429monthly quota exceeded
noteKeyless calls are accepted only when the Referer contains visa.orizn.app, localhost, or the Origin starts with chrome-extension:// — that powers the public landing demo and the browser extension. Supports Accept: text/markdown.
GET/api/v1/visa/bulkapi key · hobby+

Bulk destinations for a passport

One passport against up to 25 destinations in a single round-trip. Pass a comma-separated destinations list (required, max 25 per call). Each returned pair counts as one request against your monthly quota. Returns a curated subset of the extended fields (fees, safety, health, vaccinations, insurance, entry-by-mode, remote-work).

Parameters
ParameterTypeRequiredDescription
passportstringYesISO 3166-1 alpha-3.
destinationstringNoComma-separated ISO3 list, e.g. JPN,THA,BRA. Omit to return every destination.
langstringNoDefault en. One of the 15 supported codes.
example
curl "https://visa.orizn.app/api/v1/visa/bulk?passport=FRA" \
  -H "x-api-key: YOUR_API_KEY"
Response · 200 OK
{
  "passport": "FRA",
  "lang": "en",
  "total": 199,
  "destinations": [
    {
      "destination": "JPN",
      "requirement": "visa_free",
      "visa_free_days": 90,
      "description": "Visa-free for up to 90 days.",
      "passport_validity_months": 3,
      "visa_fee": { "single_entry": { "amount": 0, "currency": "JPY" } },
      "safety": { "level": 1 },
      "health_requirements": { "covid_test": false },
      "vaccinations_required": [],
      "insurance_required": { "required": false },
      "entry_by_mode": { "air": 90, "land": 90, "sea": 90 },
      "remote_work_visa": { "available": false }
    }
  ]
}
Error Codes
400missing/invalid passport, malformed destination list, unsupported lang401missing api key403plan below Hobby404no data for the passport429monthly quota exceeded
noteMethod is GET (not POST). A single bulk call always counts as 1 against your quota.
GET/api/v1/visa/groupapi key · hobby+

Group trip — multi-passport intersection

Built for group travel: pass 2-10 passports and get back every destination accessible by ALL of them, with the per-passport breakdown and the group's worst-case requirement. By default a destination qualifies when every passport is visa_free, eta, visa_on_arrival or e_visa — narrow or widen with the allow param (e.g. allow=visa_free for strictly visa-free). group_visa_free_days is the minimum allowed stay across the group, i.e. the binding constraint for a shared trip. Destinations are sorted easiest-first. Each passport×destination pair served counts as one request.

Parameters
ParameterTypeRequiredDescription
passportsstringYesComma-separated ISO3 list, 2 to 10 distinct codes. Example: USA,FRA,IND.
allowstringNoComma-separated requirement types that qualify. Default: visa_free,eta,visa_on_arrival,e_visa.
example
curl "https://visa.orizn.app/api/v1/visa/group?passports=USA,FRA,IND" \
  -H "x-api-key: YOUR_API_KEY"
Response · 200 OK
{
  "passports": ["USA", "FRA", "IND"],
  "allow": ["visa_free", "eta", "visa_on_arrival", "e_visa"],
  "total": 97,
  "excluded": { "requirement_not_allowed": 100, "incomplete_data": 4 },
  "destinations": [
    {
      "destination": "FJI",
      "group_requirement": "visa_free",
      "group_visa_free_days": 120,
      "by_passport": {
        "USA": { "requirement": "visa_free", "visa_free_days": 120 },
        "FRA": { "requirement": "visa_free", "visa_free_days": 120 },
        "IND": { "requirement": "visa_free", "visa_free_days": 120 }
      }
    }
  ]
}
Error Codes
400fewer than 2 or more than 10 passports, non-ISO3 code, unknown requirement in allow401missing api key403plan below Hobby404no data for one of the passports429monthly quota exceeded
notePerfect for retreats and group trips: one call answers “where can everyone go?”. A single group call always counts as 1 against your quota.
POST/api/v1/visa/decisionapi key · any plan

Itinerary decision — answers a trip, not a pair

Post a whole trip — passport, ordered stops with dates, transit stops, passport expiry — and get a per-step decision plus the blockers only a full itinerary reveals: days accumulated across repeat visits to the same country, passport validity measured against each arrival date, and an expiry that falls before the last exit. Every field states its granularity (pair or destination) and every gap comes back as status: "unknown" with a reason, never as a default value. verdict is no_blocker_found or blocked — it reports what the data supports, it is not legal advice.

request body
{
  "passport": "FRA",
  "residence": "VNM",
  "passport_expiry": "2027-03-01",
  "itinerary": [
    { "country": "THA", "from": "2026-09-01", "to": "2026-09-20" },
    { "country": "VNM", "from": "2026-09-20", "to": "2026-10-10" },
    { "country": "THA", "from": "2026-10-10", "to": "2026-10-25" }
  ],
  "transit": ["SGP"],
  "purpose": "tourism"
}
example
curl -X POST "https://visa.orizn.app/api/v1/visa/decision" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "passport": "FRA", "residence": "VNM", "passport_expiry": "2027-03-01", "itinerary": [ { "country": "THA", "from": "2026-09-01", "to": "2026-09-20" }, { "country": "VNM", "from": "2026-09-20", "to": "2026-10-10" }, { "country": "THA", "from": "2026-10-10", "to": "2026-10-25" } ], "transit": ["SGP"], "purpose": "tourism" }'
Response · 200 OK
{
  "data": {
    "passport_country": "FRA",
    "steps": [
      {
        "country": "THA", "from": "2026-09-01", "to": "2026-09-20", "days": 20,
        "admission": "allowed",
        "regime": { "status": "known", "value": "visa_free", "granularity": "pair",
                    "source_url": null, "last_verified": null },
        "max_stay": { "status": "known", "days": 30, "granularity": "pair", "basis": "visa_free_days" },
        "passport_validity_months": { "status": "known", "months": 6, "granularity": "destination" }
      }
    ],
    "transit": [
      { "country": "SGP", "status": "destination_rule", "granularity": "destination",
        "note": "Applies to the airport, not to this nationality. Confirm with the carrier before booking." }
    ],
    "passport": { "expiry": "2027-03-01", "status": "checked", "valid_through_itinerary": true,
                  "per_destination": [{ "country": "THA", "arrival": "2026-09-01",
                                        "required_months": 6, "satisfied": true }] },
    "blockers": [
      { "type": "stay_exceeds_allowance", "country": "THA",
        "detail": "Itinerary spends 36 day(s) in THA, above the 30-day allowance recorded for this pair." }
    ],
    "unknowns": [
      { "field": "residence",
        "reason": "Residence is not a dimension of the dataset. Decisions assume a passport holder applying as a national; residence permits may grant additional rights not modelled here." }
    ],
    "verdict": "blocked"
  },
  "meta": { "api_version": "1.0", "pairs_charged": 3, "max_steps": 12 }
}
Error Codes
400invalid JSON, non-ISO3 code, non-calendar date, to before from, or more than 12 steps401missing api key403invalid or inactive key404no visa data for that passport at all429monthly quota exceeded
noteBilling: one request per distinct country resolved (stops + transit), like /bulk — a 3-country trip costs 3, and meta.pairs_charged tells you exactly what was billed. Max 12 itinerary steps and 12 transit countries per call.
GET/api/v1/visa/changespublic — no authfree to quota

Policy change feed — temporarily unavailable

This endpoint currently returns 503 for every caller and is not part of any plan. The previous feed compared two internal tables rather than official gazettes, so it was withdrawn instead of being sold as verified change data. It will come back only once it is sourced from official publications and each event carries a named, verified source. No client integration was affected — the endpoint had never been called. Need change monitoring before then? Write to [email protected] and we will tell you honestly where we are.

example
curl "https://visa.orizn.app/api/v1/visa/changes"
Response · 200 OK
{
  "error": "The policy-change feed is being rebuilt on verified official sources.",
  "status": "unavailable",
  "contact": "[email protected]"
}
Error Codes
503feed withdrawn pending verified official sources — Retry-After: 86400
noteDo not build against this endpoint yet. Webhooks and device push subscriptions are unaffected.
GET/api/v1/visa/statspublic — no authfree to quota

Coverage statistics

Public, no auth, edge-cached for one hour. Use it on marketing pages to display live coverage numbers and a breakdown of how many pairs fall into each requirement bucket.

example
curl "https://visa.orizn.app/api/v1/visa/stats"

Response · 200 OK
{
  "coverage": {
    "visa_details": 47362,
    "passports": 199,
    "destinations": 238,
    "passport_index_pairs": 47362,
    "translations": 663068,
    "languages": 15
  },
  "supported_languages": [
    { "code": "en", "name": "English" },
    { "code": "fr", "name": "Français" }
  ],
  "requirement_distribution": {
    "visa_free": 14210,
    "visa_required": 12740,
    "e_visa": 4830,
    "visa_on_arrival": 5102,
    "eta": 2103,
    "no_admission": 600
  },
  "api_version": "1.0",
  "docs": "https://visa.orizn.app"
}
noteCache-Control: public, max-age=3600. Safe to call from a static frontend.
Passport scoring

Public mobility scores — single passport or side-by-side comparison.

GET/api/v1/visa/scorepublic — no authfree to quota

Passport mobility score

Composite mobility score and global rank for one passport. The score factors in visa-free / visa-on-arrival / e-visa access, destination diversity, and an economic weighting. Public — no key required.

Parameters
ParameterTypeRequiredDescription
passportstringYesISO 3166-1 alpha-3.
example
curl "https://visa.orizn.app/api/v1/visa/score?passport=FRA"

Response · 200 OK
{
  "passport": "FRA",
  "score": 96.4,
  "rank": 3,
  "breakdown": {
    "visa_free":       { "count": 158, "weight": 0.55 },
    "visa_on_arrival": { "count": 17, "weight": 0.15 },
    "e_visa":          { "count": 20, "weight": 0.10 },
    "diversity":       { "continents": 6, "weight": 0.10 },
    "economic":        { "score": 88,    "weight": 0.10 }
  },
  "total_accessible": 195
}
Error Codes
400missing or malformed passport param404passport not in the index500internal error
noteShape returned by computePassportScore(). Field names may evolve — depend on the documented keys, not on field order.
GET/api/v1/visa/score/comparepublic — no authfree to quota

Compare two passports

Side-by-side comparison of two passports: their individual scores, the set difference of destinations they unlock, and a normalised combined-passport score (the max destinations any single passport can reach is 238 → 1000 points). Powers dual-citizenship calculators and second-passport landing pages.

Parameters
ParameterTypeRequiredDescription
passport1stringYesFirst passport, ISO 3166-1 alpha-3.
passport2stringYesSecond passport, ISO 3166-1 alpha-3. Must differ from passport1.
example
curl "https://visa.orizn.app/api/v1/visa/score/compare?passport1=FRA&passport2=MAR"

Response · 200 OK
{
  "passport1": { "code": "FRA", "score": 96.4, "rank": 3 },
  "passport2": { "code": "MAR", "score": 41.2, "rank": 75 },
  "combined": {
    "score": 988,
    "total_accessible": 197,
    "only_passport1": ["USA", "CAN", "GBR"],
    "only_passport2": ["DZA", "TUN"],
    "both": ["JPN", "THA", "BRA"],
    "neither": ["PRK"]
  },
  "share_text": "FRA + MAR unlock 197/238 destinations — share your dual-passport score on https://visa.orizn.app"
}
Error Codes
400missing params or identical passports404either passport not in the index500internal error
Live activity

Real-time SSE stream + recent snapshot for social-proof widgets.

GET/api/v1/visa/livepublic — no authfree to quota

Live activity stream (SSE)

Server-Sent Events. Each successful /visa or /visa/check call worldwide produces an event with the passport + destination + timestamp. A `: keepalive` comment is sent every 2 s when no new traffic. Perfect for social-proof tickers on landing pages.

example
curl "https://visa.orizn.app/api/v1/visa/live"
Response · 200 OK
// Content-Type: text/event-stream

data: {"passport":"USA","destination":"JPN","timestamp":"2026-05-31T09: 14: 00Z"}

data: {"passport":"IND","destination":"ARE","timestamp":"2026-05-31T09: 13: 58Z"}

: keepalive
noteCache-Control: no-store, Connection: keep-alive. EventSource clients reconnect automatically on network drops.
GET/api/v1/visa/live/recentpublic — no authfree to quota

Recent activity snapshot

Same data as /live but as a single JSON snapshot — last 20 events, plus aggregate counters and the top-5 most popular corridors today. Edge-cached for 5 seconds.

example
curl "https://visa.orizn.app/api/v1/visa/live/recent"

Response · 200 OK
{
  "entries": [
    { "passport": "USA", "destination": "JPN", "timestamp": "2026-05-31T09: 14: 00Z" },
    { "passport": "IND", "destination": "ARE", "timestamp": "2026-05-31T09: 13: 58Z" }
  ],
  "stats": { "today": 14328, "this_week": 92041, "total": 1248302 },
  "top_corridors": [
    { "pair": "USA→JPN", "count": 412 },
    { "pair": "IND→ARE", "count": 388 },
    { "pair": "DEU→THA", "count": 301 }
  ]
}
noteCache-Control: public, max-age=5.
Push notifications

Subscribe devices (iOS / Android) to policy-change alerts.

POST/api/v1/visa/devicesapi key · any planfree to quota

Register a device for push

Subscribe an APNs / FCM device token to receive push notifications when visa policies change. Each device tracks one passport plus an optional wishlist of destinations and a set of preferences (instant alerts, weekly digest, wishlist-only, positive-changes-only).

request body
{
  "device_token": "8a3f...e2b1",
  "passport_iso3": "FRA",
  "platform": "ios",
  "bundle_id": "com.orizn-visa",
  "wishlist_iso3": ["THA", "JPN", "BRA"],
  "locale": "en",
  "tz": "Europe/Paris",
  "premium": false,
  "preferences": {
    "instant": true,
    "digest_weekly": true,
    "only_wishlist": false,
    "only_positive": false
  }
}
example
curl -X POST "https://visa.orizn.app/api/v1/visa/devices" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "device_token": "8a3f...e2b1", "passport_iso3": "FRA", "platform": "ios", "bundle_id": "com.orizn-visa", "wishlist_iso3": ["THA", "JPN", "BRA"], "locale": "en", "tz": "Europe/Paris", "premium": false, "preferences": { "instant": true, "digest_weekly": true, "only_wishlist": false, "only_positive": false } }'
Response · 200 OK
{ "device_id": 42 }
Error Codes
400missing device_token, invalid passport_iso3, or wishlist_iso3 not an array401missing api key403invalid api key500internal error
noteUpserts on device_token — re-posting the same token updates the existing subscription.
PATCH/api/v1/visa/devices/{id}api key · any planfree to quota

Update a device subscription

Update any subset of the subscription fields — passport, wishlist, locale, timezone, premium flag, preferences. At least one field is required.

request body
{
  "wishlist_iso3": ["THA", "JPN", "BRA", "PRT"],
  "preferences": { "only_wishlist": true }
}
example
curl -X PATCH "https://visa.orizn.app/api/v1/visa/devices/42" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "wishlist_iso3": ["THA", "JPN", "BRA", "PRT"], "preferences": { "only_wishlist": true } }'
Response · 200 OK
{ "updated": true, "device_id": 42 }
Error Codes
400invalid body, no fields to update, malformed passport or wishlist401missing api key403invalid api key404device not found
DELETE/api/v1/visa/devices/{id}api key · any planfree to quota

Unsubscribe a device

Permanently delete the device subscription. Use this when the user revokes notifications or uninstalls.

example
curl -X DELETE "https://visa.orizn.app/api/v1/visa/devices/42" \
  -H "x-api-key: YOUR_API_KEY"
Response · 200 OK
{ "deleted": true, "device_id": 42 }
Error Codes
401missing api key403invalid api key404device not found
Webhooks

Server-to-server policy-change delivery. HMAC-signed payloads.

GET/api/v1/visa/webhooksapi key · business+free to quota

List your webhooks

Returns every webhook subscription owned by your account, including its filters, last trigger time, and failure counter.

example
curl "https://visa.orizn.app/api/v1/visa/webhooks" \
  -H "x-api-key: YOUR_API_KEY"
Response · 200 OK
{
  "webhooks": [
    {
      "id": 17,
      "url": "https://your-app.com/orizn-hook",
      "passport_filter": ["FRA"],
      "destination_filter": null,
      "active": true,
      "created_at": "2026-05-29T09: 14: 00Z",
      "last_triggered_at": "2026-05-31T08: 02: 11Z",
      "failures": 0
    }
  ]
}
Error Codes
401missing api key403invalid key or plan below Business
POST/api/v1/visa/webhooksapi key · business+free to quota

Create a webhook

Register a URL to receive POSTs whenever a policy change matches your filter. The response contains a one-time secret — store it now, we won't show it again. Sign verifications use HMAC-SHA256 of the raw body keyed by the secret.

request body
{
  "url": "https://your-app.com/orizn-hook",
  "passport_filter": ["FRA"],
  "destination_filter": ["THA", "JPN"]
}
example
curl -X POST "https://visa.orizn.app/api/v1/visa/webhooks" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-app.com/orizn-hook", "passport_filter": ["FRA"], "destination_filter": ["THA", "JPN"] }'
Response · 200 OK
{
  "webhook": {
    "id": 17,
    "url": "https://your-app.com/orizn-hook",
    "passport_filter": ["FRA"],
    "destination_filter": ["THA", "JPN"],
    "active": true,
    "secret": "whsec_4f2a91c8...e2b1",
    "created_at": "2026-05-31T09: 14: 00Z"
  },
  "message": "Webhook created. Store the secret — it won't be shown again."
}
Error Codes
400invalid body or url401missing api key403invalid key or plan below Business
noteReturns 201 Created.
DELETE/api/v1/visa/webhooks?id={id}api key · business+free to quota

Delete a webhook

Permanently delete a webhook subscription you own.

Parameters
ParameterTypeRequiredDescription
idintYesWebhook id, from list/create.
example
curl -X DELETE "https://visa.orizn.app/api/v1/visa/webhooks?id=42?id=42" \
  -H "x-api-key: YOUR_API_KEY"
Response · 200 OK
{ "deleted": true, "id": 17 }
Error Codes
400missing id query param401missing api key403invalid key or plan below Business404webhook not found or not owned by you
Team keys

Per-environment subkeys sharing the owner account quota.

GET/api/v1/visa/team-keysapi key · business+free to quota

List team keys

Return every team subkey under your account. Each team key inherits its owner's plan and shares the same monthly quota — useful for isolating environments or attributing usage.

example
curl "https://visa.orizn.app/api/v1/visa/team-keys" \
  -H "x-api-key: YOUR_API_KEY"
Response · 200 OK
{
  "team_keys": [
    {
      "id": 7,
      "api_key": "orizn_visa_team_a1b2c3...",
      "name": "ci-staging",
      "active": true,
      "requests_month": 14823,
      "requests_total": 184238,
      "created_at": "2026-05-12T11: 04: 00Z"
    }
  ]
}
Error Codes
401missing api key403invalid key or plan below Business
POST/api/v1/visa/team-keysapi key · business+free to quota

Create a team key

Mint a new team subkey. The key is prefixed orizn_visa_team_ and immediately usable in the x-api-key header. Shared quota with the owner account.

request body
{ "name": "ci-staging" }
example
curl -X POST "https://visa.orizn.app/api/v1/visa/team-keys" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "ci-staging" }'
Response · 200 OK
{
  "team_key": {
    "id": 7,
    "api_key": "orizn_visa_team_a1b2c3...",
    "name": "ci-staging",
    "active": true,
    "requests_month": 0,
    "requests_total": 0,
    "created_at": "2026-05-31T09: 14: 00Z"
  },
  "message": "Team key created. It shares the monthly quota of the owner account."
}
Error Codes
400missing/empty name401missing api key403invalid key or plan below Business
noteReturns 201 Created.
DELETE/api/v1/visa/team-keys?id={id}api key · business+free to quota

Deactivate a team key

Soft delete — flips active to false. Existing requests with the key start returning 403 immediately.

Parameters
ParameterTypeRequiredDescription
idintYesTeam key id.
example
curl -X DELETE "https://visa.orizn.app/api/v1/visa/team-keys?id=42?id=42" \
  -H "x-api-key: YOUR_API_KEY"
Response · 200 OK
{ "deactivated": true, "team_key": { "id": 7, "name": "ci-staging" } }
Error Codes
400missing id query param401missing api key403invalid key or plan below Business404not found, not owned by you, or already inactive
Account & billing

Self-serve signup, key rotation, and Stripe-hosted billing flow.

POST/api/v1/visa/registerpublic — no authfree to quota

Sign up (get a free API key)

Self-serve signup. Returns a free-plan API key (50 req/month). Idempotent on email — re-registering returns the existing key instead of creating a duplicate.

request body
{ "name": "Ada Lovelace", "email": "[email protected]" }
example
curl -X POST "https://visa.orizn.app/api/v1/visa/register" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Ada Lovelace", "email": "[email protected]" }'
Response · 200 OK
{
  "api_key": "orizn_visa_a06113a2e4f0...",
  "plan": "free",
  "message": "API key created successfully."
}
Error Codes
400invalid JSON, missing name/email, malformed email500database error
GET/api/v1/visa/auth/ensure-keysession cookiefree to quota

Get the logged-in user's API key

Dashboard helper: validates the orizn_token cookie against api.orizn.app/auth/me, then returns the matching visa_api_users row — creating one on the free plan if the Orizn account doesn't have one yet.

example
curl "https://visa.orizn.app/api/v1/visa/auth/ensure-key" \
  --cookie "orizn_token=YOUR_SESSION"
Response · 200 OK
{
  "user": {
    "id": 42,
    "orizn_id": "01HZ...",
    "email": "[email protected]",
    "name": "Ada Lovelace",
    "photo": "https://...",
    "plan": "pro",
    "api_key": "orizn_visa_a06113...",
    "requests_month": 14238,
    "requests_total": 412938,
    "monthly_limit": 250000,
    "created_at": "2026-04-12T11: 04: 00Z"
  }
}
Error Codes
400could not resolve user identity from session401missing or invalid orizn_token cookie
POST/api/v1/visa/auth/regenerate-keysession cookiefree to quota

Rotate the API key

Generate a new key and invalidate the old one. Use this when a key may have leaked. Affects only the owner row, not team subkeys.

example
curl -X POST "https://visa.orizn.app/api/v1/visa/auth/regenerate-key" \
  --cookie "orizn_token=YOUR_SESSION" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200 OK
{
  "api_key": "orizn_visa_b71224...",
  "message": "API key rotated. Update your clients."
}
Error Codes
400could not resolve user identity401missing or invalid session404no active API account for this user
POST/api/v1/visa/stripe/checkoutsession cookiefree to quota

Start a paid-plan checkout

Returns a Stripe Checkout session URL. Redirect the user to it; on success Stripe pings our webhook which upgrades their plan. Annual billing with a valid affiliate_id grants a 30-day trial.

request body
{
  "plan": "pro",
  "billing": "monthly",
  "affiliate_id": "aff_4f2a91c8"
}
example
curl -X POST "https://visa.orizn.app/api/v1/visa/stripe/checkout" \
  --cookie "orizn_token=YOUR_SESSION" \
  -H "Content-Type: application/json" \
  -d '{ "plan": "pro", "billing": "monthly", "affiliate_id": "aff_4f2a91c8" }'
Response · 200 OK
{ "url": "https://checkout.stripe.com/c/pay/cs_test_..." }
Error Codes
400invalid plan, annual unsupported for this plan, could not resolve email401missing or invalid session
noteplan: one of hobby, starter, pro, business. billing: monthly | annual.
POST/api/v1/visa/stripe/portalsession cookiefree to quota

Open the Stripe billing portal

Returns a one-time URL into Stripe's customer portal — invoices, payment method, plan changes, cancellation.

example
curl -X POST "https://visa.orizn.app/api/v1/visa/stripe/portal" \
  --cookie "orizn_token=YOUR_SESSION" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200 OK
{ "url": "https://billing.stripe.com/p/session/..." }
Error Codes
400could not resolve email401missing or invalid session404no Stripe customer (likely still on the free plan)
Affiliate program

Earn 15% commission on referred subscriptions — web + iOS.

POST/api/v1/visa/affiliate/registerpublic — no authfree to quota

Become an affiliate

Open a partner account. We mint an affiliate_id (format aff_<8 hex>) you embed in checkout URLs to earn 15% commission on every subscription you refer. Idempotent on email.

request body
{
  "name": "Ada Lovelace",
  "email": "[email protected]",
  "website": "https://travelblog.example",
  "payment_method": "paypal",
  "payment_info": "[email protected]",
  "source": "web"
}
example
curl -X POST "https://visa.orizn.app/api/v1/visa/affiliate/register" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Ada Lovelace", "email": "[email protected]", "website": "https://travelblog.example", "payment_method": "paypal", "payment_info": "[email protected]", "source": "web" }'
Response · 200 OK
{
  "affiliate_id": "aff_4f2a91c8",
  "dashboard_url": "https://visa.orizn.app/affiliate?aff=aff_4f2a91c8&email=ada%40example.com",
  "message": "Affiliate account ready. Share your link to earn 15% commission."
}
Error Codes
400invalid JSON, missing fields, malformed email/website/payment_method409unique constraint race — retry500internal error
notewebsite may be the literal string ios-app for app-store affiliates. Returns 201 on first registration, 200 if the affiliate already exists.
POST/api/v1/visa/affiliate/track-clickpublic — no authfree to quota

Record an affiliate click

Increment the click counter for an affiliate_id. Fire-and-forget from your landing pages and ad creatives.

request body
{ "affiliate_id": "aff_4f2a91c8" }
example
curl -X POST "https://visa.orizn.app/api/v1/visa/affiliate/track-click" \
  -H "Content-Type: application/json" \
  -d '{ "affiliate_id": "aff_4f2a91c8" }'
Response · 200 OK
{ "ok": true }
Error Codes
400missing or malformed affiliate_id404affiliate not found or inactive500internal error
POST/api/v1/visa/affiliate/apply-referralapi key · any planfree to quota

Attach a referral to a user

Bind YOUR account to a referrer after sign-up — typical use is the iOS app collecting a referral code post-registration. Requires your API key: the referred account is the one owning the key. Sets referred_by; the referrer earns 15% on your future subscriptions.

request body
{ "referral_code": "aff_4f2a91c8" }
example
curl -X POST "https://visa.orizn.app/api/v1/visa/affiliate/apply-referral" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "referral_code": "aff_4f2a91c8" }'
Response · 200 OK
{
  "ok": true,
  "message": "Referral applied successfully. The referrer will earn 15% commission on your future subscriptions.",
  "referred_by": "aff_4f2a91c8"
}
Error Codes
400missing fields, bad format, or self-referral attempt401missing api key403invalid api key404invalid or inactive referral code500internal error
GET/api/v1/visa/affiliate/statspublic — no authfree to quota

Affiliate dashboard data

Lifetime + this-month performance for an affiliate account: clicks, conversions, referred users, revenue, commission breakdowns by product and platform, and the 20 most recent transactions. Authenticated via either the orizn_token cookie (Orizn account login) or affiliate_id + email query params.

Parameters
ParameterTypeRequiredDescription
affiliate_idstringNoRequired when no session cookie. Format aff_<8 hex>.
emailstringNoRequired when no session cookie. Must match the affiliate's email.
example
curl "https://visa.orizn.app/api/v1/visa/affiliate/stats"
Response · 200 OK
{
  "affiliate_id": "aff_4f2a91c8",
  "active": true,
  "source": "web",
  "clicks": 1423,
  "conversions": 28,
  "referred_users": 28,
  "revenue_cents": 419800,
  "commission_rate": 0.15,
  "commission_cents": 62970,
  "this_month": { "transactions": 7, "revenue_cents": 138600, "commission_cents": 20790 },
  "by_product": {
    "orizn.visa.premium.annual":  { "transactions": 18, "commission_cents": 47100 },
    "orizn.visa.premium.monthly": { "transactions": 10, "commission_cents": 15870 }
  },
  "by_platform": { "ios": 22, "web": 6 },
  "recent_transactions": [
    { "product": "orizn.visa.premium.annual", "platform": "ios", "amount_cents": 19800, "commission_cents": 2970, "date": "2026-05-30T18: 14: 00Z" }
  ],
  "payment_method": "paypal",
  "payment_info_masked": "a***@paypal.com",
  "links": {
    "visa_app": "https://visa.orizn.app?aff=aff_4f2a91c8",
    "guide":    "https://visa.orizn.app/guide?aff=aff_4f2a91c8",
    "extension": "https://visa.orizn.app/extension?aff=aff_4f2a91c8",
    "api":      "https://visa.orizn.app/api?aff=aff_4f2a91c8"
  }
}
Error Codes
401no cookie and no affiliate_id + email query404affiliate not found500internal error
what the /visa endpoint returns

30 Data Points

Every GET /api/v1/visa response carries up to 32 fields. Fields not relevant to a given pair are null, an empty array, or omitted — you can rely on the shape, not on presence.

Core (always present)
fieldTypeDescription
passportstringISO 3166-1 alpha-3 (e.g. FRA)
destinationstringISO 3166-1 alpha-3 (e.g. JPN)
requirementenumvisa_free | visa_required | e_visa | visa_on_arrival | eta | no_admission
visa_free_daysint | nullNumber of days allowed without a visa (null if visa required)
visa_requiredboolTrue if any visa formality is needed
descriptionstringLocalized human-readable summary
documents_requiredstring[]Documents to bring/submit
processstring[]Step-by-step application process
tipsstring[]Travel tips
country_infoobjectCurrency, language, timezone, capital
verifiedboolTrue only when an official source confirms this exact pair
sourcestring | nullWhere the value came from (e.g. official, manual)
source_urlstring | nullOfficial page this pair was read from — null until the pair has been re-sourced
last_verified_atstring | nullISO 8601 date source_url was last read — null whenever source_url is null
requirement_statusstringOnly on legally unsettled pairs. Currently the single value uncertain — key absent otherwise
requirement_status_notestring | nullPlain-English reason the regime is unsettled, safe to show to a traveler
Extended intelligence (optional, only when relevant)
fieldTypewhat it tells you
transit_visaobjectTransit visa rules and free transit hours at top hubs
passport_validity_monthsintMinimum passport validity required at entry
visa_feeobjectSingle-entry and multiple-entry visa cost, with currency
processing_daysobjectStandard / express / rush processing times
photo_specsobjectPhoto dimensions (mm), background, glasses & head-covering rules
vaccinations_requiredstring[]Mandatory vaccines (e.g. yellow_fever)
insurance_requiredobjectMinimum travel insurance coverage required
dual_nationality_warningsstring[]Warnings for dual-nationals (e.g. military service)
stamp_warningsstring[]Passport stamps that may block entry
minor_rulesobjectRules for travelers under 18
overstay_penaltyobjectFine per day, ban duration, criminal liability
entry_by_modeobjectDifferent stay limits for air / land / sea arrivals
remote_work_visaobjectDigital nomad visa availability, duration, fee
extension_rulesobjectWhether the stay can be extended, max days, fee, where
reciprocity_historyobject[]Historical policy changes between the two countries
safetyobjectTravel advisory level (1–4) with source and last update
best_apply_periodstringRecommended application window
health_requirementsobjectCOVID test, vaccination proof, quarantine, screenings
embassy.your_embassy_at_destinationobjectYour country's embassy at the destination — emergencies
embassy.visa_application_embassyobjectDestination's embassy in your country — where to apply
provenance, freshness, and what we do when the law is unclear

Data quality

Provenance — where a value came from

Every /visa response carries source_url and last_verified_at: the official page a pair was read from, and the date it was last read. Cite them in your own UI — that is what they are for.

Both fields are null on pairs that have not been re-sourced yet, and that is most of them. 56 of the 199 passport countries currently have an official source on file; the rest are answered from the consolidated dataset without a citation. A null here means we cannot show you a document, not that the answer is wrong — but if your product makes a claim a user could act on, treat a null as unverified and say so.

Coverage grows; the numbers above are a snapshot, not a ceiling. Do not branch on the count — branch on the field.

requirement_status: "uncertain" — when the law itself is unsettled

Most visa datasets answer every pair with a definite requirement, because their schema has no way to say anything else. Some pairs do not have a definite answer. When a country leaves a bloc whose freedom-of-movement treaty was the legal basis for entry, what happens at the border and what the law guarantees stop being the same thing. Burkina Faso, Mali and Niger left ECOWAS on 2025-01-29: visa-free movement between them and the remaining member states now rests on unilateral, revocable declarations rather than on a treaty.

On those pairs the response carries requirement_status: "uncertain" and a requirement_status_note you can show to a traveler verbatim. requirement itself is untouched — it stays visa_free, because that is what happens at the border today. The status says the right no longer exists; the requirement says the practice continues. Both are true, and a comparison test found no other source that reports the difference.

Because it is a separate, additive field, a client that ignores it sees no change at all — nothing breaks, you simply keep the old blind spot. The key is absent on every pair that is not affected, so test for presence, not for a value. Currently uncertain is the only value.

What /decision cannot decide

Three gaps, stated here because an endpoint you are meant to defend a decision with has to name its blind spots. They also come back inside every response, in the unknowns array, per field and per country — you never have to remember this page at runtime.

  • Transit is a dead end on most of the map. A transit rule exists for 44 of 238 destinations and is recorded per transited country, not per nationality — so even where it exists it never says whether this passport may transit. Every other destination returns status: "unknown". Confirm transit with the carrier, always.
  • residence and purpose are accepted, echoed back, and not modelled. The dataset has no residence dimension and covers short-stay tourism only. Send a residence different from the passport, or a purpose other than tourism, and you get an entry in unknowns saying exactly that — a residence permit may grant rights this endpoint cannot see.
  • Length of stay is only computed where a numeric allowance exists. visa_free_days is the one figure the dataset holds; where the permitted stay depends on the permit issued, max_stay is unknown rather than a parsed guess, and no accumulation blocker can be raised for that country.

verdict: "no_blocker_found" is worded the way it is on purpose. It means the checks that could run found nothing — not that the trip is cleared.

what each tier unlocks

Plan matrix

featureFreehobby $9starter $49pro $199business $699
Monthly requests5010,00030,000250,0001,000,000
Burst rate (req/s)102550100200
/visa, /visa/check✓ (en only)✓ (15 langs)✓ (15 langs)
/visa — extended fieldsstubsstubsall except remote-work, reciprocityall 32all 32
/visa/bulk (all destinations)
/visa/group (multi-passport)
/visa/decision (itinerary)
Device push subscriptions
Webhooks (server-to-server)
Team subkeys
Score, compare, stats, live✓ (public)

Enterprise plans add unlimited requests, custom burst rates, on-prem deployment, dedicated IPs, and a 99.95% SLA. Talk to us.

quotas, burst, reset

5. Rate Limits

Quotas reset on the 1st of each calendar month (UTC). Only the five counted endpoints — /visa, /visa/check, /visa/bulk, /visa/group and /visa/decision — increment your monthly counter. Score, stats, live, devices, webhooks and team-keys management are free against your quota.

PlanPriceMonthly LimitRatehighlights
Free$05010 req/s/check + /visa (en only) + public endpoints
Starter$49/mo30,00050 req/s+ all 15 languages, extended fields
Pro$199/mo250,000100 req/s+ /bulk, remote-work-visa, reciprocity history
Business$699/mo1,000,000200 req/s+ webhooks, team subkeys, SLA
EnterpriseCustomUnlimitedCustom+ on-prem, dedicated IP, 99.95% SLA

Over your quota you receive HTTP 429 with X-RateLimit-Reset indicating when the next window opens.

observability without parsing the body

Response headers

headervaluewhere
X-RateLimit-LimitMonthly quota for your plan/visa, /visa/check, /visa/bulk
X-RateLimit-RemainingRemaining calls in this month/visa, /visa/check, /visa/bulk
X-RateLimit-ResetISO timestamp of next reset (sent only on 429)/visa, /visa/check, /visa/bulk
X-PlanYour current plan (free, starter, pro, business, enterprise)/visa, /visa/check, /visa/bulk
X-Powered-Byorizn Visa API v1All authenticated endpoints
X-Orizn-UpgradeURL to the upgrade page (only on free)/visa, /visa/check
VaryAccept (set when Markdown negotiation is available)/visa, /visa/check
Cache-Controlpublic, max-age=3600 (stats) · max-age=5 (live/recent) · no-store (live)Public endpoints
predictable failures

6. Error Codes

Errors return a JSON body with a stable shape. Use the HTTP status for routing, the body for diagnostics, and the per-endpoint error pills above when you want to know exactly which 4xx code comes from which condition.

CodeMeaningtypical cause
400Bad RequestMissing or malformed parameter — non-ISO3 code, unsupported lang, invalid body
401UnauthorizedMissing x-api-key header / api_key query / orizn_token cookie
403ForbiddenInvalid or inactive key, or your plan does not include this endpoint
404Not FoundNo data for this pair / resource not owned by you
409ConflictUnique-constraint race (affiliate signup) — safe to retry
429Too Many RequestsMonthly quota or burst rate exceeded
500Internal ErrorTransient backend issue — retry with exponential backoff
{
  "error": "Monthly limit exceeded (50 req/month on free plan). Upgrade at https://visa.orizn.app",
  "plan": "free",
  "limit": 50,
  "upgrade_url": "https://visa.orizn.app/visa-api/pricing"
}
15 languages, one query param

7. Supported Languages

Pass lang as a query parameter (or in the JSON body for POSTs). Defaults to en. Non-English requires the Starter plan or above.

enEnglish
frFrançais
esEspañol
ptPortuguês
deDeutsch
itItaliano
ja日本語
ko한국어
zh中文
ruРусский
arالعربية
hiहिन्दी
thไทย
viTiếng Việt
tlTagalog
typed, retried, batteries included

Official SDKs

Hand-written wrappers — fully typed, with retries, rate-limit backoff, and a structured error type. Same 30 data points everywhere.

npm install orizn              # JavaScript / TypeScript
pip install orizn              # Python
cargo add orizn                # Rust
npx orizn-visa-mcp             # MCP server (Claude, Cursor, Codex)
pip install langchain-orizn    # LangChain Python
npm install @orizn/langchain   # LangChain JS
what's new

8. Changelog

v1.1May 2026

30 data points + full endpoint reference

  • 21 new optional fields on /visa: transit, fees, photo specs, vaccinations, insurance, embassies, safety, overstay penalties, reciprocity history, remote-work visa, extension rules.
  • New MCP tool check_transit_visa; enriched descriptions so agents pick the right tool.
  • Docs page now covers all 28 public endpoints — visa data, scoring, live, devices, webhooks, team keys, account, affiliate.
  • SDK v1.1: [email protected] (npm), orizn==1.1.0 (PyPI), [email protected].
  • Backward-compatible — old clients keep working, new fields are additive.
v1.0May 2026

Launch

  • 47,362 passport/destination pairs covered
  • 15 supported languages (en, fr, es, pt, de, it, ja, ko, zh, ru, ar, hi, th, vi, tl)
  • Endpoints: check, visa, bulk, changes, stats, register
  • Plans: Free (50/mo), Starter ($49), Pro ($199), Business ($699), Enterprise
  • Dashboard with usage analytics, billing and interactive documentation