---
url: /api/create-request.md
---
# Create a Request

Raises a new payment request in Johari. It appears in Johari like a request typed in by a person, and goes through the same approval and payment steps. Its creator is shown as the API key's user with ` (API)` after the name, e.g. `Neema Mollel (API)`, so people can see a system raised it.

## Endpoint

| Method | URL |
|---|---|
| `POST` | `/api/requests` |

Send the body as JSON with `Content-Type: application/json`, and sign the exact JSON text:

```
<timestamp>.POST./api/requests.<the JSON body>
```

## Body fields

| Field | Type | Required | Description |
|---|---|---|---|
| `idempotencyKey` | `string` | **Yes** | Your unique ID for this request, 1 to 255 characters. A UUID is ideal. See [Idempotency](/guide/idempotency) |
| `description` | `string` | **Yes** | What the payment is for, e.g. `"Cement for slab, Block B"`. Up to 500 characters |
| `amount` | `number` or `string` | **Yes** | Greater than 0, at most 2 decimal places. A JSON number (`150000`, `1500.5`), or a string of digits with optional thousands commas and an optional decimal part (`"150,000"`, `"150,000.50"`). Any other string is refused, e.g. `"0x10"`, `"1e3"`, `"-5"`, `".5"` |
| `requestingCompany` | `string` | **Yes** | A company `id` from [List Companies](/api/companies) |
| `requestingSite` | `string` | **Yes** | A site `id` from [List Sites](/api/sites), for that company |
| `costCode` | `string` | **Yes** | A cost code `value` from [List Cost Codes](/api/cost-codes), for that company |
| `modeOfPayment` | `string` | **Yes** | `"cash"` or `"bank"` |
| `admin` | `string` | **Yes** | An approver `id` (24 hex characters) from [List Approvers](/api/approvers) for `requestingSite`. See [The admin field](#the-admin-field) |
| `currency` | `string` | No | `"TZS"` (default), `"USD"`, `"KES"`, `"ZAR"`, `"AED"` or `"GBP"` |
| `isUrgent` | `boolean` | No | `true` to mark it urgent. Default `false` |
| `notes` | `string` | No | A note shown on the request in Johari. Up to 2000 characters |
| `files` | `array` | No | Up to 5 files to attach to the request. See [Attaching files](#attaching-files) |
| `callbackURL` | `string` | No | Where to send [callbacks](/guide/callbacks) when the status changes. Up to 2048 characters. On the live server it must be `https://` and point to a public address. See [Turning callbacks on](/guide/callbacks#turning-callbacks-on) |

Any other field is ignored. Receipts cannot be attached through the API. Files can: see [Attaching files](#attaching-files).

## The admin field

`admin` is **required**. It is an approver `id` from [List Approvers](/api/approvers) for the site in `requestingSite`.

Johari still routes the request through the site's approval rules first. `admin` is the approver used when **no one** in those rules can approve it (for example, the amount is above everyone's limit). This is the same as the web form, where the approver field is always required.

## Attaching files

`files` is an optional list of up to **5** files. They are attached to the request, and approvers see them in Johari like files attached on the web form.

Each item in `files` is an object:

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | **Yes** | The file name shown in Johari, e.g. `"invoice-1042.pdf"`. Up to 255 characters. No `/` or `\`, and no control or invisible formatting characters (for example right-to-left marks) |
| `contentType` | `string` | **Yes** | The file type. One of the values in the table below |
| `content` | `string` | **Yes** | The file's bytes, encoded as standard base64. The bytes must really be of the type in `contentType` |

Accepted `contentType` values. When you attach a file from disk, pick `contentType` from its extension with this table; any other extension cannot be attached:

| `contentType` | File | File extensions |
|---|---|---|
| `application/pdf` | PDF | `.pdf` |
| `image/jpeg` | JPEG image | `.jpg`, `.jpeg` |
| `image/png` | PNG image | `.png` |
| `image/webp` | WebP image | `.webp` |
| `image/heic` | HEIC image | `.heic` |
| `application/msword` | Word | `.doc` |
| `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | Word | `.docx` |
| `application/vnd.ms-excel` | Excel | `.xls` |
| `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | Excel | `.xlsx` |

Rules:

* At most **5** files. Leaving `files` out and sending `"files": []` are the same: no files.
* All files together at most **25 MB**, counted after decoding. That keeps the whole JSON body under the **35 MB** limit; a larger body gets `413`.
* `content` is standard base64: `A`–`Z`, `a`–`z`, `0`–`9`, `+` and `/`, with `=` padding. Its length is a multiple of 4. No `data:` prefix (such as `data:application/pdf;base64,`), and no line breaks.
* The file's bytes must really be of the declared `contentType`. Johari checks the file's first bytes: a PDF must contain `%PDF-` in its first 1024 bytes, JPEG, PNG, WebP and HEIC images are checked by their standard signatures, `.doc` and `.xls` must be the old Office format, and `.docx` and `.xlsx` must be ZIP-based. Otherwise you get `400` (`files[0].content does not match its contentType`). Pick `contentType` from the extension table above, and do not send placeholder text as `content`.
* Johari's server uploads the files. There is no separate upload step and no presigned URL.
* Signing does not change. The signature covers the whole JSON body, base64 `content` included.
* [Idempotency](/guide/idempotency) still applies. A retry must send the identical body, files included.

How to read a file and base64-encode it:

| Language | Code |
|---|---|
| Node.js | `fs.readFileSync(path).toString("base64")` |
| Python | `base64.b64encode(open(path, "rb").read()).decode()` |
| bash | `base64 < invoice-1042.pdf \| tr -d '\n'` |

After the request is created, every file on it is listed in `files` in [Get a Request](/api/get-request) and in [callbacks](/guide/callbacks): the files sent when creating it, the cashier's proof of payment, the EFD receipt, any file people attach to the request in Johari, and any file you add later with [Add Files](/api/add-files). Download one with [Get a File](/api/get-file).

## Validation rules

* All required fields must be present and of the type shown. Text must not be empty.
* `amount` must be greater than 0 with at most 2 decimal places. `1500.5` and `"1,500.50"` are fine; `1500.555` is refused. A string may only hold digits, thousands commas and one decimal part: `"0x10"`, `"1e3"`, `"-5"` and `".5"` are refused.
* `requestingSite` must be a site **of** `requestingCompany`, and `costCode` a cost code **of** `requestingCompany`.
* `admin` must be an approver **of** `requestingSite`.
* `currency` and `modeOfPayment` only accept the values listed above, in lower case for `modeOfPayment`, upper case for `currency`.
* `files`, when sent, must follow the [file rules](#attaching-files).
* `callbackURL`, when sent, must be a full URL of up to 2048 characters. On the live server it must be `https://`, and `localhost` or an IP address that is not public is refused. See [Turning callbacks on](/guide/callbacks#turning-callbacks-on).
* `idempotencyKey` must not have been used before for a **different** request. See [Idempotency](/guide/idempotency).

Any rule that fails returns `400` with a message naming the field. The full list is on the [Errors](/guide/errors#create-request-errors) page.

## Response

`data` on success:

| Field | Type | Description |
|---|---|---|
| `requestID` | `string` | The new request's ID. **Save it**: you need it for [Get a Request](/api/get-request) and to match callbacks |
| `requestNumber` | `string` | The number people see in Johari, e.g. `"1042"` |
| `status` | `string` | `"pending"` (waiting for approval) or `"approved"` (approved straight away). See [Request Statuses](/guide/statuses) |

## Examples

### The smallest valid request

```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"
}
```

### Every field

The `content` is shortened here. Send the whole file.

```json
{
  "idempotencyKey": "PO-2026-00123-PAY-1",
  "description": "Steel bars, 12mm, 200 pieces",
  "amount": "4,250,000",
  "currency": "TZS",
  "requestingCompany": "6650f1a2b3c4d5e6f7a8b901",
  "requestingSite": "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90",
  "costCode": "1801",
  "modeOfPayment": "bank",
  "isUrgent": true,
  "notes": "Supplier needs payment before Friday delivery",
  "admin": "6650f1a2b3c4d5e6f7a8b7a1",
  "files": [
    {
      "name": "quote-steel-200.pdf",
      "contentType": "application/pdf",
      "content": "JVBERi0xLjQK"
    }
  ],
  "callbackURL": "https://erp.yourcompany.com/johari/callback"
}
```

### A US dollar request

```json
{
  "idempotencyKey": "3a1f9c2e-7b4d-4e8a-a6c1-5d2e9f0b8c71",
  "description": "Imported tiles, deposit",
  "amount": 1200.5,
  "currency": "USD",
  "requestingCompany": "6650f1a2b3c4d5e6f7a8b901",
  "requestingSite": "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90",
  "costCode": "1802",
  "modeOfPayment": "bank",
  "admin": "6650f1a2b3c4d5e6f7a8b7a1"
}
```

### A request with one file

The `content` is shortened here. Send the whole file, base64-encoded.

```json
{
  "idempotencyKey": "c4d8e2f1-6b3a-4c9d-8e7f-2a1b3c4d5e6f",
  "description": "Cement for slab",
  "amount": 150000,
  "requestingCompany": "6650f1a2b3c4d5e6f7a8b901",
  "requestingSite": "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90",
  "costCode": "1003",
  "modeOfPayment": "bank",
  "admin": "6650f1a2b3c4d5e6f7a8b7a1",
  "files": [
    {
      "name": "invoice-1042.pdf",
      "contentType": "application/pdf",
      "content": "JVBERi0xLjQK"
    }
  ]
}
```

### Code

::: code-group

```bash [cURL]
BODY=$(cat <<'EOF'
{
  "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"
}
EOF
)
TS=$(date +%s)
SIG=$(printf '%s' "$TS.POST./api/requests.$BODY" \
  | openssl dgst -sha256 -hmac "$SIGNING_SECRET" \
  | sed 's/^.* //')
curl -X POST "$BASE_URL/api/requests" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $API_KEY" \
  -H "x-timestamp: $TS" \
  -H "x-signature: $SIG" \
  --data-raw "$BODY"
```

```js [Node.js]
const crypto = require("crypto");

// callJohari is from the Signing page
const result = await callJohari("POST", "/api/requests", {
  idempotencyKey: crypto.randomUUID(),
  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",
});

if (result.httpStatus === 200) {
  console.log("Created request", result.data.requestNumber, result.data.requestID);
} else {
  console.error(result.httpStatus, result.message, result.data);
}
```

```python [Python]
import uuid

# call_johari is from the Signing page
result = call_johari("POST", "/api/requests", {
    "idempotencyKey": str(uuid.uuid4()),
    "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",
})

if result["httpStatus"] == 200:
    print("Created request", result["data"]["requestNumber"], result["data"]["requestID"])
else:
    print(result["httpStatus"], result["message"], result["data"])
```

:::

### Code: attaching a file

Read the file, base64-encode it, and put it in `files`. Then sign and send the body as usual.

::: code-group

```bash [cURL]
# The file as base64, on one line
FILE_B64=$(base64 < invoice-1042.pdf | tr -d '\n')

# Build the body in a file: with a file inside, it is too long for a command-line argument
cat > body.json <<EOF
{
  "idempotencyKey": "c4d8e2f1-6b3a-4c9d-8e7f-2a1b3c4d5e6f",
  "description": "Cement for slab",
  "amount": 150000,
  "requestingCompany": "6650f1a2b3c4d5e6f7a8b901",
  "requestingSite": "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90",
  "costCode": "1003",
  "modeOfPayment": "bank",
  "admin": "6650f1a2b3c4d5e6f7a8b7a1",
  "files": [
    {
      "name": "invoice-1042.pdf",
      "contentType": "application/pdf",
      "content": "$FILE_B64"
    }
  ]
}
EOF

# Sign the exact bytes of body.json, then send that same file
TS=$(date +%s)
SIG=$( { printf '%s' "$TS.POST./api/requests."; cat body.json; } \
  | openssl dgst -sha256 -hmac "$SIGNING_SECRET" \
  | sed 's/^.* //')
curl -X POST "$BASE_URL/api/requests" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $API_KEY" \
  -H "x-timestamp: $TS" \
  -H "x-signature: $SIG" \
  --data-binary @body.json
```

```js [Node.js]
const crypto = require("crypto");
const fs = require("fs");

// callJohari is from the Signing page
const result = await callJohari("POST", "/api/requests", {
  idempotencyKey: crypto.randomUUID(),
  description: "Cement for slab",
  amount: 150000,
  requestingCompany: "6650f1a2b3c4d5e6f7a8b901",
  requestingSite: "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90",
  costCode: "1003",
  modeOfPayment: "bank",
  admin: "6650f1a2b3c4d5e6f7a8b7a1",
  files: [
    {
      name: "invoice-1042.pdf",
      contentType: "application/pdf",
      content: fs.readFileSync("invoice-1042.pdf").toString("base64"),
    },
  ],
});

console.log(result.httpStatus, result.message, result.data);
```

```python [Python]
import base64
import uuid

# call_johari is from the Signing page
with open("invoice-1042.pdf", "rb") as f:
    content = base64.b64encode(f.read()).decode()

result = call_johari("POST", "/api/requests", {
    "idempotencyKey": str(uuid.uuid4()),
    "description": "Cement for slab",
    "amount": 150000,
    "requestingCompany": "6650f1a2b3c4d5e6f7a8b901",
    "requestingSite": "3c9a7f52-1d4e-4b8a-9f6c-2e1d5a7b8c90",
    "costCode": "1003",
    "modeOfPayment": "bank",
    "admin": "6650f1a2b3c4d5e6f7a8b7a1",
    "files": [
        {
            "name": "invoice-1042.pdf",
            "contentType": "application/pdf",
            "content": content,
        }
    ],
})

print(result["httpStatus"], result["message"], result["data"])
```

:::

Response `200`:

```json
{
  "status": "success",
  "data": {
    "requestID": "6650f1a2b3c4d5e6f7a8b9e1",
    "requestNumber": "1042",
    "status": "pending"
  },
  "message": "Request created successfully"
}
```

Response `409`, same `idempotencyKey` sent again:

```json
{
  "status": "error",
  "data": {
    "requestID": "6650f1a2b3c4d5e6f7a8b9e1",
    "requestNumber": "1042",
    "status": "pending"
  },
  "message": "A request with this idempotencyKey has already been processed"
}
```

Response `400`, a field is wrong:

```json
{
  "status": "error",
  "data": "costCode is not one of this company's cost codes",
  "message": "costCode is not one of this company's cost codes"
}
```

Response `400`, `admin` left out:

```json
{
  "status": "error",
  "data": "Please provide an admin: an approver id from the site's approvers list",
  "message": "Please provide an admin: an approver id from the site's approvers list"
}
```

Response `400`, a file that is not valid base64 (here the first file, `files[0]`):

```json
{
  "status": "error",
  "data": "files[0].content must be the file encoded as base64",
  "message": "files[0].content must be the file encoded as base64"
}
```

Every possible error is listed on the [Errors](/guide/errors#create-request-errors) page.

## What happens next

1. People in Johari approve, confirm and pay the request.
2. Every status change is sent to your `callbackURL`, if you gave one. See [Callbacks](/guide/callbacks).
3. You can check the status at any time with [Get a Request](/api/get-request), and download its files with [Get a File](/api/get-file).
4. If the cashier returns it to you (`status` is `"returned"` and `returnedTo` is `"creator"`), answer with [Respond to a Return](/api/respond-to-return). You can add files to it at any time with [Add Files](/api/add-files).

## Common mistakes

::: danger A new idempotencyKey on every retry
Retries must reuse the key, or you may create the same request twice. See [Idempotency](/guide/idempotency).
:::

::: danger Leaving out admin
`admin` is required. Send an approver `id` from [List Approvers](/api/approvers) for the same `requestingSite`. Without it you get `400` (`Please provide an admin: an approver id from the site's approvers list`).
:::

::: danger Mixing IDs from different companies
`requestingSite` and `costCode` must belong to `requestingCompany`. Look them up with that company's ID. `admin` must be an approver of `requestingSite`.
:::

::: danger Numbers where text is expected
`costCode` is text (`"1003"`). `isUrgent` is a boolean (`true`), not `"yes"`.
:::

::: danger `amount` in cents
`amount` is in normal units: `150000` means 150,000 TZS, not 1,500.00.
:::

::: danger `amount` strings in other formats
A string amount may only hold digits, thousands commas and one decimal part. `"1e3"`, `"0x10"`, `"-5"` and `".5"` are refused with `400`. When in doubt, send a JSON number.
:::

::: danger A `data:` prefix or line breaks in file content
`content` is plain standard base64: no `data:application/pdf;base64,` in front, and no line breaks. Many `base64` command-line tools wrap lines; remove the line breaks (for example with `tr -d '\n'`).
:::

::: danger A `contentType` that does not match the file
Johari checks the file's first bytes against `contentType`. Pick `contentType` from the file's extension with the [table above](#attaching-files), and send the real file, not placeholder text. Otherwise you get `400` (`files[0].content does not match its contentType`).
:::

::: danger A different file on a retry
The files are part of the signed body. A retry with the same `idempotencyKey` must send exactly the same files, with the same names, types and content.
:::

## Related pages

* [Idempotency](/guide/idempotency)
* [Callbacks](/guide/callbacks)
* [Get a Request](/api/get-request)
* [Get a File](/api/get-file)
* [Respond to a Return](/api/respond-to-return)
* [Add Files](/api/add-files)
* [List Approvers](/api/approvers)
