Africa Compliance Data

Días festivos públicos, días hábiles, IVA, retención en la fuente, convenios tributarios, formatos de identificación fiscal y plazos de presentación para 54 países africanos. Cada valor cita su fuente.

Servidor MCP alojado

npx add-mcp 'https://api.africacompliancedata.com/mcp'

Se instala en Claude Code, Codex, Cursor y más

Documentación

Documentation

The API serves a compiled, sourced dataset of public holidays, business days, VAT, withholding tax, tax treaties, tax ID formats and filing deadlines for 54 African countries, with coverage shown per country.

Base URL: https://api.africacompliancedata.com. Data endpoints are GET and return JSON; the tax ID check and the country pack also take POST, the webhook endpoints POST and DELETE, and the MCP endpoint (/mcp) takes POST. Country codes are ISO 3166-1 alpha-2 (e.g. KE, NG, ZA, CI, EG).

Authentication

Get a key from the pricing section (the free plan takes a minute). Send it on every request:

curl -H "x-api-key: ACD-XXXX-XXXX-XXXX" \
  https://api.africacompliancedata.com/v1/countries/NG/vat

Authorization: Bearer ACD-… also works. Errors are JSON {"error": "…"}:

StatusWhen
400Missing or invalid parameter (the message names it)
401Missing or invalid API key
403The endpoint is not in your plan (tax data on Free and Developer; the holiday watch and webhooks on Free; finance-law digests and webhooks for features other than holidays on every plan except Compliance), tax data for a country outside your Pro country pack, or the key is paused for review (“Access paused”)
404Unknown country, path or webhook id; a calendar feed for a country with no verified holiday list
405Method not allowed (only GET / HEAD, except the webhook endpoints and POST /mcp)
409, 413Webhook creation: 3 webhooks already exist, or the request body is over 4,000 bytes. Country pack: a change that removes a country within 30 days of the last one (409, with Retry-After)
429Over the plan's monthly quota (until the 1st of next month, UTC), over the plan's rate limit, over the shared limit of 5 per minute for full profiles and the holiday watch, at most 5 webhook tests per minute, or key checks busy (many new keys from your network); honour Retry-After
503Key check temporarily unavailable, or webhook storage unavailable (webhook endpoints); retry after Retry-After

Plans

PlanPriceDataRequests
Free$0Country list, holidays, business days, calendar feeds, MCP server. Non-commercial use; credit Africa Compliance Data when you show the data3,000 / month; 30 / min
Developer$19 / month or $190 / yearSame as Free for commercial use, + the holiday watch (estimated and confirmed moon-sighted dates across countries) and webhooks for holiday changes50,000 / month; 60 / min
Pro$99 / month or $990 / yearEverything in Developer, + tax data for the 5 countries of your country pack (+$15 / month per extra country): VAT (with the rate on a date via ?date, and /vat/history), withholding tax (with treaty rates via ?recipient_country), /treaties, tax ID checks and rules, filing deadlines250,000 / month; 300 / min
Compliance$249 / month or $2,490 / yearEverything in Pro for all 54 countries, + /finance-law digests, webhooks for every change, email alerts when rates, holidays or deadlines change, and priority support (no SLA)1,000,000 / month; 1,000 / min
Data licencesInternal from $6,000 / year; Platform from $15,000 / year; Private / offline from $18,000 / yearThe dataset under a written contract: internal use (Internal), display inside your product (Platform, with a 99.5% monthly availability commitment and service credits), or on your own systems (Private / offline)By agreement

Yearly billing costs 10 times the monthly price (2 months free). Subscriptions started before 20 October 2026 (Starter, Pro and Compliance at the earlier prices) keep their plan and price; their keys get the features of the closest new plan (Starter: those of Developer), with no monthly quota, and the earlier Pro keeps tax data for every country. GET /v1/account shows which plan a key is on (grandfathered: true for those).

On Free and Developer, the VAT, VAT-history, withholding-tax, treaties, tax-ID and deadline endpoints return 403 with an upgrade message (the matching MCP tools return a tool error, isError: true, with the same message); the country profile omits those blocks and lists them in not_in_plan. /finance-law returns 403 on every plan except Compliance; /v1/holidays/watch and /v1/webhooks return 403 on Free. GET /v1/account shows your key's plan, monthly quota, country pack, rate limit and today's usage.

