Appearance
Getting Started
This page explains what you need before you write any code, and how the whole integration fits together.
What you need
You need three things:
| Item | What it is | Example |
|---|---|---|
| Base URL | Where the API lives. Your Johari administrator gives it to you | https://api.example.com |
| API key | Says who is calling. Sent on every call, in the x-api-key header. jh_key_ followed by 64 hex characters | jh_key_67a759397b0b364bac4d06874dea60d042d48d435af11bbf8ad711da6a717238 |
| Signing secret | Proves the call is really from you. Never sent, only used to sign. jh_secret_ followed by 43 base64url characters (A–Z, a–z, 0–9, -, _) | jh_secret_JwJB-xiOCTBo12TCXjbjzQsbTq070L9-UWUZEpAUzWU |
The examples above are made up. Use your own.
A machine-readable OpenAPI 3.1 spec of the whole API is at /openapi.yaml, for generating clients or agent tools.
Use the key and secret whole
Use both values exactly as you were given them, prefix included. The API key, jh_key_ and all, is the x-api-key header. The signing secret, jh_secret_ and all, is the HMAC-SHA256 key (as UTF-8 text). Never strip the prefix.
The two look different on purpose, so you cannot mix them up: the one starting jh_key_ is sent, the one starting jh_secret_ never is.
Replace the base URL
Examples in this guide use https://api.example.com. Use the base URL your administrator gave you. The test server and the live server have different base URLs and different keys.
Getting a key
A Johari user with the permission Users → "Manage API Keys" generates keys. They open the page of the user the integration will act as, go to the API Keys tab, and generate a key there.
The key and secret are shown only once
The API key and signing secret are shown one time, when they are generated. They cannot be shown again. If you lose them, have the key rotated to get a new pair.
Keep the signing secret on your server. Never put it in a browser, a mobile app, or a git repository.
The key's owner gets a WhatsApp message whenever a key is generated or rotated on their account, and when one is disabled.
Who you are acting as
Every API key belongs to one Johari user: the user whose page it was generated on. Requests made with the key run as that user, with their permissions and limits:
- You only see the companies and sites that user can raise requests for.
- Requests you raise are shown in Johari as created by that user, with
(API)after their name (for exampleNeema Mollel (API)), so people can see a system raised them. The same goes for returns you answer and files you add.
Rotating a key
Rotating generates a new API key and signing secret for the same key. Use it when a pair is lost or may have leaked.
- The old pair stops working immediately. Switch your system to the new pair straight away.
- Every request raised with the key stays yours. Get a Request, Get a File and callbacks keep working with the new pair.
- From then on, callbacks are signed with the new secret. That includes retries of callbacks first sent before the rotation.
Disabling a key
Disabling is permanent. A disabled key cannot be turned back on.
- Every call with that key returns
401(Invalid or disabled API key). - Pending callbacks for its requests stop.
Requests stay tied to the key that raised them. A different key cannot see them: Get a Request with it returns 404. Check the status of any open requests in Johari itself.
If a secret may have leaked
Rotate the key. Your requests and callbacks carry on with the new pair. Only disable a key you will never use again.
How it works
The whole integration is six steps. The last one is only needed when the cashier returns a request to you:
Step 1: Look up the IDs you need
GET /api/companies
GET /api/companies/:companyID/sites
GET /api/companies/:companyID/cost-codes
GET /api/sites/:siteID/approvers
↓
Step 2: Raise the payment request
POST /api/requests
Include an idempotencyKey and an admin (an approver id),
a callbackURL if you want updates, and up to 5 files if you have any.
↓
Step 3: Johari answers straight away
You get back requestID, requestNumber and status ("pending" or "approved").
↓
Step 4: People in Johari approve, pay or reject it
This can take minutes or days.
↓
Step 5: You find out about every status change
Either: we POST to your callbackURL (recommended)
Or: you call GET /api/requests/:requestID
Download any file on the request with
GET /api/requests/:requestID/files/:fileID
↓
Step 6: If the cashier returns it to you, answer
(status is "returned" and returnedTo is "creator")
POST /api/requests/:requestID/files (optional: add a document first)
POST /api/requests/:requestID/respond-to-returnQuick start
These six calls take you from nothing to a raised request. Each one is explained in detail on its own page.
1. List the companies you can raise requests for
GET /api/companiesjson
{
"status": "success",
"data": [
{
"id": "6650f1a2b3c4d5e6f7a8b901",
"name": "Example Construction Ltd"
}
],
"message": "Companies fetched successfully"
}2. List that company's sites
GET /api/companies/6650f1a2b3c4d5e6f7a8b901/sitesjson
{
"status": "success",
"data": [
{
"id": "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90",
"name": "Block A Apartments"
}
],
"message": "Sites fetched successfully"
}3. List that company's cost codes
GET /api/companies/6650f1a2b3c4d5e6f7a8b901/cost-codesjson
{
"status": "success",
"data": [
{
"value": "1003",
"label": "1003 - Security & Guards",
"category": "contractor"
}
],
"message": "Cost codes fetched successfully"
}4. List that site's approvers
GET /api/sites/3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90/approversjson
{
"status": "success",
"data": [
{
"id": "6650f1a2b3c4d5e6f7a8b7a1",
"name": "Amina Juma"
}
],
"message": "Approvers fetched successfully"
}5. Raise the request
admin is required: an approver id from step 4.
POST /api/requestsjson
{
"idempotencyKey": "b7e2c1d4-5a6f-4e3b-9c8d-1f2a3b4c5d6e",
"description": "Cement for slab",
"amount": 150000,
"requestingCompany": "6650f1a2b3c4d5e6f7a8b901",
"requestingSite": "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90",
"costCode": "1003",
"modeOfPayment": "bank",
"admin": "6650f1a2b3c4d5e6f7a8b7a1",
"callbackURL": "https://erp.yourcompany.com/johari/callback"
}json
{
"status": "success",
"data": {
"requestID": "6650f1a2b3c4d5e6f7a8b9e1",
"requestNumber": "1042",
"status": "pending"
},
"message": "Request created successfully"
}6. Wait for callbacks, or check the status yourself
GET /api/requests/6650f1a2b3c4d5e6f7a8b9e1Every one of these calls must carry three headers (x-api-key, x-timestamp, x-signature). See Signing Requests.
Response envelope
Every response uses the same wrapper:
json
{
"status": "success",
"data": {},
"message": "Human-readable description"
}| Field | Meaning |
|---|---|
status | "success" for HTTP 200. "error" for everything else |
data | The result on success. On an error it is usually the error text, null, or (for 409) what the first call with that idempotencyKey returned |
message | A short description, safe to log |
Always check the HTTP status code first. 200 means it worked. Anything else is an error, explained on the Errors page.
The endpoint pages show the whole response, wrapper included.
Conventions
| Thing | Format | Example |
|---|---|---|
| IDs | Strings. Company, request and user (approver) IDs are 24 hex characters. Site IDs are UUIDs | "6650f1a2b3c4d5e6f7a8b901", "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90" |
| Cost codes | Strings, even though they look like numbers | "1003" |
| Amounts | A JSON number, or a string of digits with optional thousands commas and an optional decimal part. Spaces before or after the string are ignored. Nothing else in a string (no "1e3", "0x10", "-5", ".5") | 150000, 1500.5, "150,000", "150,000.50" |
| Dates | ISO 8601 strings in UTC | "2026-10-29T10:12:36.571Z" |
| Request numbers | Strings | "1042" |
| Files | Sent inside the JSON body as standard base64 text. See Attaching files | |
| Body | JSON, sent with Content-Type: application/json. At most 35 MB. A larger body gets 413 | |
| Lists | Not paginated. Each list endpoint returns everything your key's user can raise requests for, in one response | |
| Rate limits | Per API key, per server, per minute: at most 120 calls in total, and at most 30 writes within those. Writes are creates, responses to returns and file uploads together (every POST). Over a limit you get 429 with a Retry-After header (seconds until the minute resets). Wait that long, then retry; for a POST, with the same idempotencyKey and body. After 30 failed sign-ins (any 401) from one address within a minute, every call from that address gets 429 for the rest of that minute, even with a valid key. See Errors |
Integration checklist
- [ ] Get the base URL from your Johari administrator, and an API key and signing secret (Getting a key)
- [ ] Store the key and secret as server-side secrets (environment variables, a vault)
- [ ] Write the signing function and check it against the test vectors
- [ ] Call
GET /api/companiesand get a200. If you do, your signing works - [ ] Map your own projects and cost centres to Johari's company, site and cost code IDs
- [ ] Raise a request with a fresh
idempotencyKeyand anadminfrom List Approvers - [ ] Build a callback endpoint that verifies the signature, answers
200, and handles botheventvalues (request.status-changedandrequest.deleted) - [ ] Handle a request returned to you (
status"returned",returnedTo"creator"): answer it with Respond to a Return, adding any document first with Add Files - [ ] Test it end to end on the test server: raise a request, have someone approve it in Johari, check that your callback receives it
- [ ] Switch to the live base URL and live credentials
What's next
- Signing Requests: how to build the three headers
- Respond to a Return and Add Files: what to do when the cashier returns a request to you
- Full Example: a complete working integration you can copy