Uploads
The multipart upload flow — idempotency via external_file_id, accepted content types, asynchronous processing states, and polling vs webhooks.
Documents enter a submission as uploads. An upload is a single multipart request that stores one
file and queues it for processing. Uploads can be attached to a specific document slot of the
submission's template (create_slot_upload, targeting a slot_id from the submission detail), or
left unassigned and sorted into a slot later.
The multipart request
Send multipart/form-data with exactly two parts you care about:
external_file_id— a text part; your identifier for this file and its idempotency key.file— the file bytes. Supply a filename and content type on the part.
Any other parts are ignored for forward compatibility.
# Unassigned upload
curl -X POST \
https://api.aircredit.de/partner/v1/submissions/$SUBMISSION_ID/files \
-H "Authorization: Bearer $AIRCREDIT_API_KEY" \
-F "external_file_id=payslip-jan" \
-F "file=@./payslip-january.pdf;type=application/pdf"
# Into a specific document slot
curl -X POST \
"https://api.aircredit.de/partner/v1/submissions/$SUBMISSION_ID/document-slots/$SLOT_ID/files" \
-H "Authorization: Bearer $AIRCREDIT_API_KEY" \
-F "external_file_id=payslip-jan" \
-F "file=@./payslip-january.pdf;type=application/pdf"import { readFile } from "node:fs/promises";
const file = new File([await readFile("./payslip-january.pdf")], "payslip-january.pdf", {
type: "application/pdf",
});
// Unassigned
await client.createUpload({ submissionId, externalFileId: "payslip-jan", file });
// Into a slot
await client.createSlotUpload({ submissionId, slotId, externalFileId: "payslip-jan", file });with open("payslip-january.pdf", "rb") as f:
part = ("payslip-january.pdf", f, "application/pdf")
# Unassigned
client.create_upload(submission_id=submission_id, external_file_id="payslip-jan", file=part)
with open("payslip-january.pdf", "rb") as f:
part = ("payslip-january.pdf", f, "application/pdf")
# Into a slot
client.create_slot_upload(
submission_id=submission_id, slot_id=slot_id, external_file_id="payslip-jan", file=part
)let bytes = tokio::fs::read("payslip-january.pdf").await?;
// Unassigned
client.create_upload()
.submission_id(&submission_id)
.external_file_id("payslip-jan")
.file(bytes.clone(), "payslip-january.pdf", "application/pdf")
.send().await?;
// Into a slot
client.create_slot_upload()
.submission_id(&submission_id)
.slot_id(&slot_id)
.external_file_id("payslip-jan")
.file(bytes, "payslip-january.pdf", "application/pdf")
.send().await?;Idempotency
Uploads are idempotent on external_file_id within a submission. If you re-send an
external_file_id that already exists on the submission, AirCredit returns the existing upload with
200 OK and does not store the new bytes. A fresh store returns 202 Accepted.
This makes retries safe. If a request times out, resend it with the same external_file_id: you
either created the upload (202) or you're getting back the one that already landed (200). Either way
there's exactly one upload.
Tip
Pick external_file_ids that are stable and meaningful in your system (e.g. payslip-jan,
id-front, bank-statement-q4). They flow through to evaluation results and webhook payloads, so
they double as your correlation key.
Accepted content types
| type | extension |
|---|---|
application/pdf | .pdf |
image/jpeg | .jpg |
image/png | .png |
application/msword | .doc |
application/vnd.openxmlformats-officedocument.wordprocessingml.document | .docx |
The default maximum file size is 100 MiB. A file that is the wrong type or too large is accepted
for asynchronous processing and then fails with a failure code (see below) rather than being
rejected at request time — so always check the eventual upload_status.
Processing states
Processing is asynchronous. An upload moves through:
processing— stored and queued; the initial state of a202 Accepted.ready— processed successfully;file_idis now set and the file feeds evaluation.failed— processing failed;failurecarries a{ code, message }.
Failure codes you may see (new ones may be added — treat unknown codes by their family):
| code | meaning |
|---|---|
unsupported_content_type | the file's real type isn't accepted |
file_too_large | over the size limit |
unreadable_file | corrupt or not machine-readable |
malware_detected | flagged by the malware scan |
storage_failed | transient storage error |
processing_failed | generic processing failure |
internal_error | unexpected server error |
Managing uploads
Beyond creating uploads, you can list them (list_uploads), fetch one (get_upload), move one
between slots (update_upload), and delete one (delete_upload):
# Poll one upload without fetching the whole submission.
curl https://api.aircredit.de/partner/v1/submissions/$SUBMISSION_ID/files/$UPLOAD_ID \
-H "Authorization: Bearer $AIRCREDIT_API_KEY"
# Assign an unassigned upload to a document slot (null unassigns).
curl -X PATCH https://api.aircredit.de/partner/v1/submissions/$SUBMISSION_ID/files/$UPLOAD_ID \
-H "Authorization: Bearer $AIRCREDIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "document_slot_id": "'$SLOT_ID'" }'
# Remove a wrong or rejected document before re-uploading.
curl -X DELETE https://api.aircredit.de/partner/v1/submissions/$SUBMISSION_ID/files/$UPLOAD_ID \
-H "Authorization: Bearer $AIRCREDIT_API_KEY"// Poll one upload without fetching the whole submission.
const upload = await client.getUpload({ submissionId, uploadId });
console.log(upload.uploadStatus);
// Assign an unassigned upload to a document slot (null unassigns).
await client.updateUpload({ submissionId, uploadId, documentSlotId: slotId });
// Remove a wrong or rejected document before re-uploading.
await client.deleteUpload({ submissionId, uploadId });# Poll one upload without fetching the whole submission.
upload = client.get_upload(submission_id=submission_id, upload_id=upload_id)
print(upload.upload_status)
# Assign an unassigned upload to a document slot (None unassigns).
client.update_upload(submission_id=submission_id, upload_id=upload_id, document_slot_id=slot_id)
# Remove a wrong or rejected document before re-uploading.
client.delete_upload(submission_id=submission_id, upload_id=upload_id)// Poll one upload without fetching the whole submission.
let upload = client.get_upload()
.submission_id(&submission_id).upload_id(&upload_id)
.send().await?.into_inner();
println!("{:?}", upload.upload_status);
// Assign an unassigned upload to a document slot (None unassigns).
client.update_upload()
.submission_id(&submission_id).upload_id(&upload_id)
.body_map(|b| b.document_slot_id(Some(slot_id.clone())))
.send().await?;
// Remove a wrong or rejected document before re-uploading.
client.delete_upload()
.submission_id(&submission_id).upload_id(&upload_id)
.send().await?;Deleting removes the stored bytes and frees the external_file_id for reuse on the submission;
reassigning and deleting recompute the affected slot's status. Both are rejected with
409 evaluation_in_progress while an evaluation is queued or running — cancel or wait first.
The re-upload loop
Document slots carry a review status (missing, partial, complete, accepted, needs_review,
rejected). When a reviewer rejects a document, the slot moves to rejected and a
document_slot.updated event fires — no polling needed. The intended fix loop is: receive the
event, delete the bad upload, upload the corrected file into the same slot, and
start a new evaluation once everything is ready.
Polling vs webhooks
You learn an upload's outcome three ways:
- Poll
get_uploadfor a single file, orget_submissionfor the whole picture. Simple, but do it sparingly — a few times a minute at most. - Webhooks — subscribe and receive
file.ready/file.failedthe moment processing finishes, anddocument_slot.updatedwhen a slot's review status changes. This is the production path; it's push-based, immediate, and doesn't waste calls. See Webhooks.
For evaluation outcomes (as opposed to individual files), the evaluation.* events carry the result
summary; see Evaluations for the full result.
Templates & customization
Templates bundle document requirements, checks, and data fields. Discover the builtin catalog, duplicate and customize templates, and author your own checks and data fields.
Evaluations
Start an agentic evaluation, follow its lifecycle and progress, and read the typed result — summary, per-check verdicts, extracted fields, confidence, and citations.