Monthly quota

Each plan includes a number of requests per calendar month (UTC): Free 3,000, Developer 50,000, Pro 250,000, Compliance 1,000,000. Every successful answer (2xx) counts, including calendar feeds, tax ID rules downloads and country bundles (private mode: one request per bundle), and so does every successful MCP tool call. Errors, refused requests, MCP messages other than tool calls, /v1/account, /v1/account/countries and webhook management do not count. Counted answers carry X-Quota-Limit and X-Quota-Remaining. Over the quota, requests get 429 with a message, the same headers and Retry-After (seconds until 00:00 UTC on the 1st of next month); the count starts again then. There are no overage charges: move to a larger plan for more. Grandfathered plans have no monthly quota.

Pro country pack

Pro includes tax data (VAT, VAT history, withholding tax, treaties, tax ID checks and rules, deadlines) for 5 countries, plus one per extra country bought as an add-on ($15 / month each; buy it with the same email as your Pro subscription, and choose one seat per extra country). Holidays, business days and the holiday watch cover all 54 countries on every paid plan.

  • The first countries you request tax data for join the pack while it has room. Once it is full, tax data for another country returns 403 (MCP: a tool error) saying how to change the pack. Refused requests are not counted.
  • GET /v1/account/countries shows the pack: countries, size (base 5 + extra), last_swap and next_swap_from. If an add-on ends, the countries beyond the new size are listed in inactive and refused until there is room again.
  • POST /v1/account/countries with {"countries": ["KE", "NG", "GH", "ZA", "EG"]} sets the pack. Adding countries to free places is always possible; a change that removes a country (a swap) is possible once every 30 days (409 with next_swap_from and Retry-After otherwise). More countries than the pack holds: 400.
  • A full country profile includes the tax parts only for pack countries (others get the holiday part, with not_in_plan); profiles never add a country to the pack.
  • GET /v1/tax-id/rules returns the rules of the pack's countries only, with "scope": "pack". The SDKs (version 1.1.1 and later) send a check for another country to the API instead.
  • A country bundle with parts=all (the default) uses the pack as a tax endpoint does: the country joins a pack that has room; with a full pack that does not hold it, the bundle has the holiday parts and not_included.tax says why. parts=holidays never changes the pack.
curl -X POST https://api.africacompliancedata.com/v1/account/countries \
  -H "x-api-key: ACD-XXXX-XXXX-XXXX" -H "Content-Type: application/json" \
  -d '{"countries": ["KE", "NG", "GH", "ZA", "EG"]}'

{"countries": ["KE", "NG", "GH", "ZA", "EG"], "inactive": [], "size": 5, "last_swap": null,
 "next_swap_from": null, "base": 5, "extra": 0, "swapped": false}

Fair use

Full country profiles and the holiday watch (both cover a lot of data in one answer) share a limit of 5 requests per minute per key (429 with Retry-After: 60); full profiles are also limited to a number of different countries per day (Free 6, Developer 8, Pro 10, Compliance 15); use the specific endpoints for regular reads. Daily reading per key: Free up to 12 countries, Developer 25, Pro 45 (tax data for 25), Compliance all 54 (tax data for 35); tax data is also capped over 7 days. Grandfathered plans have the limits of their earlier plan (Starter as Developer, Pro as Pro, Compliance as Compliance). Honour Retry-After: retrying a full profile or the holiday watch again and again before the 60 seconds have passed counts as bulk access (waiting as told never does). Over MCP, the same limit is reported as a tool error that says to retry after 60 seconds; set your agent not to retry it immediately. A key that goes beyond these limits is paused for review (403, “Access paused”); email support to restore it. Need the whole dataset? Ask about a data licence. See the Terms, sections 9 and 10.

Response format

Every value cites its source: official texts first; secondary sources, press reports and estimates are labelled, with a confidence level. Answers about one country and one feature (holidays, business days, VAT, VAT history, withholding tax, treaties, finance law, tax ID, deadlines) include the fields below. The country list gives per-feature coverage, and the full profile gives coverage and sources for all features.

