curl · any language
REST API
The layer everything else sits on. Four endpoints, one header, no SDK required.
What was executed to write this page
Every request and response on this page was executed with curl against https://visa.orizn.app on the day of writing. The error bodies are verbatim from /api/v1/visa/check; /api/v1/visa returns the same status codes with different wording, so match on the status, not on the message.
Before you start
- An Orizn API key. Free tier: 50 requests/month, no credit card — get one at visa.orizn.app/visa-api. It is 5 requests until you confirm your email, so click the link in the confirmation mail.
- curl, or any HTTP client. There is nothing else to install.
- Country codes are ISO 3166-1 alpha-3 — FRA, JPN, USA. Alpha-2 codes (FR, JP) are rejected with a 400, they are not silently converted.
Walkthrough
1. Confirm the API is reachable, without a key
/stats is public. If this returns, your network is fine and any later failure is about the key.
curl -s "https://visa.orizn.app/api/v1/visa/stats"{
"coverage": {
"visa_details": 40027,
"passports": 199,
"destinations": 202,
"passport_index_pairs": 39999,
"translations": 560378,
"languages": 15
},
"supported_languages": [
{ "code": "fr", "name": "Francais" }, { "code": "en", "name": "English" },
{ "code": "es", "name": "Espanol" }, { "code": "pt", "name": "Portugues" },
{ "code": "de", "name": "Deutsch" }, { "code": "it", "name": "Italiano" },
{ "code": "ja", "name": "Japanese" }, { "code": "ko", "name": "Korean" },
{ "code": "zh", "name": "Chinese" }, { "code": "ru", "name": "Russian" },
{ "code": "ar", "name": "Arabic" }, { "code": "hi", "name": "Hindi" },
{ "code": "th", "name": "Thai" }, { "code": "vi", "name": "Vietnamese" },
{ "code": "tl", "name": "Filipino" }
],
"api_version": "1.0"
}2. Your first authenticated call
The key goes in an x-api-key header. There is no OAuth dance, no token exchange.
export ORIZN_API_KEY=orizn_visa_...
curl -s -H "x-api-key: $ORIZN_API_KEY" \
"https://visa.orizn.app/api/v1/visa/check?passport=FRA&destination=JPN"{
"passport": "FRA",
"destination": "JPN",
"requirement": "visa_free",
"visa_free_days": 90,
"visa_required": false,
"last_verified": "2026-05-08"
}3. The full record
/check is the cheap yes-or-no. /api/v1/visa is the whole file on that pair.
curl -s -H "x-api-key: $ORIZN_API_KEY" \
"https://visa.orizn.app/api/v1/visa?passport=FRA&destination=JPN""description": "France citizens can enter Japan without a visa for a stay of up to 90 days.
A valid passport is required.",
"documents_required": [
"Valid passport (6 months minimum)",
"Return or onward ticket",
"Proof of accommodation",
"Proof of sufficient funds"
],
"process": [
"No prior formalities required",
"Present valid passport upon arrival",
"Entry stamp will be issued at border control"
],
"visa_types": [
{ "nom": "Visa exemption (tourism)", "cout": "Free", "duree": "90 days",
"description": "Tourism, family visit, short business" },
{ "nom": "Working Holiday Visa (WHV)", "cout": "Free", "duree": "1 year",
"description": "For ages 18-30, annual quota of 1,500 places" }
],
"extension": { "possible": true, "cout": "4,000 JPY", "duree": "90 additional days",
"lieu": "Regional immigration office" }4. The same record in another language
Add &lang=. All 15 languages are on every plan, free included — it is the written fields that get translated, not the codes.
curl -s -H "x-api-key: $ORIZN_API_KEY" \
"https://visa.orizn.app/api/v1/visa?passport=FRA&destination=JPN&lang=ja"Franceの国民は、最大90日間、ビザなしでJapanに入国できます。有効なパスポートが必要です。
["有効なパスポート(残存6ヶ月以上)", "帰国または乗り継ぎの航空券",
"宿泊証明", "十分な資金の証明"]And in French, from the same pair — these are written translations, not machine output at request time:
"Les ressortissants français bénéficient d'une exemption de visa de 90 jours pour le
Japon. Un accord bilatéral de longue date permet les séjours touristiques sans visa."5. The four endpoints
- GET /api/v1/visa/check?passport=&destination= — requirement, visa-free days, last_verified. One request. Any key.
- GET /api/v1/visa?passport=&destination=&lang= — the full record, up to ~30 fields. One request. Any key.
- GET /api/v1/visa/bulk?passport=&destination=JPN,USA,THA — up to 25 destinations for one passport. Each destination returned counts as one request. Hobby plan and above.
- GET /api/v1/visa/stats — coverage of the dataset. No key, no quota.
Quota state is on every authenticated response, so you never have to guess where you stand:
x-plan: hobby
x-ratelimit-limit: 10000
x-ratelimit-remaining: 9783When it goes wrong
Every failure the integration can hand you, with the message it actually prints.
No x-api-key header
401{"error":"API key required","message":"Get your free API key in 10 seconds — no credit card needed.","signup_url":"https://visa.orizn.app/visa-api","free_tier":"50 requests/month"}Send the header. Note the machine-readable signup_url — an agent can surface it without parsing prose.
Key unknown or deactivated
403{"error":"Invalid API key","message":"Get your free API key at https://visa.orizn.app/visa-api"}Check the dashboard. Whitespace around the key is the usual cause.
Alpha-2 codes, or a missing parameter
400{"error":"Required: ?passport=FRA&destination=JPN (ISO3 codes)"}Use ISO 3166-1 alpha-3. FR is not FRA.
Valid codes, but no record for that pair
404{"error":"Pair not found"}The dataset covers 199 passports into 202 destinations, which is not every conceivable pair. Treat 404 as 'unknown', never as 'no visa needed'.
Monthly quota spent
429{"error":"Monthly limit exceeded (50 req/month on free plan). Upgrade at https://visa.orizn.app"} with X-RateLimit-Limit, X-RateLimit-Remaining: 0 and X-RateLimit-ResetBack off until the monthly reset, or upgrade.
Many distinct destinations for one passport in a day
429{"error":"Anti-abuse limit: N distinct destinations already queried for passport XXX today (cap C on <plan> plan). Upgrade at ..."} with a Retry-After header pointing at the next UTC midnightThis is the anti-scraping guard, not your monthly quota. Legitimate full-passport pulls should use /bulk.
The 401, 403 and 404 bodies were reproduced live against /api/v1/visa/check and pasted verbatim. /api/v1/visa returns the same codes with different messages — branch on the status code. The 400 is unreachable with an invalid key, since the key is checked before the parameters. The two 429 bodies are quoted from the API's source: the key used to write this page is uncapped.
End to end: a booking-flow entry check
The common shape — the traveller has picked a flight, and you need to tell them what they need before they pay.
#!/usr/bin/env bash
set -euo pipefail
: "${ORIZN_API_KEY:?set ORIZN_API_KEY}"
PASSPORT=${1:-FRA}
DEST=${2:-JPN}
LANG_CODE=${3:-en}
http=$(curl -s -o /tmp/visa.json -w "%{http_code}" \
-H "x-api-key: $ORIZN_API_KEY" \
"https://visa.orizn.app/api/v1/visa?passport=$PASSPORT&destination=$DEST&lang=$LANG_CODE")
case "$http" in
200) python3 -c "import json;d=json.load(open('/tmp/visa.json'))['data'];\
print(d['requirement'], d.get('visa_free_days'));\
print('\n'.join(d['documents_required']))" ;;
401|403) echo "Key problem — see /tmp/visa.json"; exit 1 ;;
404) echo "No record for $PASSPORT -> $DEST. Do NOT assume visa-free."; exit 2 ;;
429) echo "Rate limited. Retry-After applies."; exit 3 ;;
*) echo "Unexpected $http"; exit 4 ;;
esacThe branch that matters is 404. A pair with no record is unknown, and rendering “no visa needed” for unknown is how a traveller gets turned around at the gate. Fail loud.
Source & reference
Other integrations
50 requests a month, no credit card
All 15 languages included on the free plan. Hit a wall with this tutorial? Mail [email protected] — a tutorial that does not work is a bug.