Metadata-Version: 2.4
Name: orizn
Version: 1.2.0
Summary: Official Python SDK for the Orizn Visa API — 39,999 passport-destination pairs in 15 languages
Project-URL: Homepage, https://visa.orizn.app
Project-URL: Repository, https://github.com/MattJeff/orizn-py
Project-URL: Documentation, https://visa.orizn.app/visa-api/dashboard/docs
Project-URL: Issues, https://github.com/MattJeff/orizn-py/issues
Author-email: Orizn <api@orizn.app>
License-Expression: MIT
License-File: LICENSE
Keywords: api,immigration,orizn,passport,sdk,travel,visa
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Requires-Dist: requests>=2.28.0
Description-Content-Type: text/markdown

# orizn

Official Python SDK for the [Orizn Visa API](https://visa.orizn.app/visa-api).

Visa requirements for **39,999 passport-destination pairs**, in **15 languages**, with up to
**32 data points per visa** — fees, processing times, photo specs, transit visas, embassies,
overstay penalties, safety advisories.

## Install

```bash
pip install orizn
```

## Quick start

```python
from orizn import Orizn

# Free key, 10 seconds, no credit card → https://visa.orizn.app/visa-api
client = Orizn(api_key="orizn_visa_...")   # or: export ORIZN_API_KEY=...

r = client.check("FRA", "JPN")
print(r.requirement)      # "visa_free"
print(r.visa_free_days)   # 90

visa = client.get_visa("USA", "CHN", lang="fr")   # all 15 languages, free plan included
print(visa.description)
print(visa.documents_required)
print(visa.process)
```

That runs as-is once `ORIZN_API_KEY` is set. **Every visa endpoint needs a key** — the free
tier is 50 requests/month (5 until you confirm your email).

## Get a key

| | |
|---|---|
| Free key, instant | https://visa.orizn.app/visa-api |
| Upgrade to Hobby — $9/mo, 10,000 req | [billing](https://visa.orizn.app/visa-api/login?next=%2Fvisa-api%2Fdashboard%2Fbilling%3Fplan%3Dhobby) |

| Plan | Price | Requests/month |
|---|---|---|
| Free | $0 | 50 |
| Hobby | $9 | 10,000 |
| Starter | $49 | 30,000 |
| Pro | $199 | 250,000 |
| Business | $699 | 1,000,000 |

All 15 languages are available on **every** plan, free included. What paid plans add is volume
and the extended data points (fees, transit, embassies, safety, …).

## Methods

| Method | Auth | Description |
|---|---|---|
| `check(passport, destination)` | Key | Requirement, visa-free days, last-verified date |
| `get_visa(passport, destination, lang="en")` | Key | Full record — up to 32 data points |
| `bulk(passport, destinations, lang="en")` | Key (Hobby+) | Up to 25 destinations in one call |
| `stats()` | None | Public coverage statistics |

`bulk()` requires an explicit list of destinations (max 25 per call), and each destination
returned counts as one request against your quota.

```python
for row in client.bulk("FRA", ["JPN", "USA", "THA"]):
    print(row.destination, row.requirement, row.visa_free_days)
```

The client is a context manager, so the HTTP session gets closed:

```python
with Orizn() as client:
    print(client.check("DEU", "BRA").requirement)
```

## Errors

Four typed exceptions, each carrying the URL that unblocks you:

```python
from orizn import OriznAuthError, OriznForbiddenError, OriznRateLimitError, OriznNotFoundError

try:
    visa = client.get_visa("FRA", "JPN")
except OriznAuthError:        # 401 — no key. Message links to the free key page.
    ...
except OriznForbiddenError:   # 403 — invalid key, or plan too low for this call.
    ...
except OriznRateLimitError as e:   # 429 — quota spent. e.retry_after when the API sends it.
    ...
except OriznNotFoundError:    # 404 — no data for that pair.
    ...
```

All four subclass `OriznError`, which also covers timeouts and connection failures.

Bad country codes and unsupported languages raise `ValueError` **before** the request goes out —
the API bills a request before validating params, so a typo would otherwise cost you quota.

## What's in `get_visa()`

Fields your plan doesn't include come back as `None`.

### Core (always present)

| Field | Type | Description |
|---|---|---|
| `passport` / `destination` | `str` | ISO 3166-1 alpha-3 |
| `requirement` | `VisaRequirement` | `visa_free` \| `visa_required` \| `e_visa` \| `visa_on_arrival` \| `eta` \| `no_admission` |
| `visa_free_days` | `int \| None` | Max visa-free stay, in days |
| `visa_required` | `bool` | True if any visa formality applies |
| `description` | `str` | Localized summary |
| `documents_required` | `list[str]` | Documents to bring/submit |
| `process` | `list[str]` | Step-by-step application process |
| `tips` | `list[str]` | Travel tips |
| `visa_types` | `list[VisaType]` | Visa categories available — `name`, `cost`, `duration`, `description` |
| `extension`, `processing_time`, `cost`, `validity`, `max_stay` | `str \| None` | Localized free-text details |
| `country_info` | `CountryInfo` | Currency, language, timezone, capital |
| `verified` | `bool` | Verified against an official source |
| `source` | `str \| None` | Where the record came from |
| `plan`, `remaining` | `str \| int \| None` | Your plan and requests left, read from the response headers |

### Extended intelligence (plan-gated)

| Field | Type | What it tells you |
|---|---|---|
| `transit_visa` | `TransitVisa` | Transit rules + free transit hours at major hubs (DXB, IST, DOH, SIN, …) |
| `passport_validity_months` | `int` | Minimum passport validity at entry |
| `visa_fee` | `VisaFee` | Single- and multiple-entry cost, with currency |
| `processing_days` | `ProcessingDays` | Standard / express / rush |
| `photo_specs` | `PhotoSpecs` | Dimensions, background, glasses & head-covering rules |
| `vaccinations_required` | `list[str]` | Mandatory vaccines |
| `insurance_required` | `InsuranceRequired` | Minimum travel insurance coverage |
| `dual_nationality_warnings` | `list[str]` | Warnings for dual nationals |
| `stamp_warnings` | `list[str]` | Passport stamps that may block entry |
| `minor_rules` | `MinorRules` | Rules for under-18s |
| `overstay_penalty` | `OverstayPenalty` | Fine per day, max fine, criminal liability, and `ban_duration` — days keyed by scenario (`{"under_30_days": 365, …}`), since the API has no single number |
| `entry_by_mode` | `EntryByMode` | `air` / `land` / `sea`, each the API's raw object (`visa_free_days`, `esta_required`, `e_visa_available`, … — the keys vary by pair) |
| `remote_work_visa` | `RemoteWorkVisa` | Digital nomad visa availability, duration, fee |
| `extension_rules` | `ExtensionRules` | Whether the stay can be extended, and how |
| `reciprocity_history` | `list[ReciprocityChange]` | Past policy changes between the two countries |
| `safety` | `SafetyInfo` | Advisory level (1–4), source, last update |
| `best_apply_period` | `str` | Recommended application window |
| `health_requirements` | `HealthRequirements` | Tests, vaccination proof, quarantine, screenings |
| `embassy` | `EmbassyData` | Two **lists** of `EmbassyInfo`: your missions at the destination, and the destination's missions in your country (a country usually has an embassy plus consulates) |

```python
visa = client.get_visa("FRA", "JPN")

if visa.visa_fee and visa.visa_fee.single_entry:
    fee = visa.visa_fee.single_entry
    print(f"Single entry: {fee.amount} {fee.currency}")

if visa.transit_visa:
    for hub in visa.transit_visa.hubs:
        print(f"{hub.airport} ({hub.city}): {hub.transit_free_hours}h transit-free")

if visa.safety:
    print(f"Advisory level: {visa.safety.level}")

if visa.embassy:
    for e in visa.embassy.visa_application_embassy:   # where to apply
        print(f"{e.name} ({e.city}) — {e.phone}")

print(visa.entry_by_mode.air.get("visa_free_days") if visa.entry_by_mode else None)
```

All types are exported: `from orizn import VisaData, TransitVisa, EmbassyData, ...`.

## Languages

`fr en es pt de it ja ko zh ru ar hi th vi tl` — the tuple is exported as `orizn.LANGUAGES`.

## Tests

```bash
python tests/test_client.py                 # offline, no key
ORIZN_API_KEY=... python tests/test_client.py   # + a live smoke test (~5 requests)
```

Offline, a fake session replays payloads captured from production. With a key set, the same
run also hits the real API, so a schema drift shows up instead of silently decoding to `None`.

## Feedback

Building a travel agent or visa tool? Tell us what you need —
**api@orizn.app**.

## Links

- [Get a free API key](https://visa.orizn.app/visa-api)
- [API docs](https://visa.orizn.app/visa-api/dashboard/docs)
- [MCP server](https://github.com/MattJeff/orizn-mcp-server)
- [npm SDK](https://www.npmjs.com/package/orizn)
