Perception
Uploads
Uploads put a file on the server, to use as a workspace_file source for the tools. They're free and work at any balance. The bytes go straight to the document service with a one-use token, never with your API key, your defaultHeaders or the SDK's X-Drex-* headers: they carry only X-Upload-Token, plus an Idempotency-Key on a one-request upload and on completing a session.
Upload a file
client.documents.upload(content, params?, options?) returns Promise<UploadedFile>.
| Parameter | Type | Required | Description |
|---|---|---|---|
content | FileContent | Yes | The bytes: a Blob (a File included), a Uint8Array or an ArrayBuffer. |
params.file_name | string | Yes, unless content is a File | The file's name, and the default path. Default: the File's own name. |
params.path | string | No | Where the file goes, relative. Keep the file's extension: the document service types an upload from its first bytes, and from this extension when those don't settle it. Default: file_name. |
params.on_conflict | OnConflict | No | "reject" (default) refuses a different file at path; "new_version" replaces it with a new version. |
params is an UploadParams. A file under 32 MiB goes up in one request through an upload grant. From 32 MiB (UPLOAD_SESSION_THRESHOLD_BYTES) it goes up in parts through an upload session, each part read only as it is sent. options apply in full to the call to Drex; the calls to the document service take its signal, timeout and retry, not headers.
import { openAsBlob } from "node:fs";
import { workspaceFile } from "nace-sdk";
// A Blob backed by the file on disk: the SDK reads it part by part, not all at once.
const blob = await openAsBlob("invoices/invoice.pdf");
const file = await client.documents.upload(blob, {
file_name: "invoice.pdf",
path: "invoices/invoice.pdf",
on_conflict: "new_version",
});
const job = await client.documents.extract({
source: workspaceFile(file),
schema: { type: "object", properties: { total: { type: "number" } } },
});Uploading the same bytes to the same path again returns the file that is there. A different file at a taken path, with on_conflict: "reject", fails with code path_conflict: a ConflictError (409) for a file under 32 MiB, and a NaceError naming the failed upload job and path_conflict for a larger one. upload without a file_name for a non-File throws a NaceError. A file sent in parts that is still assembling after about ten minutes throws a NaceError; the document service keeps assembling it, so upload the same file to the same path again later to get it.
Upload grants
client.documents.createUploadGrant(params?, options?) mints a one-use token for one upload, so another client, such as a browser, can upload without your API key. It returns APIPromise<UploadGrant>. params is an UploadGrantParams:
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | No | Pin the upload to this path. |
max_bytes | number | No | Refuse a larger file. Default: the document service's limit for the file type. |
total_size_bytes | number | No | The exact size the file must have. |
ttl_seconds | number | No | How long the token works, 1 to 3600. Default: 3600. |
UploadGrant field | Type | Description |
|---|---|---|
workspace_id | string | The workspace id the upload goes to. |
upload_url | string | The document service's upload URL, not Drex's. |
token | string | Sent as X-Upload-Token. Good for one upload. |
expires_at | string | When the token stops working. |
max_bytes | number | null | The cap you set, or null. |
Mint the grant on your server, then let the browser send the file to the grant's upload_url itself:
const grant = await client.documents.createUploadGrant({
path: "invoices/invoice.pdf",
total_size_bytes: 48_213,
ttl_seconds: 600,
});
// Hand grant.upload_url and grant.token to the browser. The token works for one upload.declare const grant: { upload_url: string; token: string };
declare const file: File; // from an <input type="file">
const form = new FormData();
form.append("file", file);
form.append("metadata", JSON.stringify({ path: "invoices/invoice.pdf", total_size_bytes: file.size }));
const response = await fetch(grant.upload_url, {
method: "POST",
body: form,
headers: { "X-Upload-Token": grant.token },
credentials: "omit",
});
const upload = await response.json(); // upload.result.file has workspace_id and file_idSend only the X-Upload-Token header, with credentials: "omit", so the request works from any origin. The metadata part also takes on_conflict. Send the file's workspace_id and file_id back to your server and use them as a workspace_file source: the /v1 routes send no Access-Control-Allow-Origin header, so keep every Drex call on your server. See Uploads.
Upload sessions
An upload session takes a large file in parts and can resume after a drop. upload runs one for you from 32 MiB; run one yourself to resume, or to send parts from somewhere else.
client.documents.createUploadSession(params, options?) returns APIPromise<UploadSession>. params is an UploadSessionParams:
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Where the file goes. |
total_size_bytes | number | Yes | The file's exact size across every part, at least 1. |
ttl_seconds | number | No | How long the session takes parts, 1 to 5400. Default: 5400. |
on_conflict | OnConflict | No | "reject" (default) fails the assembly with code path_conflict when a different file has the path; "new_version" replaces it. |
idempotency_key | string | No | Sent as Idempotency-Key: the same key returns the same session. Minted per call when omitted, so a retry never opens a second session. |
UploadSession has workspace_id, session_id, session_token, chunk_size (every part's size, except a shorter last part), total_parts, expires_at and upload_session_url, the document service's URL for the session.
| Method | Returns | Description |
|---|---|---|
documents.uploadPart(session, number, data, options?) | Promise<void> | Sends part number, from 0: chunk_size bytes of data (a FileContent), the last part shorter. Sending a part again replaces it. |
documents.completeUploadSession(session, params?, options?) | Promise<UploadedFile> | Assembles the parts into one file. Waits up to 10 minutes under one params.idempotency_key (minted when omitted), then throws a NaceError naming the key: call again with it to keep waiting. A failed assembly, such as path_conflict, throws a NaceError with the job's error code. |
documents.getUploadSession(session, options?) | APIPromise<UploadSessionStatus> | Which parts have landed, to resume after a drop. |
documents.abortUploadSession(session, options?) | Promise<void> | Drops the session and its parts. Does nothing to a finished session. A session still assembling (completing) is refused with ConflictError, code session_busy: wait for completeUploadSession instead. |
These four send X-Upload-Token: session.session_token to session.upload_session_url, never your API key, so they need only those two fields: save them to resume from another process. Their options take signal, timeout and retry, not headers.
A session refuses calls that don't fit its state. None of these is retried:
session_busy(ConflictError): sending a part or aborting while the session iscompleting, or completing it with a different key while a completion runs. CallcompleteUploadSessionagain with the same key to resume that completion.upload_expired(ConflictError): sending a part to a session that completed or was aborted, or completing an aborted one. Completing a completed session again returns its file.- After
expires_atthe session token stops working, and every call on the session throwsAuthenticationError(401). Open a new session.
UploadSessionStatus field | Type | Description |
|---|---|---|
session_id | string | The session. |
status | "open" | "completing" | "completed" | "aborted" | Where it stands, or another string a newer service sends. |
chunk_size, total_parts | number | As on UploadSession. |
parts | Array<{ part_number, size_bytes }> | Parts landed so far, in no order. Send only the missing ones. |
received_bytes | number | The sum of parts[].size_bytes. |
declared_size_bytes | number | total_size_bytes from the create. |
expires_at | string | When the session stops taking parts. |
import { openAsBlob } from "node:fs";
const blob = await openAsBlob("media/interview.mp4");
const session = await client.documents.createUploadSession({
path: "media/interview.mp4",
total_size_bytes: blob.size,
});
// After a drop, ask which parts landed and send only the rest.
const status = await client.documents.getUploadSession(session);
const landed = new Set(status.parts.map((part) => part.part_number));
for (let number = 0; number < session.total_parts; number++) {
if (landed.has(number)) continue;
const start = number * session.chunk_size;
await client.documents.uploadPart(session, number, blob.slice(start, start + session.chunk_size));
}
const file = await client.documents.completeUploadSession(session);UploadedFile has the file's workspace_id, file_id and path, and the document service's other file fields, such as file_name, size_bytes and version.
Delete a file
client.documents.deleteFile(workspaceId, fileId, options?) deletes a file you uploaded: every version of it and everything derived from it. It can't be undone. It's free and works at any balance. It returns APIPromise<FileDeletion>.
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId | string | Yes | The file's workspace_id, from UploadedFile. Only your account's workspace. |
fileId | string | Yes | The file's file_id, from UploadedFile. |
The call waits while the deletion runs. Another account's workspace throws NotFoundError with code workspace_not_found, and a file that isn't there file_not_found.
FileDeletion field | Type | Description |
|---|---|---|
object | "file_deletion" | Always file_deletion. |
workspace_id, file_id | string | The file deleted. |
status | "deleted" | "deleting" | deleted once every trace is confirmed gone; deleting if it is still running. Calling again then reports on the same deletion. |
const file = await client.documents.upload(await readFile("invoice.pdf"), { file_name: "invoice.pdf" });
const deleted = await client.documents.deleteFile(file.workspace_id, file.file_id);
console.log(deleted.status); // "deleted"