CIPAget

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.

Base URL 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.

The 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:

AddressWhat 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.jsonOpenAPI description for generating clients
http://127.0.0.1:3000/healthzQueue, 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

Leave this empty unless the server sets 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.

Only what CIPA shows

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.

One entity at a time

It cannot list the register, export it in bulk, or follow links from one company to another.

Answers are reused for five minutes

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.

Requests wait their turn

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.

60 requests a minute

Past that, the server answers 429 until the minute is over.

If CIPA refuses, everything pauses

When CIPA limits or blocks the browser session, all lookups stop for 60 seconds.

A key may be required

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.

Read an entity

GET/v1/entities/:id

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 (BW followed by 5 to 20 digits, for example BW00000790718), or a business-name registration number (BN, the year, a slash and a number, for example BN2019/11223). In a URL, write the slash as %2F.
includestringdefault all
Section keys separated by commas, or all on 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 maxPages pages: 10 unless you change it, and never more than 20. When a list is longer, complete is false and warnings names 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

GET/v1/entities/:id/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 unavailable and no content. If one download fails, the other is still returned and complete is 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

GET/healthz

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.

PropertyWhat it means
active, queued, concurrencyDetail reads running now, waiting, and allowed at once. search and documents hold the same three numbers for their own lines.
cacheHow many answers are stored, and their size in bytes
cooldownMsMilliseconds left in the pause after CIPA refused a session. Zero means no pause.
transportCounters 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

PropertyWhat it holds
statusavailable, empty (CIPA says there is nothing, such as “No auditors added yet”), unavailable (this entity has no such tab), restricted or error
fieldsEvery labelled value on the tab, in page order
recordsPeople and organisations, each with its own fields
tablesTables as headers and rows: share allocations, filing history
textThe tab's visible text, for anything that is not a labelled field
ownershipStatementsOn visualisation only: the ownership data behind CIPA's diagram, in the open BODS 0.3 format
filingDetailsOnly with filingDetails=true
pagesFetched, total, complete, warningsHow 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" }

A record

{ "title": "Deloitte Consulting Proprietary Limited (BW00000543030)",
  "summary": [ "Plot 64518 Deloitte House, Fairgrounds, Gaborone, Botswana" ],
  "group": "Previous Auditors",
  "fields": [ … ],
  "text": "…" }

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.

TypeIdentifierSection keys
Private company, public companyUINgeneral addresses directors secretaries shareholders shareAllocations beneficialOwners auditors visualisation filings
Company limited by guaranteeUINmembers takes the place of shareholders and shareAllocations
External companyUINAs a company, with authorisedPersons (persons authorised to accept service) and no secretaries
Close companyUINgeneral addresses members beneficialOwners accountingOfficers visualisation filings
Business nameBN numbergeneral 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.

HTTPCodeWhat happened
400INVALID_REQUEST and other INVALID_… codes, PAGE_OUT_OF_RANGE, SEARCH_PAGE_LIMITA parameter is missing or out of range. The message says which.
401UNAUTHORIZEDThe server has an API key and the request did not send it.
403HOST_NOT_ALLOWED, CROSS_SITE_REQUESTThe 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.
404ENTITY_NOT_FOUNDCIPA's search returned no entity with that identifier.
409ENTITY_AMBIGUOUSCIPA returned more than one business name for that number. Use search to tell them apart.
429RATE_LIMITEDToo many requests to this server in a minute.
502UPSTREAM_LAYOUT_CHANGED, UPSTREAM_IDENTITY_MISMATCH, UPSTREAM_UNAVAILABLECIPA's page did not look as expected, opened a different entity, or did not respond properly. Nothing uncertain is returned.
503QUEUE_FULL, QUEUE_TIMEOUTToo many lookups are waiting. Try again shortly.
503UPSTREAM_RATE_LIMITED, UPSTREAM_ACCESS_RESTRICTED, UPSTREAM_COOLDOWNCIPA limited or refused the session. The server then pauses all lookups for 60 seconds.
504UPSTREAM_TIMEOUT, OPERATION_TIMEOUTCIPA 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.

CallTypical 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=generalabout 2 s
Four sections of a large companyabout 5 s
Everything, small or medium company2.5 to 5 s
Everything, large company with long filing and director histories5 to 7 s
The first read of an entity since the server started1 to 2 s more

Getting faster answers

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.

CompanyBefore, firstBefore, repeatNow, firstNow, repeatsDirectors returned
A small private company6.63.63.92.6, 3.51
Sefalana Cash & Carry4.82.74.32.7, 2.73
Mascom Wireless5.25.05.14.4, 4.215
Kgalagadi Breweries6.24.95.94.3, 4.515 before, 17 now
Choppies Enterprises11.35.96.85.0, 5.09
Botswana Telecommunications10.45.95.65.1, 4.520 before, 23 now
Debswana Diamond Company7.213.97.05.1, 5.223 or 38 before, 38 now
Engen Botswana7.09.07.35.0, 5.812
First National Bank of Botswana8.77.17.96.2, 6.821
Letshego Africa Holdings10.08.78.67.3, 6.627 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.

SettingDefaultWhat it changes
PORT, HOST3000, 127.0.0.1Where the server listens
API_KEYemptyRequired before HOST can be anything other than localhost
ALLOWED_HOSTSemptyExtra 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_CHANNELemptychrome uses the installed Chrome; empty uses the bundled Chromium
HYBRID_NAVIGATIONfalsetrue is the faster mode measured above. false does every step through the visible page controls.
BROWSER_PREWARMtrueOpen CIPA's empty search form at startup so the first call is not slow
CACHE_TTL_MS300000How long answers are kept in memory. 0 turns the cache off.
ROUTE_CACHE_TTL_MS43200000How long the server remembers an entity's public page link, which saves a repeat search. No company data is kept.
DETAIL_CONCURRENCY4Browser views used for one full read, 1 to 4
BROWSER_CONCURRENCY, SEARCH_CONCURRENCY1, 1Detail reads and searches handled at the same time
RATE_LIMIT60Requests per minute this server accepts
ACTION_TIMEOUT_MS, OPERATION_TIMEOUT_MS, DOCUMENT_TIMEOUT_MS20000, 180000, 180000Time 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.

On demand only

Nothing is fetched until you ask for it, and no register data is written to disk. Answers live in memory for a few minutes.

Public pages only

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.

One entity at a time

It does not crawl or list the register, and it does not follow links from one company to another.

It verifies what it opened

The identifier on the opened page must match the one you asked for, or the call fails.

Not an official CIPA service

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.