This guide shows how to upload files to a report, list them, update their metadata, and delete them. All examples use the production base URL https://api.kalepa.com and a token in $TOKEN. See Getting started for the token and Work with reports for the report identifier rules.
The values ACME-2026-000123, COMMERCIAL, and EAST are examples. Replace them with your own submission number, org group, and sub-org group.
Which identifier goes where
| Endpoint | Report identifier | File identifier |
|---|---|---|
POST /external/files | Kalepa report ID or your external_id (report_id form field) | — |
POST /external/files/upload_url | Kalepa report ID or your external_id (report_id body field) | — |
GET /external/reports/{report_id}/files | Kalepa report ID or your external_id | — |
PUT /external/reports/{report_id}/files/{uploaded_file_id} | Kalepa report ID or your external_id | {uploaded_file_id} — the id from the upload response |
DELETE /external/reports/{report_id}/files/{uploaded_file_id} | Kalepa report ID or your external_id | {uploaded_file_id} — the id from the upload response |
PUT and DELETE carry two identifiers in one URL. They are not interchangeable:
{report_id}identifies the report. Put the Kalepa report ID or your ownexternal_idhere.{uploaded_file_id}identifies the file. Put theidKalepa returned when you uploaded it. Kalepa mints this value, so you never choose it, and a non-UUID value here is a400. It is not the same field as theidthatGET /external/reports/{report_id}/filesreturns — see Theidin the list.
So one URL holds a value you chose and a value Kalepa gave you. Only {report_id} accepts your own identifier.
id_type, org_group, and sub_org_group work on every endpoint in the table exactly as on GET /external/reports/{report_id} — as query parameters on the /external/reports/{report_id}/files… endpoints, and as body or form fields next to report_id on the two upload endpoints.
Upload a file
Send the file as multipart/form-data. The file can be up to 10 MB. For a larger file, use Upload a large file below.
By Kalepa report ID
curl -X POST "https://api.kalepa.com/external/files" \
-H "Authorization: Bearer $TOKEN" \
-F "report_id=f116fa5c-e5d9-43c2-8dc3-ac3fa60ff3b3" \
-F "name=acme-sov.xlsx" \
-F "client_file_type=SOV" \
-F "file=@/path/to/acme-sov.xlsx"
Example response (201 Created):
{
"id": "0c9d2b1e-4f3a-4c8e-9a7b-2d6e8f1a3b5c",
"name": "acme-sov.xlsx"
}
Store the id. It is the value that PUT and DELETE take in {uploaded_file_id}. client_file_type and tags are returned only where Kalepa enables them for your organization.
By your external_id
external_idPut your own identifier in the same report_id form field. Kalepa detects the shape, so nothing else changes:
curl -X POST "https://api.kalepa.com/external/files" \
-H "Authorization: Bearer $TOKEN" \
-F "report_id=ACME-2026-000123" \
-F "name=acme-sov.xlsx" \
-F "client_file_type=SOV" \
-F "file=@/path/to/acme-sov.xlsx"
Narrow the report lookup when uploading
id_type, org_group, and sub_org_group are form fields next to report_id, not query parameters:
curl -X POST "https://api.kalepa.com/external/files" \
-H "Authorization: Bearer $TOKEN" \
-F "report_id=ACME-2026-000123" \
-F "id_type=external_id" \
-F "org_group=COMMERCIAL" \
-F "sub_org_group=EAST" \
-F "name=acme-sov.xlsx" \
-F "file=@/path/to/acme-sov.xlsx"
client_file_type is free text. To send tags in this multipart request, send the array as a JSON-encoded string, for example -F 'tags=[{"source":"agency-portal"}]'. In the JSON body of PUT (below), send tags as the array itself.
Upload a large file
For a file over 10 MB, or when you do not want to send the content through the API, upload it to storage in two steps.
Step 1 — ask for an upload URL. report_id takes either identifier, with id_type, org_group, and sub_org_group as body fields:
curl -X POST "https://api.kalepa.com/external/files/upload_url" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"file_name": "acme-sov-large.xlsx",
"report_id": "ACME-2026-000123",
"org_group": "COMMERCIAL"
}'
Example response (201 Created):
{
"url": "https://kalepa-uploads.s3.amazonaws.com/",
"fields": {
"key": "post/…/acme-sov-large.xlsx",
"policy": "…",
"signature": "…"
},
"id": "7b1f4d9a-2c6e-4a58-8f3b-1d0e5c7a9b24"
}
Step 2 — send the file to that URL. Use a multipart/form-data POST, not a PUT. Send every entry of fields as a form field before the file part, unchanged, and send no Authorization header: the URL is already signed. Storage decides which fields it returns, so read them from the response instead of hard-coding the list.
curl -X POST "https://kalepa-uploads.s3.amazonaws.com/" \
-F "key=post/…/acme-sov-large.xlsx" \
-F "policy=…" \
-F "signature=…" \
-F "file=@/path/to/acme-sov-large.xlsx"
Storage answers 204 No Content on success. Kalepa then picks the file up and attaches it to the report, so it appears in GET /external/reports/{report_id}/files a short time later, not immediately. Store the id from step 1: it is the value that PUT and DELETE take in {uploaded_file_id}.
file_name and report_id are both required. Either one missing is a 400.
List the files of a report
By Kalepa report ID
curl "https://api.kalepa.com/external/reports/f116fa5c-e5d9-43c2-8dc3-ac3fa60ff3b3/files" \
-H "Authorization: Bearer $TOKEN"
By your external_id
external_idcurl "https://api.kalepa.com/external/reports/ACME-2026-000123/files" \
-H "Authorization: Bearer $TOKEN"
Example response (200 OK):
[
{
"id": "0c9d2b1e-4f3a-4c8e-9a7b-2d6e8f1a3b5c",
"name": "acme-sov.xlsx",
"created_at": "2026-03-02T10:15:30Z",
"s3_key": "…",
"presigned_url": "https://…",
"parent_file_id": null
}
]
The list contains the files a client may see. Files that Kalepa marks as internal are not listed. presigned_url is a download link that is valid for 1 hour. Request the list again when you need a fresh link.
The id in the list
id in the listThe id in the list is the Kalepa file ID. For a file that you uploaded through this API, it is the same value as the id in the upload response, so you can use it in PUT and DELETE. For a file that reached the report another way (for example by email), the two values can differ. PUT accepts both values; DELETE accepts only the id from the upload response. When a DELETE with a listed id returns 404, the file was not uploaded through the API.
Narrow the report lookup
When you list by external_id, the same options apply as on GET /external/reports/{report_id}:
curl "https://api.kalepa.com/external/reports/ACME-2026-000123/files?org_group=COMMERCIAL&sub_org_group=EAST" \
-H "Authorization: Bearer $TOKEN"
curl "https://api.kalepa.com/external/reports/3f2b9c0e-8a1d-4c6f-9e2a-7b5d1c4e8f90/files?id_type=external_id" \
-H "Authorization: Bearer $TOKEN"
Update a file's metadata
PUT /external/reports/{report_id}/files/{uploaded_file_id} updates the file's client_file_type and tags. Target the report by either identifier; {uploaded_file_id} is the id from the upload response.
curl -X PUT "https://api.kalepa.com/external/reports/ACME-2026-000123/files/0c9d2b1e-4f3a-4c8e-9a7b-2d6e8f1a3b5c" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"client_file_type": "Loss Run", "tags": [{"source": "agency-portal"}]}'
Example response (200 OK):
{
"id": "0c9d2b1e-4f3a-4c8e-9a7b-2d6e8f1a3b5c",
"name": "acme-sov.xlsx"
}
client_file_type and tags are echoed back only where Kalepa enables them for your organization. A 422 response means the submission is frozen and its files cannot be changed.
Delete a file
curl -X DELETE "https://api.kalepa.com/external/reports/ACME-2026-000123/files/0c9d2b1e-4f3a-4c8e-9a7b-2d6e8f1a3b5c" \
-H "Authorization: Bearer $TOKEN"
A successful delete returns 204 No Content. After a 204, a retry can return 204 or 404, so do not use the status of a retry to decide whether the first request succeeded.
To delete a file in one org group when the same external_id exists in several groups:
curl -X DELETE "https://api.kalepa.com/external/reports/ACME-2026-000123/files/0c9d2b1e-4f3a-4c8e-9a7b-2d6e8f1a3b5c?org_group=COMMERCIAL" \
-H "Authorization: Bearer $TOKEN"
Errors
| Status | When | What to do |
|---|---|---|
400 | id_type=uuid with a value that is not a Kalepa report ID, an unknown id_type, an invalid body, a missing file_name or report_id on POST /external/files/upload_url, or a DELETE that cannot remove the file in the report's current state (for example a frozen submission, or a file whose data is already processed). | Fix the request. For a state-dependent DELETE, retry only after the report state changes. |
400 with code AMBIGUOUS_IDENTIFIER | Your external_id matches more than one report. | Add org_group / sub_org_group, or use the Kalepa report ID. |
401 | The token is missing, expired, or invalid. | Request a new token. |
403 | Your API client is not permitted to act on this report. | Check the client's scope with Kalepa. |
404 | No report available to your API client matches the report identifier, or no file matches {uploaded_file_id} on that report. | Check both identifiers. For DELETE, use the id from the upload response. |
422 | PUT on a frozen submission. | The files of this submission cannot be changed. |
The AMBIGUOUS_IDENTIFIER body has the same shape as on the other report endpoints:
{
"detail": {
"code": "AMBIGUOUS_IDENTIFIER",
"message": "…"
}
}
Recommendations
- Store the file
idfrom the upload response together with your own file reference. Use it forPUTandDELETE. - Keep each
external_idunique inside your organization, so a lookup byexternal_idis never ambiguous.