Uploading a file is a three-step flow:
- request a signed (multipart) upload
- PUT each part of the file directly to storage
- confirm the upload
This avoids streaming large files through Fileflare’s servers.
Requires Bearer authentication with your Fileflare key. Rate-limited to 60 requests per minute.
Step 1 — Request a signed upload
POST https://app.digital-downloads.com/api/v1/assets/signed
Body (JSON)
name(string, required) — the filename customers will seesize(integer, required) — file size in bytesmime(string, required) — MIME type, e.g.application/pdf
curl -X POST https://app.digital-downloads.com/api/v1/assets/signed \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name": "my-book.pdf", "size": 10485760, "mime": "application/pdf"}'
Response — 200 OK
{
"chunk_size": 100000000,
"upload_id": "2~aBcD...",
"urls": [
{ "start": 0, "end": 10485760, "part": 1, "url": "https://<bucket>...&partNumber=1&uploadId=..." }
],
"id": "82664d96-6dfd-4343-96b0-05c46f412a5b",
"file_url": "https://app.digital-downloads.com/embedded/assets/82664d96-6dfd-4343-96b0-05c46f412a5b"
}
chunk_size— fixed part size in bytes (currently 100 MB /100000000). The file is split into parts of this size; the final part is whatever remains.upload_id— the multipart upload ID. Pass it back in Step 3.urls— one entry per part. Each entry haspart(the part number, 1-indexed; use asPartNumberin Step 3),start/end(the byte range of the file this part covers;endis exclusive), andurl(the presigned PUT URL, valid for 12 hours).id— the Fileflare asset UUID. Use this in the Step 3 path.file_url— the asset’s location in Fileflare (informational).
A small file returns a single-entry urls array (one part covering the whole file).
Step 2 — PUT each part to storage
For each entry in urls, PUT the file’s start–end byte range to that entry’s url, and read the ETag from the response headers — you’ll need it in Step 3.
# Part 1 = bytes [start, end) of the file. -D - prints response headers so you can read the ETag.
curl -X PUT "<urls[0].url>" --data-binary @part-1.bin -D -
# ...repeat for each entry in urls, slicing the file at each part's start/end.
Step 3 — Confirm the upload
curl -X POST https://app.digital-downloads.com/api/v1/assets/$ASSET_ID/uploaded \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"upload_id": "2~aBcD...", "parts": [{"PartNumber": 1, "ETag": "\"abc...\""}]}'
Body (JSON)
upload_id(string, required) — the value returned from Step 1.parts(array, required, min 1) — one entry per part you PUT in Step 2.parts.*.PartNumber(int, required) — 1-indexed; thepartvalue from the matchingurlsentry.parts.*.ETag(string, required) — theETagresponse header from that part’s PUT. May be sent with or without storage’s surrounding quote characters.
Response — 201 Created
Empty body.
Headers
Include Accept: application/json. Without it, validation errors are returned as a 302 redirect to the dashboard instead of a JSON error body (a Laravel quirk shared with the attach/detach endpoints).
Common errors
Step 1 — /assets/signed
- 402 — your storage quota is full. Free up space, upgrade, or connect your own S3.
- 422 —
name,size, ormimeis missing.
Step 3 — /uploaded
- 422 —
upload_idis missing, or aparts.*.ETag/parts.*.PartNumberis missing or the wrong type. - 500 “Server Error” —
partsis omitted or empty. Only the individualparts.*items are validated, so an entirely missingpartsarray slips past validation and fails downstream. In practice a 500 here means thepartspayload is missing or malformed.
Notes
- If the upload fails or you want to abandon it, call
POST /api/v1/assets/{asset}/cancelto free the placeholder.