---
url: /guide/getting-started.md
---
# 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`](/openapi.yaml), for generating clients or agent tools.

::: warning 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.
:::

::: tip 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.

::: danger 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](#rotating-a-key) 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](/api/get-request), [Get a File](/api/get-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](/api/get-request) with it returns `404`. Check the status of any open requests in Johari itself.

::: tip 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](/guide/signing).

## 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](/guide/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](/api/create-request#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](/guide/errors#too-many-requests) | |

## Integration checklist

* \[ ] Get the base URL from your Johari administrator, and an API key and signing secret ([Getting a key](#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](/guide/signing#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](/api/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](/api/respond-to-return), adding any document first with [Add Files](/api/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](/guide/signing): how to build the three headers
* [Respond to a Return](/api/respond-to-return) and [Add Files](/api/add-files): what to do when the cashier returns a request to you
* [Full Example](/guide/full-example): a complete working integration you can copy
