post /<uuid>/file
Post element file
Uploads a file to the referenced element. Fails with 409 Conflict if the element already has a file attached - use PUT /<uuid>/file to replace an existing file instead.
The endpoint supports two upload modes, selected through the Upload-Complete request header:
- Direct upload -
Upload-Complete: ?1, or the header omitted entirely. The request body is the complete file. On success, the endpoint responds with201 Createdand no body. - Resumable upload -
Upload-Complete: ?0. Starts a resumable upload instead of attaching a file right away; any data sent in this first request is stored as the upload’s first chunk. On success, the endpoint responds with204 No Content, and aLocationheader pointing at the new resumable upload’s URL under/upload/<uuid>. Continue it withPATCH /upload/<uuid>, inspect it withHEAD /upload/<uuid>, or abort it withDELETE /upload/<uuid>.
Both modes use the same request headers:
Content-Disposition: attachment; filename="<name>"(optional) - determines the file’s extension, which is stored and later used to reconstruct theContent-Dispositionheader onGET /<uuid>/file. Falls back tobinif omitted, or if no extension can be parsed from the filename.Upload-Length(optional, resumable uploads only) - total size in bytes of the finished file, once all chunks have been appended.
Resumable uploads follow the IETF draft, not tus.io
The header names (Upload-Offset, Upload-Length, Upload-Complete, Upload-Limit) come from the IETF “resumable uploads for HTTP” draft, not the older tus.io protocol - there is no Tus-Resumable header, and Upload-Complete uses the structured boolean values ?0/?1 rather than a plain string.
Whenever a resumable upload is created (or continued via PATCH /upload/<uuid>), the response includes an Upload-Limit header describing the current server limits: how long an unfinished upload is kept alive, the maximum total file size, and the minimum and maximum size of an individual chunk. A chunk smaller than the minimum is rejected with 400 Bad Request, unless it is the upload’s last chunk (Upload-Complete: ?1) or has zero length. A direct upload’s 201 Created response carries none of these headers, since there is no ongoing upload to describe.
Since a zero-length chunk is always accepted, Upload-Complete: ?0 with an empty body can be used purely to create the resumable upload and read back its Upload-Limit, before deciding on a chunking strategy.
Verifying upload integrity
A direct upload (Upload-Complete: ?1, or the header omitted) can optionally carry a Repr-Digest header with the SHA-256 digest of the file, in RFC 9530 structured-field format (sha-256=:[base64]:). Once the upload finishes, the server recomputes the digest and compares it; a mismatch fails with 400 Bad Request and the file is not stored. Content-Digest is accepted as a fallback header name.
This only verifies a direct upload’s complete body. For a resumable upload, send the digest on the completing PATCH /upload/<uuid> request instead, once the full file has been reassembled server-side.
Request
Create file directly
curl \ -H 'Authorization: Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc' \ -H 'Content-Type: application/octet-stream' \ -H 'Content-Disposition: attachment; filename="report.pdf"' \ --data-binary @report.pdf \ https://api.example.com/7b80b203-2b82-40f5-accd-c7089fe6114e/fileimport requests
url = "https://api.example.com/7b80b203-2b82-40f5-accd-c7089fe6114e/file"
payload = "@report.pdf"headers = { "Authorization": "Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc", "Content-Type": "application/octet-stream", "Content-Disposition": "attachment; filename=\"report.pdf\""}
response = requests.post(url, data=payload, headers=headers)
print(response.json())const url = 'https://api.example.com/7b80b203-2b82-40f5-accd-c7089fe6114e/file';const options = { method: 'POST', headers: { Authorization: 'Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="report.pdf"' }, body: '@report.pdf'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}<?php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.example.com/7b80b203-2b82-40f5-accd-c7089fe6114e/file', [ 'body' => '@report.pdf', 'headers' => [ 'Authorization' => 'Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc', 'Content-Disposition' => 'attachment; filename="report.pdf"', 'Content-Type' => 'application/octet-stream', ],]);
echo $response->getBody();Create resumable upload
curl \ -H 'Authorization: Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc' \ -H 'Content-Type: application/octet-stream' \ -H 'Content-Disposition: attachment; filename="report.pdf"' \ -H 'Upload-Complete: ?0' \ -H 'Upload-Length: 20971520' \ --data-binary @report.pdf.part1 \ https://api.example.com/7b80b203-2b82-40f5-accd-c7089fe6114e/fileimport requests
url = "https://api.example.com/7b80b203-2b82-40f5-accd-c7089fe6114e/file"
payload = "@report.pdf.part1"headers = { "Authorization": "Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc", "Content-Type": "application/octet-stream", "Content-Disposition": "attachment; filename=\"report.pdf\"", "Upload-Complete": "?0", "Upload-Length": "20971520"}
response = requests.post(url, data=payload, headers=headers)
print(response.json())const url = 'https://api.example.com/7b80b203-2b82-40f5-accd-c7089fe6114e/file';const options = { method: 'POST', headers: { Authorization: 'Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="report.pdf"', 'Upload-Complete': '?0', 'Upload-Length': '20971520' }, body: '@report.pdf.part1'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}<?php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.example.com/7b80b203-2b82-40f5-accd-c7089fe6114e/file', [ 'body' => '@report.pdf.part1', 'headers' => [ 'Authorization' => 'Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc', 'Content-Disposition' => 'attachment; filename="report.pdf"', 'Content-Type' => 'application/octet-stream', 'Upload-Complete' => '?0', 'Upload-Length' => '20971520', ],]);
echo $response->getBody();Response
201 · Created
The complete file was uploaded directly (Upload-Complete: ?1, or the header was omitted) and is now attached to the element. Returned without a response body.
204 · No Content
A resumable upload (Upload-Complete: ?0) was created successfully. Its URL is returned in the Location header - continue it with PATCH, inspect it with HEAD, or abort it with DELETE against that URL.
400 · Bad Request
A provided property has the wrong type or fails a validation rule.
409 · Conflict
The element already has a file attached. Delete the existing file first, or replace it with PUT.
response has no body
Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateX-Powered-By: Ember Nexus APIno schema available
response has no body
Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateLocation: /455a89b9-4ec7-4b02-b023-bcfbcbba9a9fUpload-Complete: ?0Upload-Offset: 5242880Upload-Length: 20971520Upload-Limit: max-age=10800, max-size=10737418240, min-append-size=5242880, max-append-size=105906176Expires: Sat, 22 Aug 2026 21:22:07 GMTX-Powered-By: Ember Nexus APIno schema available
{ "type": "https://api.example.com/error/400/bad-content", "title": "Bad content", "status": 400, "detail": "Endpoint expects property 'newPassword' to be password which is not identical to the old password, got string '<redacted>'."}Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateX-Powered-By: Ember Nexus API{ "$schema": "https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Schema", "type": "object", "description": "RFC 7807 Problem Details object.", "properties": { "type": { "type": "string", "format": "uri" }, "title": { "type": "string" }, "status": { "type": "integer" }, "detail": { "type": "string" } }}{ "type": "https://api.example.com/error/401/unauthorized", "title": "Unauthorized", "status": 401, "detail": "Authorization for the request failed due to possible problems with the token (incorrect or expired), password (incorrect or changed), the user's unique identifier, or the user's status (e.g., missing, blocked, or deleted)."}Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateX-Powered-By: Ember Nexus API{ "$schema": "https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Schema", "type": "object", "description": "RFC 7807 Problem Details object.", "properties": { "type": { "type": "string", "format": "uri" }, "title": { "type": "string" }, "status": { "type": "integer" }, "detail": { "type": "string" } }}{ "type": "https://api.example.com/error/404/not-found", "title": "Not Found", "status": 404, "detail": "Requested element was not found."}Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateX-Powered-By: Ember Nexus API{ "$schema": "https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Schema", "type": "object", "description": "RFC 7807 Problem Details object.", "properties": { "type": { "type": "string", "format": "uri" }, "title": { "type": "string" }, "status": { "type": "integer" }, "detail": { "type": "string" } }}{ "type": "https://api.example.com/error/409/conflict", "title": "Conflict", "status": 409, "detail": "Element with id '7b80b203-2b82-40f5-accd-c7089fe6114e' already has an associated file; can not create new file. Delete existing file first or replace it with PUT."}Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateX-Powered-By: Ember Nexus API{ "$schema": "https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Schema", "type": "object", "description": "RFC 7807 Problem Details object.", "properties": { "type": { "type": "string", "format": "uri" }, "title": { "type": "string" }, "status": { "type": "integer" }, "detail": { "type": "string" } }}Request Parameters
uuid
required
Id of the element the file is attached to.
Request Header
Authorization
optional
Bearer token for user authentication. If omitted, the request is processed as an anonymous user; if provided, the associated user account is used.
Upload-Complete
optional
?1 uploads the request body as the complete, final file. ?0 starts a resumable upload instead - the request body, if any, is stored as the first chunk. Omitting the header behaves like ?1.
Upload-Length
optional
Total size in bytes of the finished file. Only read when starting a resumable upload (Upload-Complete: ?0); optional even then.
Content-Disposition
optional
Original filename of the uploaded file, used to derive the file's extension. Falls back to bin if omitted or if no extension can be parsed.
Repr-Digest
optional
Cryptographic digest of the complete file, checked against the uploaded content once the direct upload finishes. Only sha-256 is supported. A mismatch fails with 400 Bad Request and the file is not stored. Content-Digest is accepted as a fallback if Repr-Digest is absent. Only meaningful for a complete (non-resumable) upload; for a resumable upload, verify the digest on the completing PATCH request instead.
Request Body
no example available
{ "type": "string", "format": "binary"}Response Header
Upload-Complete
always
Structured boolean indicating whether the upload is finished: ?1 for complete, ?0 for still in progress.
Upload-Offset
always
Number of bytes already stored for the resumable upload. On a request, it must match the upload's current offset exactly; on a response, it reflects the offset after the request was processed.
Upload-Length
optional
Total number of bytes the finished file will have. Optional - omitted when the total size is not known ahead of time.
Upload-Limit
always
Structured field listing the server's limits for this upload: max-age (seconds an unfinished upload is kept alive since its first request), max-size (maximum total file size in bytes), min-append-size (minimum size in bytes for a non-final chunk) and max-append-size (maximum size in bytes per chunk).
Expires
always
Point in time after which the unfinished resumable upload is discarded, if it is not completed before then.
Access-Control-Allow-Headers
always
Specifies which HTTP headers can be used during a cross-origin request, returned in preflight responses.
Access-Control-Allow-Methods
always
Indicates which HTTP methods are permitted for cross-origin requests, typically returned in preflight responses.
Access-Control-Allow-Origin
always
Specifies which origin(s) may access the resource.
Access-Control-Expose-Headers
always
Lists the response headers that browsers are allowed to access from JavaScript beyond the default restricted set.
Allow
always
Lists the HTTP methods supported by the endpoint; returned alongside a 405 Method Not Allowed response.
Cache-Control
always
Directives controlling caching behavior for the response.