How eligibility works

Read this before you build on the answer. Most integration mistakes here are not code mistakes — they are treating a verdict as more certain than it is, or less specific than it is.

The unit is a person and a journey

Not a nationality. A passport alone cannot tell you whether someone needs a visa, because the exemptions that matter most often depend on what the traveller is doing:

That is why the request takes transit_airside, residence_permit_iso3 and passport_type. Omit them and those exemptions are not applied — the engine will not assume an exemption it was not told about.

Every verdict carries its source

response excerpt
{
  "verdict": "exempt",
  "matched_rule_id": "ken.exempt90.eth",
  "citation": "Kenya Citizenship and Immigration (Amendment) Rules, 2025 (s.48, Act No. 12 of 2011)",
  "sources": [{ "url": "https://etakenya.go.ke/general-information", "accessed": "2026-10-01" }]
}

matched_rule_id names the exact rule that decided the answer. When a partner disputes a verdict, that is the difference between an argument and a lookup.

Unknown is a real answer

Where we hold no verified rule for a traveller, the verdict is unknown and visa_required is null — not false.

a destination with no verified rules loaded yet
{
  "verdict": "unknown",
  "visa_required": null,
  "notes": ["No verified requirement data for this destination yet."]
}

Today unknown is the answer for Ethiopia, Somalia and Djibouti, whose requirement schedules are not yet transcribed. Kenya is complete. That is stated plainly rather than hidden behind a status page.

Rules are true on a date, not forever

Send as_of with the travel date. A schedule amended between today and the flight changes the answer, so answering with today's rules would be answering a different question.

ask about a specific travel date
curl -X POST https://api.haavisa.com/v1/check   -H "Authorization: Bearer $HAAVISA_API_KEY"   -H "Content-Type: application/json"   -d '{
    "nationality_iso3": "ETH",
    "destination_iso3": "SOM",
    "as_of": "2025-09-02"
  }'

That returns unknown. Somalia's exemption schedule comes from SCAA AIC 10/2025, effective 3 September 2025, which cancelled the instruments before it — and we have never captured those. Before that date we hold nothing verified, and the API says so rather than applying today's list backwards.

the window the answer used
{
  "verdict": "unknown",
  "visa_required": null,
  "notes": ["No verified rules are in force for Somalia on 2025-09-02. ..."],
  "answered_as_of": "2025-09-02",
  "effective_from": null,
  "effective_to": null
}

answered_as_of, effective_from and effective_to come back on every answer, so you can always see which date you asked about and which rule window decided it. Omit as_of and you get today.

Products are filtered for you

Where a traveller needs something, the response lists what they can buy. Two rules govern that list, and both exist because the alternative sells people things they cannot use:

Some exemptions are not machine-evaluated

A few published exemptions cannot be evaluated from the inputs the API accepts — ship and aircraft crew, serving British military, holders of laissez-passers issued by named organisations. Rather than silently drop them, the response names them in a note so an agent knows when to override.

how that reads
{
  "verdict": "eta_required",
  "notes": [
    "Kenya requires an eTA from travellers who are not on the statutory exemption schedule.",
    "Not evaluated: Crew of any ship, aircraft, train, vehicle or carrier proceeding to a
     destination outside Kenya; Serving members of the British Military; Holders of
     laissez-passers issued by UN, African Union, ... If the traveller falls into any of
     these, they may be exempt."
  ]
}

That note is written for an integration, not a traveller. The widget summarises it for a consumer audience.

Fees are never blended

government_fee_cents and service_fee_cents are always returned separately. A single blended price is how a refund becomes impossible to explain — and how a partner ends up unable to say which part of a charge was theirs.

both fees, separately
{
  "code": "KEN_ETA_TOURIST",
  "government_fee_cents": 3000,
  "service_fee_cents": 2900,
  "currency": "USD"
}