CIPAget API
Search Botswana's public CIPA register and read a company's or business name's details as JSON. Each call opens CIPA's own public search in a headless browser and reads what the page shows.
http://127.0.0.1:3000
What you can ask for
CIPAget answers five requests. The first three read CIPA's public website while you wait. The other two only report on this server and never contact CIPA.
/v1/search
Search the register
Finds companies and business names by name or number.
Usually 0.4 to 2 s GET/v1/entities/:id
Read an entity
Reads what CIPA shows publicly about one company or business name: its details, addresses, people, owners and filings.
Usually 2 to 7 s GET/v1/entities/:id/documents
Download documents
Downloads that entity's certificate and standard extract as PDFs.
Usually 10 to 20 s GET/healthz
Check the server
Says whether this server is running and how busy it is. Never contacts CIPA.
InstantThe fifth, GET /openapi.json, describes the API in a format that tools such as Postman can import.
Quickstart
To run your own copy you need Node.js 20.19 or newer. The first three commands are one-time setup.
npm ci
npm run browser:install # skip if .env sets BROWSER_CHANNEL=chrome
cp .env.example .env
npm run dev # or: npm run build && npm start
The server prepares its browser before it starts listening, which takes roughly 10 to 20 seconds. After that:
| Address | What it is |
|---|---|
http://127.0.0.1:3000/v1/… | The API described on this page |
http://127.0.0.1:3000/docs/ | This page. Opened from the server, its Try it panels send real requests. |
http://127.0.0.1:3000/openapi.json | OpenAPI description for generating clients |
http://127.0.0.1:3000/healthz | Queue, cache and browser counters. It never calls CIPA. |
If another app already uses port 3000, set PORT in .env. By default the server listens only on localhost. To make it reachable from elsewhere, set HOST and an API_KEY; clients then send Authorization: Bearer <key>.
Sending requests from this page
API_KEY. The key stays in this tab and is never saved.Limits on every request
These apply whichever request you send. Each endpoint below lists its own limits as well.
CIPAget is not an official service and holds no data of its own. If CIPA's site is down, slow or wrong, so is the answer.
It cannot list the register, export it in bulk, or follow links from one company to another.
Ask the same thing again within that time and you get the stored answer, marked meta.cache: "hit". A single request cannot skip this. Set CACHE_TTL_MS=0 to turn it off for the whole server. Incomplete answers and errors are never stored.
Unless you change the settings, one detail read, one search and one document download run at a time. Up to 20 more wait in each line for at most 30 seconds, then fail with a 503.
Past that, the server answers 429 until the minute is over.
When CIPA limits or blocks the browser session, all lookups stop for 60 seconds.
The operator can require an API key, sent as Authorization: Bearer <key>. A server that listens on anything other than localhost must have one. A server without a key only answers requests addressed to localhost. Web pages on other sites cannot call it from a browser.
Search the register
You give it a name, part of a name, or a number. CIPAget types that into the “Name or number” box on CIPA's site, presses Search, and returns the results as JSON: each one's name, number, status, type, registration date and address, plus any earlier names and addresses CIPA lists.
Use it to find a company's number, called its UIN. The other two requests need that number. A live search takes about one second.
Parameters
qstringrequired- 2 to 200 characters: a name, a company UIN such as
BW00001142508, or a business-name number such asBN2019/11223. pageintegerdefault 1- Which page of results to return, 1 to 20.
pageSizeintegerdefault 20- Results per page: 20, 50 or 100.
Limits
- You can search by name or number only. There are no filters for type, status or date.
- Results come 20, 50 or 100 to a page, and no further than page 20. A very common word can fail with
SEARCH_PAGE_LIMIT; use a narrower one. totalis often null, because CIPA does not always say how many results there are. UsehasMoreto know whether another page exists.- Business names come back without a number (
uinis null), so their details cannot be opened from a search result. - Finding nothing is a normal answer with an empty list, not an error.
One result
Each item in data.items looks like this:
{
"uin": "BW00000877524",
"name": "Letshego Africa Holdings Limited",
"status": "Registered",
"register": "Companies",
"entityType": "Public company",
"registeredOn": "1998-03-04",
"address": "Tower C, Zambezi Towers, Plot 54352, Central Business District, Gaborone, Botswana",
"previousNames": [
"Letshego Holdings Limited (04 March 1998 - 17 April 2023)",
"Letshego Africa Holdings Limited (18 April 2023 - 07 May 2023)",
"Letshego Holdings Limited (08 May 2023 - 06 September 2023)"
],
"previousAddresses": [
"2nd Floor Letshego Place, Plot 22, Khama Crescent, The Mall, Gaborone, Botswana (04 March 1998 - 09 January 2022)"
],
"fields": [ … ],
"text": "…"
}
Alongside items you get page, pageSize, hasMore, total, sourcePagesFetched and warnings.
When a search field is null
A null here means CIPA's result card does not show that detail. It is not a failed read.
- Business names appear with
uin: nullandentityType: null. CIPA never prints a business name's number on its public pages. - Removed or cancelled entities often have
address: nullbecause the card lists no address. totalis null when CIPA does not state a count, or when repeated results were removed across its pages. UsehasMoreto page.
Read an entity
You give it one number. CIPAget opens that company or business name on CIPA's site, clicks through its tabs, and returns what each tab shows: general details, addresses, directors, secretaries, shareholders, share allocations, beneficial owners, auditors, the data behind the ownership diagram, and the list of filings.
Unless you say otherwise it also opens the “show previous” lists, so you get former directors, old addresses and earlier names too.
Parameters
idstringrequired- In the path. A company UIN (
BWfollowed by 5 to 20 digits, for exampleBW00000790718), or a business-name registration number (BN, the year, a slash and a number, for exampleBN2019/11223). In a URL, write the slash as%2F. includestringdefault all- Section keys separated by commas, or
allon its own. Fewer sections means a faster call. historybooleandefault true- Also open the “show previous…” lists: former directors, old addresses, earlier names and statuses.
filingDetailsbooleandefault false- Open each filing's own dialog as well. Much slower.
maxPagesintegerdefault 10- Most pages to read in any one list, 1 to 20.
Limits
- It needs the exact number, not a name. Search first to find a company's UIN.
- Reading everything takes 2.5 to 7 seconds, and longer for companies with long histories. Ask for fewer sections to make it faster.
- Each list is read for at most
maxPagespages: 10 unless you change it, and never more than 20. When a list is longer,completeis false andwarningsnames the section that stopped early. - Ownership data covers the entity's direct owners only. Companies that own it, or that it owns, are not opened.
- The filings section is a list of what was filed and when. It does not contain the filed papers.
- It cannot read anything CIPA hides from the public. A tab CIPA restricts comes back with status
restricted. - PDFs are not part of this answer. They have their own request.
Check complete. When it is false, a page limit was reached or a section could not be read, and warnings says which one. An incomplete answer is never stored in the cache.
The shape of the answer is described under Response format, and the section keys each kind of entity has under Entity types.
Download documents
CIPA offers two free PDFs for an entity: its certificate (incorporationCertificate) and a standard extract (standardExtract). This request asks CIPA for both and returns them inside the JSON.
Each file arrives as base64 text in contentBase64, with its filename, its size in sizeBytes and a sha256 hash. Decode the text and you have the original PDF, byte for byte.
Parameters
idstringrequired- In the path. The same company UIN or business-name number that Read an entity takes.
Limits
- It is slow. CIPA builds the PDFs when asked, so expect 10 to 20 seconds. The request gives up after 180 seconds.
- Only these two free documents. CIPAget never buys a document, never logs in and never uses “Email PDF”.
- One document request runs at a time. Others wait their turn.
- Each PDF can be at most 10 MiB.
- A document CIPA does not offer comes back with status
unavailableand no content. If one download fails, the other is still returned andcompleteis false. - The answer is large: base64 text is about a third bigger than the file itself.
- The hash shows the bytes arrived intact. It does not prove the document is genuine.
Saving the files in your own code
const res = await fetch('http://127.0.0.1:3000/v1/entities/BW00000790718/documents');
const { data } = await res.json();
for (const doc of data.documents) {
if (doc.status === 'available') {
await fs.writeFile(doc.filename, Buffer.from(doc.contentBase64, 'base64'));
}
}
This request runs on its own browser and its own waiting line, so it never holds up searches or detail reads.
Check the server
Tells you the server is running, how many lookups are running or waiting, how many answers are stored, and whether lookups are paused after a refusal by CIPA.
| Property | What it means |
|---|---|
active, queued, concurrency | Detail reads running now, waiting, and allowed at once. search and documents hold the same three numbers for their own lines. |
cache | How many answers are stored, and their size in bytes |
cooldownMs | Milliseconds left in the pause after CIPA refused a session. Zero means no pause. |
transport | Counters for how pages were opened. Useful when reporting a problem. |
Limits
- It never contacts CIPA, so it cannot tell you whether CIPA's own site is up.
- It needs no API key, and it shows no company data.
Response format
Every successful response is { data, meta }. meta.cache is miss for a fresh read from CIPA, hit for a stored answer and coalesced when an identical request was already running. meta.durationMs is how long the call took.
{
"data": {
"uin": "BW00000790718",
"name": "Sefalana Cash & Carry Limited",
"status": "Registered",
"entityType": "Public company",
"availableSections": [ { "key": "general", "label": "General Details" }, … ],
"sections": { "general": { … }, "addresses": { … }, "directors": { … }, … },
"complete": true,
"warnings": [],
"retrievedAt": "2026-10-03T02:42:17.348Z",
"source": "https://www.cipa.co.bw/master/ui/start/CIPARegisterSearch"
},
"meta": { "cache": "miss", "ageMs": 0, "durationMs": 4249 }
}
A section
| Property | What it holds |
|---|---|
status | available, empty (CIPA says there is nothing, such as “No auditors added yet”), unavailable (this entity has no such tab), restricted or error |
fields | Every labelled value on the tab, in page order |
records | People and organisations, each with its own fields |
tables | Tables as headers and rows: share allocations, filing history |
text | The tab's visible text, for anything that is not a labelled field |
ownershipStatements | On visualisation only: the ownership data behind CIPA's diagram, in the open BODS 0.3 format |
filingDetails | Only with filingDetails=true |
pagesFetched, total, complete, warnings | How much of the tab was read |
A field
{ "key": "registeredOfficeAddress",
"label": "Registered Office Address",
"value": "Plot 203, Independence Avenue, Gaborone, Botswana (Effective from 15 September 2025)",
"displayValue": "Plot 203, Independence Avenue, Gaborone, Botswana (Effective from 15 September 2025)" }
{ "key": "previousRegisteredOfficeAddress",
"label": "Registered Office Address",
"value": "Grant Thornton Business Services, Plot 50370, Acumen Park, Fairgrounds, Gaborone, Botswana (Effective from 12 October 1973 to 14 September 2025)",
"displayValue": "…",
"group": "Previous Addresses" }
valueis null only when CIPA shows a placeholder such as “Not specified”.displayValuealways keeps the exact text from the page. Zero and “No” are real values and stay as they are.groupis the sub-heading the field sits under on the page: “Previous Addresses”, “Previous Statuses”, “Company Details”, “Interests”, “Shareholder Nominator Details” and so on.- Old values get their own keys that start with
previous, such aspreviousCompanyStatus,previousPostalAddressandpreviousCompanyName. A current value is never overwritten by an old one. machineValueappears where CIPA supplies a plain value behind the display text, such as"20330000"for “20,330,000” or"BW"for “Botswana”.
A record
{ "title": "Deloitte Consulting Proprietary Limited (BW00000543030)",
"summary": [ "Plot 64518 Deloitte House, Fairgrounds, Gaborone, Botswana" ],
"group": "Previous Auditors",
"fields": [ … ],
"text": "…" }
groupis null for someone on the current list and names the list otherwise: “Previous Directors”, “Previous Shareholders”, “Previous Auditors”. To count current directors, keep the records whosegroupis null.summaryholds the lines CIPA prints under the name, such as a corporate shareholder's office or a secretary's country.
Sections always arrive in the same order: the detail tabs as CIPA orders them, then Visualisation and Filings.
Entity types
CIPA's search form lists five kinds of entity. All five can be read. Removed companies and cancelled business names open normally and report Removed or Cancelled as their status.
| Type | Identifier | Section keys |
|---|---|---|
| Private company, public company | UIN | general addresses directors secretaries shareholders shareAllocations beneficialOwners auditors visualisation filings |
| Company limited by guarantee | UIN | members takes the place of shareholders and shareAllocations |
| External company | UIN | As a company, with authorisedPersons (persons authorised to accept service) and no secretaries |
| Close company | UIN | general addresses members beneficialOwners accountingOfficers visualisation filings |
| Business name | BN number | general addresses proprietors filings |
availableSections in each response lists the tabs that entity really has. A tab the API has not seen before is still read and returned under a key made from its label. Asking for a section the entity does not have returns it with status unavailable.
Business-name numbers come from the certificate. A name search returns business names with uin: null, and the number cannot be worked out from the search result. Use the number printed on the registration certificate, for example /v1/entities/BN2019%2F11223.
Errors
Errors are { "error": { "code", "message", "retryable", "requestId" } }. Retry only when retryable is true, and wait for the Retry-After header when there is one. The API never retries on its own.
| HTTP | Code | What happened |
|---|---|---|
| 400 | INVALID_REQUEST and other INVALID_… codes, PAGE_OUT_OF_RANGE, SEARCH_PAGE_LIMIT | A parameter is missing or out of range. The message says which. |
| 401 | UNAUTHORIZED | The server has an API key and the request did not send it. |
| 403 | HOST_NOT_ALLOWED, CROSS_SITE_REQUEST | The server has no API key and was addressed by a name other than localhost, or a browser sent the request from a page that is not the server's own. |
| 404 | ENTITY_NOT_FOUND | CIPA's search returned no entity with that identifier. |
| 409 | ENTITY_AMBIGUOUS | CIPA returned more than one business name for that number. Use search to tell them apart. |
| 429 | RATE_LIMITED | Too many requests to this server in a minute. |
| 502 | UPSTREAM_LAYOUT_CHANGED, UPSTREAM_IDENTITY_MISMATCH, UPSTREAM_UNAVAILABLE | CIPA's page did not look as expected, opened a different entity, or did not respond properly. Nothing uncertain is returned. |
| 503 | QUEUE_FULL, QUEUE_TIMEOUT | Too many lookups are waiting. Try again shortly. |
| 503 | UPSTREAM_RATE_LIMITED, UPSTREAM_ACCESS_RESTRICTED, UPSTREAM_COOLDOWN | CIPA limited or refused the session. The server then pauses all lookups for 60 seconds. |
| 504 | UPSTREAM_TIMEOUT, OPERATION_TIMEOUT | CIPA did not answer in time. |
Speed
Almost all of the time in a call is CIPA's own website answering. A full read opens the entity in up to four browser views at once and clicks through every tab, and each click is a round trip to CIPA.
| Call | Typical time |
|---|---|
| Any repeat of the same call within five minutes (served from the cache) | under 0.01 s |
| Search (later result pages take up to 3 s) | 0.4 to 2 s |
include=general | about 2 s |
| Four sections of a large company | about 5 s |
| Everything, small or medium company | 2.5 to 5 s |
| Everything, large company with long filing and director histories | 5 to 7 s |
| The first read of an entity since the server started | 1 to 2 s more |
Getting faster answers
- Ask only for the sections you need with
include. Filings is the slowest tab because CIPA shows at most 20 filings per page. - Set
history=falsewhen you only need current office holders and addresses. - Repeat calls are free for five minutes. Raise
CACHE_TTL_MSif slightly older answers are acceptable.
Measured on 3 October 2026
Full reads with every section and history, cache off, on a laptop that was busy with other work. “Before” is the version from 12 September 2026. Times are seconds.
| Company | Before, first | Before, repeat | Now, first | Now, repeats | Directors returned |
|---|---|---|---|---|---|
| A small private company | 6.6 | 3.6 | 3.9 | 2.6, 3.5 | 1 |
| Sefalana Cash & Carry | 4.8 | 2.7 | 4.3 | 2.7, 2.7 | 3 |
| Mascom Wireless | 5.2 | 5.0 | 5.1 | 4.4, 4.2 | 15 |
| Kgalagadi Breweries | 6.2 | 4.9 | 5.9 | 4.3, 4.5 | 15 before, 17 now |
| Choppies Enterprises | 11.3 | 5.9 | 6.8 | 5.0, 5.0 | 9 |
| Botswana Telecommunications | 10.4 | 5.9 | 5.6 | 5.1, 4.5 | 20 before, 23 now |
| Debswana Diamond Company | 7.2 | 13.9 | 7.0 | 5.1, 5.2 | 23 or 38 before, 38 now |
| Engen Botswana | 7.0 | 9.0 | 7.3 | 5.0, 5.8 | 12 |
| First National Bank of Botswana | 8.7 | 7.1 | 7.9 | 6.2, 6.8 | 21 |
| Letshego Africa Holdings | 10.0 | 8.7 | 8.6 | 7.3, 6.6 | 27 or 20 before, 27 now |
All 30 reads in this run were complete, and each company's three responses were identical. In the earlier version, four of these ten companies sometimes lost directors after the first page. These are measurements from one machine on one day, not a guarantee.
Settings
Set these in .env. The defaults suit one person running it locally.
| Setting | Default | What it changes |
|---|---|---|
PORT, HOST | 3000, 127.0.0.1 | Where the server listens |
API_KEY | empty | Required before HOST can be anything other than localhost |
ALLOWED_HOSTS | empty | Extra hostnames, separated by commas, that a server without an API key may be reached by. Without it, such a server only answers to localhost. |
BROWSER_CHANNEL | empty | chrome uses the installed Chrome; empty uses the bundled Chromium |
HYBRID_NAVIGATION | false | true is the faster mode measured above. false does every step through the visible page controls. |
BROWSER_PREWARM | true | Open CIPA's empty search form at startup so the first call is not slow |
CACHE_TTL_MS | 300000 | How long answers are kept in memory. 0 turns the cache off. |
ROUTE_CACHE_TTL_MS | 43200000 | How long the server remembers an entity's public page link, which saves a repeat search. No company data is kept. |
DETAIL_CONCURRENCY | 4 | Browser views used for one full read, 1 to 4 |
BROWSER_CONCURRENCY, SEARCH_CONCURRENCY | 1, 1 | Detail reads and searches handled at the same time |
RATE_LIMIT | 60 | Requests per minute this server accepts |
ACTION_TIMEOUT_MS, OPERATION_TIMEOUT_MS, DOCUMENT_TIMEOUT_MS | 20000, 180000, 180000 | Time limits for one page action, one lookup and one document download |
How it works
CIPAget has no private data source. For each call it opens CIPA's public “Search the Register” page in a headless Chrome browser, searches for what you asked, opens the matching entity, visits the tabs and reads what is displayed. It then returns that as JSON.
Nothing is fetched until you ask for it, and no register data is written to disk. Answers live in memory for a few minutes.
It does not log in, pay for documents, solve access challenges or go around restrictions. If CIPA limits or refuses a session, the server stops and waits.
It does not crawl or list the register, and it does not follow links from one company to another.
The identifier on the opened page must match the one you asked for, or the call fails.
CIPA can change its site at any time. When a page no longer looks as expected, the API reports an error and does not guess.
Reading a site with an automated browser is still automated collection, even though the register is public by law. Keep usage occasional and modest, and get advice before using the data commercially or publishing personal details from it.