Work with Report Files

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

EndpointReport identifierFile identifier
POST /external/filesKalepa report ID or your external_id (report_id form field)—
POST /external/files/upload_urlKalepa report ID or your external_id (report_id body field)—
GET /external/reports/{report_id}/filesKalepa 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 own external_id here.
  • {uploaded_file_id} identifies the file. Put the id Kalepa returned when you uploaded it. Kalepa mints this value, so you never choose it, and a non-UUID value here is a 400. It is not the same field as the id that GET /external/reports/{report_id}/files returns — see The id in 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

Put 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

curl "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

The 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

StatusWhenWhat to do
400id_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_IDENTIFIERYour external_id matches more than one report.Add org_group / sub_org_group, or use the Kalepa report ID.
401The token is missing, expired, or invalid.Request a new token.
403Your API client is not permitted to act on this report.Check the client's scope with Kalepa.
404No 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.
422PUT 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 id from the upload response together with your own file reference. Use it for PUT and DELETE.
  • Keep each external_id unique inside your organization, so a lookup by external_id is never ambiguous.