---
url: /api/add-files.md
---
# Add Files

Adds up to 3 files to a request you raised. It is the same as uploading files on the request's page in Johari.

Use it, for example, when the cashier returned a request asking for a document: add the file, then [respond to the return](/api/respond-to-return).

## Endpoint

| Method | URL |
|---|---|
| `POST` | `/api/requests/:requestID/files` |

| Path parameter | Description |
|---|---|
| `requestID` | The request's `requestID`, from [Create a Request](/api/create-request) or a callback |

Send the body as JSON with `Content-Type: application/json`, and sign the exact JSON text. The path you sign includes the request ID:

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

You can only add files to a request raised with **your** API key (including [child requests](/guide/statuses#partial-payments) made from it). It works in any status.

## Body fields

| Field | Type | Required | Description |
|---|---|---|---|
| `idempotencyKey` | `string` | **Yes** | Your unique ID for this upload, 1 to 255 characters. A UUID is ideal. Use a new one for each new upload, and reuse it only to retry the same upload. See [Idempotency](/guide/idempotency) |
| `files` | `array` | **Yes** | 1 to 3 files. Each item is exactly like a `files` item when you [create a request](/api/create-request#attaching-files): `name`, `contentType` and `content` |

Any other field is ignored.

Rules:

* **1 to 3** files per call. Leaving `files` out, or sending `[]`, gets `400` (`Please provide a file`). For more than 3, make another call with a new key.
* Everything else is the same as when you create a request: the same 9 `contentType` values, the check of each file's first bytes, standard base64 with no `data:` prefix and no line breaks, and at most **25 MB** for all files together. See [Attaching files](/api/create-request#attaching-files).
* The signature covers the whole body, base64 `content` included.

## What happens

Exactly what happens when someone uploads files on the request's page in Johari:

* The files are added to the request. Files already on it stay.
* The request's log records that your key's user uploaded a file, with ` (API)` after the name, e.g. `Neema Mollel (API) uploaded a file`.
* The status does **not** change, so no callback is sent for it. Nobody is notified.
* The files appear in `files` in [Get a Request](/api/get-request#files) straight away, and in the next callbacks. Download one with [Get a File](/api/get-file).

## Response

`data` on success:

| Field | Type | Description |
|---|---|---|
| `requestID` | `string` | The request's ID |
| `requestNumber` | `string` | Its number in Johari, e.g. `"1042"` |
| `files` | `array` | Only the files this call added: `[{ "id": string, "name": string, "type": string, "size": number }]`. `size` is in bytes. Use `id` with [Get a File](/api/get-file) |

## Example

The body (the `content` is shortened here; send the whole file, base64-encoded):

```json
{
  "idempotencyKey": "e2a7c9d1-3b5f-4e8a-9c6d-7f0a1b2c3d4e",
  "files": [
    {
      "name": "stamped-invoice.pdf",
      "contentType": "application/pdf",
      "content": "JVBERi0xLjQK"
    }
  ]
}
```

::: code-group

```bash [cURL]
REQUEST_ID="6650f1a2b3c4d5e6f7a8b9e1"

# The file as base64, on one line
FILE_B64=$(base64 < stamped-invoice.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": "e2a7c9d1-3b5f-4e8a-9c6d-7f0a1b2c3d4e",
  "files": [
    {
      "name": "stamped-invoice.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/$REQUEST_ID/files."; cat body.json; } \
  | openssl dgst -sha256 -hmac "$SIGNING_SECRET" \
  | sed 's/^.* //')
curl -X POST "$BASE_URL/api/requests/$REQUEST_ID/files" \
  -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");

const requestID = "6650f1a2b3c4d5e6f7a8b9e1";

// callJohari is from the Signing page
const result = await callJohari("POST", `/api/requests/${requestID}/files`, {
  idempotencyKey: crypto.randomUUID(),
  files: [
    {
      name: "stamped-invoice.pdf",
      contentType: "application/pdf",
      content: fs.readFileSync("stamped-invoice.pdf").toString("base64"),
    },
  ],
});

if (result.httpStatus === 200) {
  console.log("Added", result.data.files.map((file) => file.id));
} else {
  console.error(result.httpStatus, result.message, result.data);
}
```

```python [Python]
import base64
import uuid

request_id = "6650f1a2b3c4d5e6f7a8b9e1"

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

result = call_johari("POST", f"/api/requests/{request_id}/files", {
    "idempotencyKey": str(uuid.uuid4()),
    "files": [
        {
            "name": "stamped-invoice.pdf",
            "contentType": "application/pdf",
            "content": content,
        }
    ],
})

if result["httpStatus"] == 200:
    print("Added", [file["id"] for file in result["data"]["files"]])
else:
    print(result["httpStatus"], result["message"], result["data"])
```

:::

Response `200`:

```json
{
  "status": "success",
  "data": {
    "requestID": "6650f1a2b3c4d5e6f7a8b9e1",
    "requestNumber": "1042",
    "files": [
      {
        "id": "7c1e9a3f-5b2d-4e6a-8f0c-1d3b5a7c9e2f",
        "name": "stamped-invoice.pdf",
        "type": "application/pdf",
        "size": 48213
      }
    ]
  },
  "message": "File uploaded successfully"
}
```

Response `409`, same `idempotencyKey` sent again after it worked (`data` is what the first call returned):

```json
{
  "status": "error",
  "data": {
    "requestID": "6650f1a2b3c4d5e6f7a8b9e1",
    "requestNumber": "1042",
    "files": [
      {
        "id": "7c1e9a3f-5b2d-4e6a-8f0c-1d3b5a7c9e2f",
        "name": "stamped-invoice.pdf",
        "type": "application/pdf",
        "size": 48213
      }
    ]
  },
  "message": "A request with this idempotencyKey has already been processed"
}
```

Response `400`, more than 3 files:

```json
{
  "status": "error",
  "data": "You can only upload up to 3 files",
  "message": "You can only upload up to 3 files"
}
```

Response `404`, wrong ID, or the request was not raised with your key:

```json
{
  "status": "error",
  "data": "Request not found",
  "message": "Request not found"
}
```

The usual `401` errors apply too. Every possible error is listed on the [Errors](/guide/errors#add-files-errors) page.

## Common mistakes

::: danger Waiting for a callback
Adding files does not change the status, so no callback is sent. The files show up in `files` of [Get a Request](/api/get-request#files) and of the next callback.
:::

::: danger Mixing it up with Get a File
`POST /api/requests/:requestID/files` adds files. `GET /api/requests/:requestID/files/:fileID` gets a download link for one. Sign the path you call.
:::

::: danger More than 3 files in one call
Send at most 3. For more, make another call with a **new** `idempotencyKey`.
:::

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

* [Create a Request: Attaching files](/api/create-request#attaching-files)
* [Respond to a Return](/api/respond-to-return)
* [Get a Request](/api/get-request#files)
* [Get a File](/api/get-file)
* [Idempotency](/guide/idempotency)
