Skip to content

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:

ItemWhat it isExample
Base URLWhere the API lives. Your Johari administrator gives it to youhttps://api.example.com
API keySays who is calling. Sent on every call, in the x-api-key header. jh_key_ followed by 64 hex charactersjh_key_67a759397b0b364bac4d06874dea60d042d48d435af11bbf8ad711da6a717238
Signing secretProves 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 example Neema 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-return

Quick 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/companies
json
{
  "status": "success",
  "data": [
    {
      "id": "6650f1a2b3c4d5e6f7a8b901",
      "name": "Example Construction Ltd"
    }
  ],
  "message": "Companies fetched successfully"
}

2. List that company's sites

GET /api/companies/6650f1a2b3c4d5e6f7a8b901/sites
json
{
  "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-codes
json
{
  "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/approvers
json
{
  "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/requests
json
{
  "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/6650f1a2b3c4d5e6f7a8b9e1

Every 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"
}
FieldMeaning
status"success" for HTTP 200. "error" for everything else
dataThe result on success. On an error it is usually the error text, null, or (for 409) what the first call with that idempotencyKey returned
messageA 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 ​

ThingFormatExample
IDsStrings. Company, request and user (approver) IDs are 24 hex characters. Site IDs are UUIDs"6650f1a2b3c4d5e6f7a8b901", "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90"
Cost codesStrings, even though they look like numbers"1003"
AmountsA 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"
DatesISO 8601 strings in UTC"2026-10-29T10:12:36.571Z"
Request numbersStrings"1042"
FilesSent inside the JSON body as standard base64 text. See Attaching files
BodyJSON, sent with Content-Type: application/json. At most 35 MB. A larger body gets 413
ListsNot paginated. Each list endpoint returns everything your key's user can raise requests for, in one response
Rate limitsPer 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/companies and get a 200. 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 idempotencyKey and an admin from List Approvers
  • [ ] Build a callback endpoint that verifies the signature, answers 200, and handles both event values (request.status-changed and request.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 ​