Documents are uploaded directly to S3 using a multipart upload. Layer issues presigned URLs for each part, you PUT the file bytes straight to S3, and then tell Layer to assemble the parts. This keeps large files off Layer’s servers and supports files that exceed standard single-request limits.The flow uses three endpoints:
Create — register the document and get presigned URLs for each part.
Complete — finalize the upload once every part is uploaded.
Abort — cancel the upload and discard any uploaded parts.
1
Create the upload
POST /v1/businesses/{businessId}/documents with the document metadata. The response contains the document_id, the S3 upload_id, the part_size_bytes each part must be, and a parts array of presigned URLs (one per part).
document_type is one of RECEIPT, UNSTRUCTURED_BOOKKEEPING_CONTEXT, or OTHER.
2
Upload each part to S3
Split the file into chunks of exactly part_size_bytes (the final part may be smaller) and PUT each chunk to its presigned url. These requests go directly to S3, not to the Layer API, and need no Authorization header.Capture the ETag response header from each PUT — you’ll need it to complete the upload.
Keep each part the full part_size_bytes (except the last), and pair every ETag with the correct part_number. A mismatched size or a missing/misnumbered ETag will cause the complete step to fail.
Parts are independent and can be uploaded in any order and in parallel. The examples below loop sequentially for clarity, but in production upload several parts concurrently (a worker pool of ~4–8 is a good default) to cut total upload time significantly — just make sure each ETag stays matched to its part_number, and sort the parts list by part_number before calling complete.
3
Complete the upload
POST /v1/businesses/{businessId}/documents/{documentId}/complete with the upload_id and the collected { part_number, etag } pairs. S3 stitches the parts into the final object and Layer returns the stored document.
The following uploads a file through all three steps. access_token is a Bearer token obtained via scoped authentication.
BASE="https://api.layerfi.com"BUSINESS_ID="..."TOKEN="$ACCESS_TOKEN"FILE="LargeZip.zip"# 1. Create — get presigned part URLsCREATE=$(curl -s -X POST "$BASE/v1/businesses/$BUSINESS_ID/documents" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d "{\"document_type\":\"OTHER\",\"file_name\":\"$FILE\",\"file_type\":\"application/zip\",\"file_size_bytes\":$(wc -c < "$FILE")}")DOCUMENT_ID=$(echo "$CREATE" | jq -r .document_id)UPLOAD_ID=$(echo "$CREATE" | jq -r .upload_id)PART_SIZE=$(echo "$CREATE" | jq -r .part_size_bytes)# 2. Split the file into parts and PUT each one to S3, capturing ETags.# Use a fresh temp dir so stale chunks from a previous run can't leak in.PARTS_DIR=$(mktemp -d)trap 'rm -rf "$PARTS_DIR"' EXITsplit -b "$PART_SIZE" "$FILE" "$PARTS_DIR/part_"PARTS="[]"; i=0for chunk in "$PARTS_DIR"/part_*; do i=$((i + 1)) URL=$(echo "$CREATE" | jq -r ".parts[$((i - 1))].url") ETAG=$(curl -s -X PUT --data-binary "@$chunk" -D - -o /dev/null "$URL" \ | tr -d '\r' | awk -F'"' '/^[Ee][Tt][Aa][Gg]:/ {print $2}') PARTS=$(echo "$PARTS" | jq ". + [{\"part_number\":$i,\"etag\":\"$ETAG\"}]")done# 3. Complete the uploadcurl -s -X POST "$BASE/v1/businesses/$BUSINESS_ID/documents/$DOCUMENT_ID/complete" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d "{\"upload_id\":\"$UPLOAD_ID\",\"parts\":$PARTS}"
Presigned URLs are time-limited. Upload the parts and call complete promptly after creating the upload; if a URL expires, start over with a new create request (and optionally abort the old upload_id).