FieldMeaning
coverageready (sourced), partial (main value sourced, a detail missing) or gap (not yet sourced)
warningWhen coverage is partial or gap, or when the requested year's holiday list is incomplete (also on deadlines adjusted for holidays): what may be missing (for example, holidays not yet confirmed by an official source, which business-day results then count as working days)
sourcesFor each source: url, legal_basis, source_type, effective_date, confidence (high / medium / low), last_verified. When the official text is not available online, url is null and a note explains it; source_type still says what the source is (e.g. statute), or secondary when no official text has been confirmed
dataset_versionDate the dataset was built
disclaimerReference information compiled mainly from official sources (see sources and confidence); not legal or tax advice

Endpoints

EndpointReturns
/v1/countriesAll countries with currency and per-feature coverage
/v1/countries/{cc}Full compliance profile of a country
/v1/countries/{cc}/holidays?year=2026Public holidays. Add &include_religious_scopes=true (or 1) for holidays limited to one group: a faith (e.g. Kenya's Idd-ul-Azha for Muslims), women, or the public sector. Business-day endpoints use national holidays only
/v1/countries/{cc}/is-business-day?date=2026-12-24business_day true/false and the reason
/v1/countries/{cc}/business-days?from=2026-12-01&to=2026-12-31Working days in the range (inclusive) and the holidays skipped
/v1/countries/{cc}/add-business-days?date=2026-12-24&days=5The date N working days later (negative N: earlier)
/v1/countries/{cc}/vatStandard and other rates, registration threshold, notes. standard_rate is the rate as the tax authority presents it: Cameroon's 19.25% already includes the communal surtax; where a surtax is charged on top (e.g. Congo, Chad), notes give the combined rate
/v1/countries/{cc}/vat?date=2019-06-01Also the standard rate in force on that date (standard_rate_on_date: rate, from, to, the law that set it, official source, confidence, and from_note when the period started before the documented history), for audits, refunds and re-invoicing. A date before the start of the documented history (covered_from, usually 2015-01-01) gets no rate: standard_rate_on_date is null and warning says from which date the history starts. Dates more than a year ahead return 400
/v1/countries/{cc}/vat/historyEvery standard-rate period (from, to, rate, legal basis, source, confidence). Coverage starts in 2015 or at the earliest period backed by an official source (from_note says which); current is today's standard rate and last_verified the date the history was last checked
/v1/countries/{cc}/withholding-tax?payment_type=dividendsResident and non-resident rates; payment_type optional (dividends, interest, royalties, rent, services, professional_fees, management_fees, technical_fees, contracts, goods, commissions, directors_fees, other)
/v1/countries/{cc}/treaties?partner=FRTax treaties of the paying country: for each partner, the maximum withholding rates on dividends, interest, royalties and technical fees (rate: a number, "5/15" with conditions, or "none" when only the recipient's country may tax; null: no treaty limit recorded, the domestic rate applies; where a rate was not captured, the entry's notes say so), dates in force and the official text. partner optional (an ISO code other than the paying country; anything else is 400, as for recipient_country). Add ?recipient_country=FR to /withholding-tax to get the domestic rates and the treaty limit together. Coverage per country is in /v1/countries (coverage.treaties). Treaty lists are partial: a partner missing from the list does not mean there is no treaty (the answer says so); more treaties are being added
/v1/countries/{cc}/finance-lawCompliance plan. Plain-English digest of the latest finance law: each business-relevant change (VAT, withholding, corporate and personal income tax, payroll, excise, digital, procedure, customs) with its effective date and article, and the official text. Which countries have a digest: coverage.finance_law in /v1/countries (ready); for the others finance_law is null with a warning. Each digest is an unofficial summary: the cited law prevails (notes says so). Updated as new finance laws are enacted
POST /v1/countries/{cc}/tax-id/validate with JSON body {"value": "…"}; GET …/tax-id/validate?value=… is deprecated and ends on 31 December 2026Checks a tax ID's format (not its registration) against the known format. format_check is full when the authority publishes the structure, partial when only the characters or maximum length are known (a weak check). Where the authority publishes no format (e.g. Angola, Sudan), valid_format is null with a message and the authority's notes. Spaces, dots and dashes are tolerated; max 64 characters. Use POST (header Content-Type: application/json): the number then stays in the request body, out of URLs, request logs, browser history and proxies. The GET form with ?value= puts the number in the URL: it still works until 31 December 2026, and its answers carry the headers Deprecation, Sunset: Thu, 31 Dec 2026 23:59:59 GMT and Link: …; rel="deprecation". The MCP tool validate_tax_id_format also sends the number in the body. Answers are never cached (Cache-Control: no-store). Does not confirm registration; the value is not stored or logged. To keep numbers on your own machine, use the signed rules below (the SDKs do it for you)
/v1/tax-id/rulesCompliance: every country; Pro: the countries of your pack only, with "scope": "pack". The tax ID format rules of each country (identifier name, pattern, whether the pattern is partial, check-digit method, normalisation, coverage, dataset version), signed with Ed25519, so you can check numbers locally and they never reach our servers. No notes, sources or examples. The signature (signature.value, base64) covers the canonical JSON of rules: keys sorted, no spaces, strings as JSON.stringify writes them. Verify it with our public key (embedded in the SDKs) and stop using the rules after expires_at (35 days after issued_at). One download per key per hour (429 with Retry-After otherwise): keep the copy and refresh it about once a day. Not counted as country reads. The SDKs' validateTaxId / validate_tax_id download, verify and refresh the rules and check locally (checked_locally: true in the answer; sources are empty and notes absent there); pass { server: true } / server=True to use the API instead
/v1/countries/{cc}/bundle?year=2026 (optional &parts=holidays)All plans. One country-year, signed with Ed25519, for computing on your own machine (see Private mode): the year's holidays with their status, the weekend, coverage, the sources with their confidence and the disclaimer; with the tax parts (Compliance; Pro for the countries of your pack) also the VAT rates, the VAT history and the deadline rules. Free and Developer get the holiday parts. Valid 1 day for the current and later years, 30 days for past years. One request against the quota, and a read of that country
/v1/holidays/watch?days=14&countries=KE,NGDeveloper, Pro and Compliance. Holiday watch, across countries: holidays whose date depends on an announcement (Eid, Mawlid and other moon-sighted holidays, ad-hoc days off) from a week ago to days ahead (1–60, default 14; countries optional). Each has status estimated (not announced yet) or confirmed (with source_url, and a source_note when the official notice is not online or the date is known from a press report); pending counts the upcoming ones still estimated; announcements says who announces them and where. See Holiday watch
/v1/accountYour key: plan (grandfathered for plans bought before 20 October 2026), what it includes, rate limit, status (active; a paused key gets 403 “Access paused” here too), monthly_quota (limit, used, remaining, resets_at), the country_pack (Pro), countries read today and the daily allowances. Not counted against the quota, and not a country read
/v1/account/countriesPro. GET: your country pack. POST with {"countries": [...]}: set it (a change that removes a country at most once every 30 days). See Pro country pack. Not counted against the quota
/v1/countries/{cc}/deadlines?year=2026&month=11Tax deadlines whose due date falls in that month (due), adjusted for weekends and holidays where the country's rule (weekend_adjust) or the deadline's own rule is known; otherwise the statutory date is returned with adjusted: false; statutory_date shows the date before adjustment, which can be in the neighbouring month. Deadlines set as "N months after year-end" are placed for a financial year ending 31 December (period says so); quarterly ones for calendar quarters. Rules without a fixed calendar date (event-based, or with no stated day) are listed in other_rules

Example: tax ID check (POST, recommended)

curl -X POST https://api.africacompliancedata.com/v1/countries/KE/tax-id/validate \
  -H "x-api-key: ACD-XXXX-XXXX-XXXX" -H "Content-Type: application/json" \
  -d '{"value": "P051234567Q"}'

The number travels in the body, so it does not appear in URLs or request logs. A body that is not JSON gets 415 (wrong content type) or 400; over 2 KB, 413. With the SDKs, acd.validateTaxId("KE", "P051234567Q") checks on your machine with the signed rules (/v1/tax-id/rules), so the number is not sent at all.

Private mode (SDKs 1.2.0)

With privateMode: true (JavaScript) or private_mode=True (Python), the SDKs compute holidays, business days, VAT (with or without a date), VAT history and deadlines on your machine. They download one signed bundle per country and year (GET /v1/countries/{cc}/bundle?year=), so the dates, ranges, months and day counts of your queries are not sent: the only details of your queries in those requests are the country and the year. The answers have the same fields and values as the API's, plus computed_locally: true.

  • Signature: Ed25519 with the same key as the tax ID rules (embedded in the SDKs), over the canonical JSON of bundle: keys sorted, no spaces, strings as JSON.stringify writes them, integers and plain decimals such as 7.5. The SDKs use a bundle only if the signature verifies, it is the country and year asked for, and it has not expired.
  • Expiry: expires_at is 1 day after issued_at for the current and later years (an estimated holiday can be confirmed or moved), 30 days for past years. An expired bundle is never used; the SDK downloads a new one.
  • Your key's invisible mark is in the top-level disclaimer, outside the signed payload, so the signed data is the same for every key. Answers computed from the bundle carry it, as the API's answers do.
  • Bundles are kept in memory; the Python SDK can also keep them in a folder (bundle_cache_dir), and the JavaScript SDK takes bundleStore load and save hooks. Saved bundles are verified again when read.
  • If a bundle cannot be downloaded or verified, the SDK raises PrivateModeError and does not send the query instead. Private mode also checks tax IDs locally only (taxIdMode: "local"), unless you choose otherwise.
const acd = new AfricaComplianceData({ apiKey: process.env.ACD_API_KEY, privateMode: true });
await acd.addBusinessDays("KE", "2026-12-23", 5);   // only GET /v1/countries/KE/bundle?year=2026&parts=holidays is sent

Examples: treaties and finance law

GET /v1/countries/KE/treaties?partner=GB          (Pro and Compliance; shortened)
{
  "country": "KE", "country_name": "Kenya", "coverage": "partial", "warning": "…",
  "treaties": [{
    "partner": "GB", "title": "Kenya - United Kingdom DTA", "signed": "1973-07-31", "in_force": "1977-07-30", "effective_from": null,
    "dividends": {"rate": 15, "conditions": "…"}, "interest": {"rate": 15, "conditions": "…"},
    "royalties": {"rate": 15, "conditions": "…"}, "technical_fees": null,
    "articles": null, "source_url": "https://www.treasury.go.ke/…", "confidence": "medium", "notes": "…"
  }],
  "notes": ["Rates are the maximum the source country may withhold under the treaty; …"],
  "last_verified": "2026-10-16",
  "sources": {"wht": {…}}, "dataset_version": "…", "disclaimer": "…"
}

GET /v1/countries/KE/finance-law                  (Compliance; shortened)
{
  "country": "KE", "country_name": "Kenya", "coverage": "ready",
  "finance_law": {
    "law": "Finance Act, 2026", "number": "Act No. 19 of 2026 (Kenya Gazette Supplement No. 157)",
    "enacted": "2026-06-23", "fiscal_year": "2026/27", "effective_from": "2026-07-01",
    "source_url": "https://www.kra.go.ke/…/Finance-Act-2026.pdf", "confidence": "high", "last_verified": "2026-10-16",
    "changes": [{"area": "wht", "summary": "Card interchange fees and merchant service fees now count as management or professional fees, …",
                 "effective_from": "2026-07-01", "reference": "s.2 (…)"}]
  },
  "notes": ["Unofficial summary; the cited law prevails."],
  "sources": {}, "dataset_version": "…", "disclaimer": "…"
}

area is one of vat, wht, cit, pit, payroll, excise, digital, procedure, customs, other. Treaty and VAT-history answers carry last_verified, the date that country's treaty list or rate history was last checked.

Holiday status values

StatusMeaning
ruleFixed by law (fixed date, Easter-based, nth weekday). Where the law moves a weekend holiday and the weekend day ceases to be a holiday (e.g. Kenya, Malawi, DR Congo, Sierra Leone, São Tomé and Príncipe), the holiday is listed on its new date with moved_from showing the original date
estimatedMoon-sighted holiday computed in advance; may move by a day or two. A reviewer publishes the confirmed date once the announcement is verified; delays are possible. A weekend substitute of an estimated date has status observed and estimated: true: it moves with the announced date
confirmedDate taken from an official announcement (source_url, with a source_note when the announcement is not published online or the date is known from a press report); moved_from shows the original date when a holiday was moved, observed_for the holiday a substitute day replaces
observedSubstitute day under the country's weekend rule (observed_for shows the holiday)
bridgeStatutory bridge day next to a holiday, e.g. Angola: Monday before a Tuesday holiday, Friday after a Thursday holiday (bridge_for shows the holiday)

Holiday watch: announced and moon-sighted holidays

Eid, Mawlid and other Islamic holidays depend on the moon sighting, and many governments announce extra or moved days off only days, or hours, before. Around each expected date we check the official announcement channels every hour (government gazettes, presidency and ministry releases, religious authorities). A reviewer publishes the confirmed date, with its source, once the announcement is verified; delays are possible. Past dates for which no official announcement was found stay estimated. How to follow it:

  • GET /v1/holidays/watch (Developer, Pro and Compliance): what is still estimated and what has been confirmed, across countries. Every plan's holiday lists show each date's status.
  • Webhooks (Developer, Pro and Compliance): a change event for holidays lists the newly confirmed dates in holidays_confirmed: [{"date": "2027-03-10", "name": "Eid al-Fitr", "rule_id": "eid_fitr", "source_url": "https://…"}] (a source_note is added when the notice is not online, which leaves source_url empty, or when the date is known from a press report).
  • Calendar feeds update the tentative event to the confirmed date; change alerts (Compliance plan) email it.

The watch answers for many countries at once, so it shares the per-key limit of 5 requests per minute with full country profiles (429 with Retry-After: 60); poll it every few minutes at most, or use webhooks. Illustrative example (shortened):

curl -H "x-api-key: ACD-XXXX-XXXX-XXXX" \
  "https://api.africacompliancedata.com/v1/holidays/watch?days=30&countries=KE,NG,MA"

{
  "from": "2027-03-01", "to": "2027-04-07", "generated": "2027-03-08", "dataset_version": "2027-03-08",
  "pending": 2,
  "holidays": [
    {"country": "KE", "country_name": "Kenya", "date": "2027-03-09", "name": "Idd-ul-Fitr", "rule_id": "eid_fitr",
     "kind": "moon_sighted", "status": "estimated", "scope": "national", "source_url": null, "past": false},
    {"country": "NG", "country_name": "Nigeria", "date": "2027-03-09", "name": "Id el Fitr", "rule_id": "eid_fitr",
     "kind": "moon_sighted", "status": "estimated", "scope": "national", "source_url": null, "past": false},
    {"country": "MA", "country_name": "Morocco", "date": "2027-03-10", "name": "Eid al-Fitr", "rule_id": "eid_fitr",
     "kind": "moon_sighted", "status": "confirmed", "scope": "national", "source_url": "https://…", "past": false},
    {"country": "MA", "country_name": "Morocco", "date": "2027-03-11", "name": "Eid al-Fitr", "rule_id": "eid_fitr",
     "kind": "moon_sighted", "status": "confirmed", "scope": "national", "source_url": "https://…", "past": false}
  ],
  "announcements": {"NG": {"authority": "Minister of Interior …", "channels": ["…"], "typical_notice_days": "1-4"}, "KE": {…}, "MA": {…}},
  "notes": ["'estimated' dates depend on the moon sighting or an announcement; …"],
  "disclaimer": "…"
}

Holiday calendar feeds

Subscribe to a country's holidays in Google Calendar, Outlook or Apple Calendar (iCalendar format). Estimated dates (moon sighting) are marked tentative and move to the confirmed date once a reviewer has verified the announcement (delays are possible). The feed's description says when the country's holiday list is partial or a year is incomplete, and carries the disclaimer.

  • Free, no key, national holidays 2025–2030: https://africacompliancedata.com/calendar/ke.ics (country code in either case). Each country page also has one-click "add to calendar" links.
  • With your key: /v1/countries/{cc}/holidays.ics?from=2026&to=2027&include_religious_scopes=true (all parameters optional).

Webhooks (Developer, Pro: holiday changes; Compliance: every change)

Get a signed HTTPS request when the data of a country changes (a new or confirmed holiday, a rate, a tax ID format, a deadline). The URL must be https on the standard port, with a public host name or public IPv4 address (no credentials, private addresses or IPv6 literals). Up to 3 webhooks per key, each optionally limited to some countries and features (holidays, vat, wht, tin_format, filing_deadlines; empty or omitted = all). On Developer and Pro (and the grandfathered Starter and Pro), webhooks cover holidays only: a webhook created without features gets ["holidays"], and asking for another feature returns 403. Changes to the VAT rate history arrive under vat, and changes to tax treaties under wht. Changes are checked every 15 minutes. Besides the MCP endpoint (POST /mcp), the tax ID check and the country pack, the webhook endpoints are the ones that take POST and DELETE; on Free they return 403. Managing webhooks does not count against the monthly quota.

GET    /v1/webhooks             list your webhooks (with the last delivery result) and "max": 3
POST   /v1/webhooks             create: {"url": "https://example.com/acd", "countries": ["KE","NG"], "features": ["vat","holidays"]}
DELETE /v1/webhooks/{id}        delete: {"deleted": "wh_…"}
POST   /v1/webhooks/{id}/test   send a test event now: {"sent": "evt_test_…", "status": 200}
curl -X POST https://api.africacompliancedata.com/v1/webhooks \
  -H "x-api-key: ACD-XXXX-XXXX-XXXX" -H "content-type: application/json" \
  -d '{"url": "https://example.com/acd", "countries": ["KE"], "features": ["vat", "holidays"]}'

{"id": "wh_3f9c…", "url": "https://example.com/acd", "countries": ["KE"], "features": ["vat", "holidays"],
 "created": "2026-10-17T09:00:00.000Z", "last_delivery": null, "secret": "whsec_…",
 "note": "Store the secret now: it is shown only once. Verify each delivery's ACD-Signature header with it."}

curl -X DELETE https://api.africacompliancedata.com/v1/webhooks/wh_3f9c… -H "x-api-key: ACD-XXXX-XXXX-XXXX"

Creating a webhook returns its secret once. The URL must be a public https address on the standard port, without credentials (400 otherwise). Creating a fourth webhook returns 409 (delete one first); a request body over 4,000 bytes returns 413. An unknown webhook id returns 404.

Deliveries

Each delivery is a JSON POST to your URL with these headers:

  • ACD-Signature: t=<unix seconds>,v1=<hex>: HMAC-SHA256 of t + "." + raw body with your secret.
  • ACD-Event: the event type (dataset.changed, or test).
  • ACD-Event-Id: the event id, the same as id in the body. It stays the same on every retry: use it to ignore duplicates.
  • Content-Type: application/json, User-Agent: AfricaComplianceData-Webhooks/1.0.
{
  "id": "evt_8c1d0e5a2b7f4a90",
  "type": "dataset.changed",
  "created": "2027-03-09T21:15:02.114Z",
  "dataset_version": "2027-03-09",
  "previous_version": "2027-03-08",
  "changes": [
    {"country": "KE", "country_name": "Kenya", "feature": "vat"},
    {"country": "KE", "country_name": "Kenya", "feature": "holidays",
     "holidays_confirmed": [{"date": "2027-03-10", "name": "Idd-ul-Fitr", "rule_id": "eid_fitr", "source_url": "https://…"}]}
  ],
  "docs": "https://africacompliancedata.com/docs#webhooks",
  "disclaimer": "Reference information compiled mainly from official sources …"
}

The event says what changed, not the new values: fetch them from the API. Every event, test events included, carries the same disclaimer as API answers. holidays_confirmed appears only on holidays changes that include newly confirmed dates (see Holiday watch). A test event has "type": "test", an id starting evt_test_, no previous_version and "changes": [].

Delivery and retries: change events are queued, then delivered by the 15-minute job. Answer with any 2xx within 10 seconds. A 2xx, a 3xx (redirects are not followed) or a 4xx answer (other than 429) ends the delivery: these are not retried. Timeouts, network errors, 429 and 5xx answers are retried on the following runs, about every 15 minutes, up to 3 attempts in all, with the same event id. Up to 50 queued events are kept per key until they are delivered (if more pile up, the oldest are dropped); deleting a webhook drops its queued events. The test endpoint delivers immediately (one attempt, at most 5 tests per minute per key, then 429) and returns the result. GET /v1/webhooks shows each webhook's last_delivery (time, HTTP status or error, event id). Reject events whose t is more than 5 minutes old.

Events go to keys on a plan with webhooks: each use of the key confirms its plan. A key not used (by any request, for example GET /v1/webhooks) for 14 days, or 13 at the earliest, moved to a plan without webhooks, cancelled or paused receives no events until it is used again on such a plan; a key moved to a holiday-only plan receives holiday changes only.

// Node.js
const [, t, v1] = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(req.headers["acd-signature"]);
const ok = crypto.timingSafeEqual(Buffer.from(v1, "hex"),
  crypto.createHmac("sha256", SECRET).update(t + "." + rawBody).digest())
  && Math.abs(Date.now() / 1000 - t) < 300;

# Python
t, v1 = (p.split("=", 1)[1] for p in header.split(","))
ok = hmac.compare_digest(v1, hmac.new(SECRET.encode(), f"{t}.{raw_body}".encode(), "sha256").hexdigest()) \
     and abs(time.time() - int(t)) < 300

Client libraries (SDKs)

Client libraries for JavaScript/TypeScript (Node 18+, Deno, Bun, Cloudflare Workers) and Python 3.8+, with no dependencies. They check tax ID formats on your machine with the signed rules, and offer the private mode. MIT licence for the client code; the data stays under the Terms.

npm install africa-compliance-data
pip install africa-compliance-data

Packages: npm · PyPI.

OpenAPI

The full API description, for code generators, Postman and other tools: openapi.json (OpenAPI 3.1). A short summary for AI tools: llms.txt.

Postman

Every endpoint of the OpenAPI description (including the webhook requests, the country bundle and the free public calendar feed) with ready-to-run examples: open the public Postman collection and fork it, or download the collection file and import it. Set the collection variable apiKey to your key.

MCP server

Endpoint https://api.africacompliancedata.com/mcp (Streamable HTTP, protocol 2025-06-18). Available on every plan, with the same data limits as the API. In tool arguments, an empty optional value counts as not given (for example countries: "" in get_holiday_watch means all countries). Send your key in the x-api-key header: use MCP clients that support an API-key header, such as Claude Code and Cursor. Send one JSON-RPC message per request (batches are not supported, as in the 2025-06-18 protocol). The 14 tools (all read-only):

ToolSame data asPlan
list_countries/v1/countriesAll
get_public_holidays/holidays (with include_religious_scopes)All
get_holiday_watch/v1/holidays/watchDeveloper, Pro, Compliance
is_business_day/is-business-dayAll
count_business_days/business-daysAll
add_business_days/add-business-daysAll
get_country_profile/v1/countries/{cc} (tax parts on Compliance, and on Pro for pack countries)All
get_vat/vat (with optional date)Pro (pack countries), Compliance
get_vat_history/vat/historyPro (pack countries), Compliance
get_withholding_tax/withholding-tax (with optional recipient_country)Pro (pack countries), Compliance
get_tax_treaties/treatiesPro (pack countries), Compliance
validate_tax_id_format/tax-id/validatePro (pack countries), Compliance
get_filing_deadlines/deadlinesPro (pack countries), Compliance
get_finance_law_changes/finance-lawCompliance

The tool list with input schemas is public, without a key, at https://api.africacompliancedata.com/.well-known/mcp/server-card.json.

Claude Code

claude mcp add --transport http africa-compliance \
  https://api.africacompliancedata.com/mcp \
  --header "x-api-key: ACD-XXXX-XXXX-XXXX"

Cursor (mcp.json)

{
  "mcpServers": {
    "africa-compliance": {
      "url": "https://api.africacompliancedata.com/mcp",
      "headers": { "x-api-key": "ACD-XXXX-XXXX-XXXX" }
    }
  }
}

Limits and data notes

  • Years supported: 2025–2030.
  • Rate limits per key are approximate (see Plans); monthly quotas are exact. Data licences: bulk delivery and custom limits by contract. API plans have no uptime commitment (SLA); the Platform licence contract includes one.
  • Weekends are taken from each country's law or practice; where the sources do not state the weekend, the profile's notes say so.
  • Data is reference information compiled mainly from official sources (see each value's sources and confidence), not legal or tax advice. Report a correction: support@africacompliancedata.com.