Metadata-Version: 2.4
Name: africa-compliance-data
Version: 1.2.0
Summary: Client library for the Africa Compliance Data API: public holidays, business days, holiday watch, VAT and VAT rate history, withholding tax and tax-treaty rates, tax ID formats, filing deadlines, finance-law digests and change webhooks for 54 African countries, each value with its source. MIT covers this client code only; API data is governed by the Terms of Service. Not legal or tax advice.
Author-email: "Ingot Supply, LLC" <support@africacompliancedata.com>
License-Expression: MIT
Project-URL: Homepage, https://africacompliancedata.com
Project-URL: Documentation, https://africacompliancedata.com/docs
Keywords: africa,public holidays,business days,vat,withholding tax,tax treaty,tax id,finance law,webhooks,compliance,api
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Office/Business :: Financial :: Accounting
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# africa-compliance-data

Client library for the [Africa Compliance Data API](https://africacompliancedata.com): public holidays, business days, a holiday watch for announced and moon-sighted holidays, VAT (with dated rate history), withholding tax and tax-treaty rates, tax ID formats, filing deadlines and finance-law digests for 54 African countries, with coverage shown per country. Every value cites its source: official texts first; secondary sources, press reports and estimates are labelled, with a confidence level.

No dependencies (standard library only). Python 3.8+.

```sh
pip install africa-compliance-data
```

## Quick start

```python
from africa_compliance_data import AfricaComplianceData

acd = AfricaComplianceData()  # reads ACD_API_KEY, or pass api_key="..."

holidays = acd.holidays("MA", 2027)
due = acd.add_business_days("KE", "2026-12-23", 5)
watch = acd.holiday_watch(days=30, countries=["KE", "NG", "MA"])
vat = acd.vat("NG")                                # Pro or Compliance plan
check = acd.validate_tax_id("ZA", "0123456782")    # Pro or Compliance plan
```

Get a key at [africacompliancedata.com](https://africacompliancedata.com/#pricing): the Free plan costs nothing (holidays and business days, non-commercial use); paid plans add commercial use and tax data.

## Methods

| Method | Endpoint | Plan |
|---|---|---|
| `countries()` / `country(country)` | `/v1/countries`, `/v1/countries/{cc}` | All (tax sections of the profile: Pro for the countries of your pack, Compliance) |
| `holidays(country, year, include_religious_scopes=None)` | `/holidays` | All |
| `holidays_ics(country, from_year=None, to_year=None, include_religious_scopes=None)` | `/holidays.ics` (returns iCalendar text) | All |
| `holiday_watch(days=None, countries=None)` | `/v1/holidays/watch` (announced and moon-sighted holidays) | Developer, Pro, Compliance |
| `is_business_day(country, date)` | `/is-business-day` | All |
| `business_days(country, from_date, to_date)` | `/business-days` | All |
| `add_business_days(country, date, days)` | `/add-business-days` | All |
| `account()` | `/v1/account` (plan, monthly quota, country pack, today's usage) | All |
| `country_pack()`, `set_country_pack(countries)` | `/v1/account/countries` (the countries of tax data in your pack; a change that removes a country: once every 30 days) | Pro |
| `vat(country, date=None)` | `/vat` (with `date`: the standard rate in force that day) | Pro (countries of your pack), Compliance |
| `vat_history(country)` | `/vat/history` | Pro (countries of your pack), Compliance |
| `withholding_tax(country, payment_type=None, recipient_country=None)` | `/withholding-tax` (with `recipient_country`: the treaty rates too) | Pro (countries of your pack), Compliance |
| `treaties(country, partner=None)` | `/treaties` | Pro (countries of your pack), Compliance |
| `validate_tax_id(country, value, server=False)` | Checked on your machine with the signed format rules (`GET /v1/tax-id/rules`, verified with the built-in Ed25519 key in pure Python, refreshed daily, valid 35 days): the number is not sent. Same answer as the API plus `checked_locally: True`. Falls back to `POST /tax-id/validate` (number in the JSON body, never the URL) if the rules cannot be had; `server=True` forces it. Options: `tax_id_mode="local"` never sends a number, `"server"` always uses the API; `tax_id_rules()` returns the bundle to save and pass back as `tax_id_rules=` | Pro (countries of your pack), Compliance |
| `deadlines(country, year, month)` | `/deadlines` | Pro (countries of your pack), Compliance |
| `finance_law(country)` | `/finance-law` | Compliance |
| `list_webhooks()`, `create_webhook(url, countries=None, features=None)`, `delete_webhook(webhook_id)`, `test_webhook(webhook_id)` | `/v1/webhooks` | Developer and Pro (holiday changes), Compliance (all changes) |

Optional arguments are keyword-only. A method outside your plan raises `ApiError` with status `403` and an upgrade message. `include_religious_scopes=True` adds holidays limited to one group (a faith, women, the public sector); by default only national holidays are returned.

Dates may be `YYYY-MM-DD` strings or `datetime.date` objects. Errors raise `ApiError` with `.status` and `.body`. Rate-limit (429) and temporary errors are retried twice, honouring `Retry-After`. Webhook create and test (POST) are retried only on 429 and 503, never after a timeout or network error (the request may have gone through). In `holiday_watch`, an empty `countries` list raises `ValueError`; omit it for all countries.

## Private mode (version 1.2.0)

```python
acd = AfricaComplianceData(private_mode=True, bundle_cache_dir="~/.cache/acd-bundles")
due = acd.add_business_days("KE", "2026-12-23", 5)   # computed on your machine
# due["computed_locally"] is True
```

With `private_mode=True`, `holidays`, `is_business_day`, `business_days`, `add_business_days`, `vat` (with or without `date`), `vat_history` and `deadlines` are computed on your machine. The client downloads a signed bundle per country and year (`GET /v1/countries/{cc}/bundle?year=`), and 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 (they also carry your API key, like every request). The answers have the same fields and values as the API's, plus `computed_locally: True`.

- Each bundle is checked before use: its Ed25519 signature (the same built-in key as the tax ID rules, verified in pure Python), the country and year, and its expiry. A bundle expires 1 day after it is issued for the current year and later years, because holiday dates can still be confirmed or moved; for past years it expires after 30 days. An expired bundle is never used: a new one is downloaded.
- Bundles are kept in memory. `bundle_cache_dir` (optional) also keeps them in that folder between runs (expanded with `os.path.expanduser`). Bundles read from disk are verified again.
- If a bundle cannot be downloaded or does not verify, the method raises `PrivateModeError`. The client never sends the query to the API instead. The API's own refusals pass through unchanged: an unknown country (404), a plan without tax data (403, as the API's tax endpoints say), or the monthly quota (429).
- Plans: holiday parts on every plan; tax parts (VAT, VAT history, deadline rules) on Compliance, and on Pro for the countries of your pack. A tax method on Pro adds the country to a pack that has room, as a tax endpoint would. Each bundle download counts as one request against the monthly quota.
- `private_mode` also sets `tax_id_mode="local"` unless you set `tax_id_mode` yourself, so tax ID numbers are not sent either.
- Other methods (countries, profiles, holiday watch, iCalendar feeds, withholding tax, treaties, finance law, account, webhooks) call the API as usual.
- `country_bundle(country, year, parts="all")` returns a verified bundle, and `verify_bundle(signed, public_key=...)` checks one yourself.

## Webhooks

```python
from africa_compliance_data import verify_webhook

event = verify_webhook(request.body, request.headers["ACD-Signature"], WEBHOOK_SECRET)
# event["type"] == "dataset.changed"; event["changes"] lists the countries and features that changed.
# A retry carries the same event["id"] (header ACD-Event-Id): use it to ignore duplicates.
```

Full reference: [africacompliancedata.com/docs](https://africacompliancedata.com/docs) · OpenAPI: [openapi.json](https://africacompliancedata.com/openapi.json)

## License

MIT · Ingot Supply, LLC. MIT covers this client code only; API data is governed by the [Terms of Service](https://africacompliancedata.com/terms). Not legal or tax advice.
