Files

A file is an object you uploaded, registered in your team's Drive. Files are the input to mass imports: you upload a spreadsheet, preview its sheets to learn their shape, then start an import against it. Files are also what file and image passport property values point at.

Files are team-global — a file belongs to no ontology. Writes need files:write, reads need files:read.

The file model

  • Name
    object
    Type
    string
    Description

    Always file.

  • Name
    id
    Type
    string
    Description

    Unique identifier, e.g. file_00a1b2c3d4e5f607.

  • Name
    filename
    Type
    string
    Description

    The stored name of the uploaded file, e.g. units.xlsx. This may differ from the name you sent: characters the Drive can't hold become _, and a name already taken in the directory is de-duplicated (units.xlsxunits (2).xlsx). Read it back rather than assuming it matches. Nullable.

  • Name
    extension
    Type
    string
    Description

    File extension without the dot, e.g. xlsx. Nullable.

  • Name
    size
    Type
    integer
    Description

    Size of the file in bytes. Nullable.

  • Name
    metadata
    Type
    object
    Description

    Caller-owned metadata bag. Files are immutable once uploaded, so it is set at upload time or not at all.

  • Name
    created_at
    Type
    string
    Description

    ISO 8601 timestamp of the upload. Nullable.

The file preview model

  • Name
    object
    Type
    string
    Description

    Always file.preview.

  • Name
    file
    Type
    string
    Description

    The id of the previewed file, e.g. file_00a1b2c3d4e5f607.

  • Name
    sheets
    Type
    array
    Description

    One entry per worksheet in the spreadsheet (see below).

The sheet object

Each entry in sheets describes one worksheet — enough to build an import mapping and rows value without downloading the file.

  • Name
    index
    Type
    integer
    Description

    Zero-based position of the sheet in the workbook.

  • Name
    name
    Type
    string
    Description

    The sheet's tab name.

  • Name
    header
    Type
    array
    Description

    The column labels taken from the first row.

  • Name
    examples
    Type
    array
    Description

    Up to two example data rows, each an array of cell values.

  • Name
    rows
    Type
    integer
    Description

    Data row count, with the header row excluded.


POST/v1/files

Upload a file

Uploads a file as multipart/form-data in a single shot. Spreadsheets can then be previewed and imported. Requires files:write. Accepts an optional Idempotency-Key header.

The 50 MB ceiling

This is a single-shot upload, which is what puts a ceiling on it at all. The default is 50 MB — and treat that as an upper bound the deployment must also honour, not a guarantee.

A size rule can only reject a request it actually received, so a body over PHP's own upload_max_filesize / post_max_size never reaches the validator and surfaces as a confusing 400 parameter_invalid with param: "file" — "file is required" — rather than a size error. An ingress body-size limit rejects it earlier still, with a bare 413 that is not in the API's error envelope at all. The three limits are configured in different places and nothing enforces that they agree.

Anything larger goes through multipart uploads, which do not send the bytes through the API at all, so none of the three limits apply.

Required attributes

  • Name
    file
    Type
    string
    Description

    The file to upload, sent as a binary multipart/form-data part. Do not set a JSON content type.

Optional attributes

  • Name
    folder_path
    Type
    string
    Description

    The directory to upload into. The Drive root when omitted.

  • Name
    metadata
    Type
    string
    Description

    A metadata bag as a JSON-encoded string — a multipart body cannot hold a nested object, so send the form field with text like {"source_system":"erp-prod"}. It is parsed and then validated identically to the JSON form; text that is not valid JSON, or that decodes to something other than an object, is 400 metadata_invalid.

Request

POST
/v1/files
curl https://api.synexcloud.com/v1/files \
  -H "Authorization: Bearer $SYNEX_API_KEY" \
  -F "file=@units.xlsx"

Response

{
  "object": "file",
  "id": "file_00a1b2c3d4e5f607",
  "filename": "units.xlsx",
  "extension": "xlsx",
  "size": 20480,
  "created_at": "2026-07-12T09:31:00Z"
}

GET/v1/files/:file

Retrieve a file

Retrieves a file's metadata by id. Requires files:read.

Request

GET
/v1/files/file_00a1b2c3d4e5f607
curl https://api.synexcloud.com/v1/files/file_00a1b2c3d4e5f607 \
  -H "Authorization: Bearer $SYNEX_API_KEY"

Response

{
  "object": "file",
  "id": "file_00a1b2c3d4e5f607",
  "filename": "units.xlsx",
  "extension": "xlsx",
  "size": 20480,
  "created_at": "2026-07-12T09:31:00Z"
}

GET/v1/files/:file/preview

Preview a spreadsheet

Parses a previously uploaded spreadsheet and returns, per sheet, the header labels, up to two example rows, and the data row count — everything you need to build an import mapping and rows value. Requires files:read.

Sheets come back in workbook order, which is the order a multi-level import consumes them: levels[0] reads the first sheet, levels[1] the second, and so on.

A file that exists but is not a spreadsheet this endpoint can parse returns 404 file_not_previewable — distinct from a missing id, which is 404 resource_missing.

Request

GET
/v1/files/file_00a1b2c3d4e5f607/preview
curl https://api.synexcloud.com/v1/files/file_00a1b2c3d4e5f607/preview \
  -H "Authorization: Bearer $SYNEX_API_KEY"

Response

{
  "object": "file.preview",
  "file": "file_00a1b2c3d4e5f607",
  "sheets": [
    {
      "index": 0,
      "name": "Units",
      "header": ["Capacity", "Alias"],
      "examples": [
        ["4500", "unit-01"],
        ["4800", "unit-02"]
      ],
      "rows": 120
    }
  ]
}

GET/v1/files

List a directory

Lists one page of a directory's direct children, files and folders interleaved. Requires files:read.

Switch on object to tell the two apart: a folder carries a path and no id, a file carries an id and no path.

Paths are normalized, so /exports/2026, exports/2026 and /exports/2026/ all name the same directory. . and .. segments, empty segments, and the | character are 400 parameter_invalid.

path joins the cursor's scope, so a cursor minted in one directory and replayed against another is 400 invalid_cursor.

There is no ?search= on files. The drive search underneath is unbounded, so exposing it would put an unpredictable scan behind a public endpoint. Until that is fixed, list a directory and filter client-side, or keep your own index of the ids you uploaded — the id is the stable handle, not the name.

Optional attributes

  • Name
    path
    Type
    string
    Description

    The directory to list. The Drive root when omitted.

  • Name
    limit
    Type
    integer
    Description

    Page size, 1–100 (default 10).

  • Name
    cursor
    Type
    string
    Description

    A pagination cursor from a previous response's next_cursor.

Request

GET
/v1/files
curl -G https://api.synexcloud.com/v1/files \
  -H "Authorization: Bearer $SYNEX_API_KEY" \
  -d "path=/exports/2026/"

Response

{
  "object": "list",
  "data": [
    {
      "object": "folder",
      "name": "q3",
      "path": "/exports/2026/q3/"
    },
    {
      "object": "file",
      "id": "file_00a1b2c3d4e5f607",
      "filename": "units.xlsx",
      "extension": "xlsx",
      "size": 20480,
      "metadata": {},
      "created_at": "2026-07-10T12:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "url": "/v1/files"
}

GET/v1/files/:file/download

Returns a link, not the bytes. Requires files:read.

Objects run to tens of megabytes, and proxying one through the API would hold a worker for the whole transfer, pay egress twice, and put a request timeout between you and a download that is otherwise object storage's problem. Fetch url directly; it needs no Authorization header.

That is also the catch: the URL carries its own authorization, so it is a bearer credential for that one object until it expires. Don't log it, don't persist it in a query string, and don't hand it to a browser you don't trust.

The lifetime is 15 minutes by default — deliberately much shorter than the links the web app mints for a logged-in session, so a URL leaked through a log or a referrer is not a standing grant. expires_at is absolute, so cache the URL for its lifetime and re-request rather than calling per download.

Request

GET
/v1/files/file_00a1b2c3d4e5f607/download
curl https://api.synexcloud.com/v1/files/file_00a1b2c3d4e5f607/download \
  -H "Authorization: Bearer $SYNEX_API_KEY"

Response

{
  "object": "file.download",
  "file": "file_00a1b2c3d4e5f607",
  "url": "https://s3.eu-west-1.amazonaws.com/…?X-Amz-Signature=…",
  "expires_at": "2026-07-26T14:25:43Z"
}

Multipart uploads

POST /v1/files is single-shot, and therefore capped by what a request body can carry — 50 MB by default. POST /v1/uploads is the same object-storage multipart pipeline the internal uploader runs, with one deliberate difference: the parts are presigned, so you PUT each one straight to object storage and the bytes never touch an API worker.

That is what lifts the ceiling — a 4 GiB upload costs the API four small JSON calls, not 4 GiB of proxying — and it is what makes an upload resumable, since a failed part is one re-signed PUT rather than a restarted request. The maximum size is 5 GiB.

Three calls, plus one PUT per part:

  1. Open the upload — returns part_size, part_count and the file id.
  2. Sign a batch of part numbers, at most 100 per call.
  3. PUT each part straight to object storage, keeping the ETag each returns.
  4. Complete with the full list of part numbers and ETags.

No per-part checksums

The internal uploader checks a SHA-256 per chunk and this does not, because nothing passes through PHP here and object storage will only accept a checksum it was told to expect when the upload was created. Integrity is what the store itself enforces: each part's ETag is returned by the PUT, you send the list back, and completion fails the whole upload if any ETag does not match the part the store holds.

For you as the caller that reduces to one rule: send back exactly the ETags you were given, and a corrupted part fails the whole completion rather than landing silently inside the finished file.


POST/v1/uploads

Open a multipart upload

Opens the upload and returns the part plan. Requires files:write.

part_size is 8 MiB, part_count is ceil(size / part_size), and part numbers run 1…part_count. Both come back on this call rather than being yours to choose — two different answers for one upload would let you sign a part the completion then refuses.

filename is rejected here, not sanitized — a deliberate divergence from POST /v1/files, which substitutes characters the Drive key layout cannot hold. There the name comes off a multipart part header the client may not control; here it is a field you typed, and silently storing an upload under a different name than the one you will complete against is worse than a 400. De-duplication still applies (export.csvexport (2).csv), so read the returned filename back.

Required attributes

  • Name
    filename
    Type
    string
    Description

    The name to store the object under.

  • Name
    size
    Type
    integer
    Description

    Total size in bytes, at most 5 GiB.

Optional attributes

  • Name
    folder_path
    Type
    string
    Description

    The directory to upload into. The Drive root when omitted.

  • Name
    metadata
    Type
    object
    Description

    Caller-owned metadata bag.

Request

POST
/v1/uploads
curl https://api.synexcloud.com/v1/uploads \
  -H "Authorization: Bearer $SYNEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "cells_q3.csv",
    "size": 734003200,
    "folder_path": "/exports/2026/"
  }'

Response

{
  "object": "upload",
  "id": "file_5e2a9c7d1b4f0836",
  "filename": "cells_q3.csv",
  "size": 734003200,
  "part_size": 8388608,
  "part_count": 88,
  "created_at": "2026-07-28T10:14:02Z"
}

POST/v1/uploads/:upload/part_urls

Sign part URLs

Presigns a batch of part numbers. Requires files:write.

Part URLs come in batches of at most 100, and each one lives 15 minutes. That is why they are requested a batch at a time instead of all at once on the create call: an upload with 600 parts would see the first batch expire long before you reached the last.

Re-signing costs nothing — the call computes signatures locally and never talks to object storage — so request the next batch as you go, and simply re-request a URL that expired before you got to it.

Required attributes

  • Name
    part_numbers
    Type
    array
    Description

    The part numbers to sign, at most 100 per call, each within 1…part_count.

Request

POST
/v1/uploads/file_5e2a9c7d1b4f0836/part_urls
curl https://api.synexcloud.com/v1/uploads/file_5e2a9c7d1b4f0836/part_urls \
  -H "Authorization: Bearer $SYNEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"part_numbers": [1, 2, 3]}'

Response

{
  "object": "upload.part_urls",
  "upload": "file_5e2a9c7d1b4f0836",
  "parts": [
    {
      "part_number": 1,
      "url": "https://s3.eu-west-1.amazonaws.com/…partNumber=1&X-Amz-Signature=…"
    },
    {
      "part_number": 2,
      "url": "https://s3.eu-west-1.amazonaws.com/…partNumber=2&X-Amz-Signature=…"
    },
    {
      "part_number": 3,
      "url": "https://s3.eu-west-1.amazonaws.com/…partNumber=3&X-Amz-Signature=…"
    }
  ],
  "expires_at": "2026-07-28T10:29:02Z"
}

POST/v1/uploads/:upload/complete

Complete a multipart upload

Assembles the parts into the finished file. Requires files:write.

Send every part number with the ETag its PUT returned. Completion emits file.created on the change feed — the same event the single-shot upload emits, because a file is a file regardless of how its bytes arrived.

A second complete against the same id, or a DELETE after completing, is 409 upload_completed.

Required attributes

  • Name
    parts
    Type
    array
    Description

    Every part, as { part_number, etag }. A mismatched ETag fails the whole completion.

Request

POST
/v1/uploads/file_5e2a9c7d1b4f0836/complete
curl https://api.synexcloud.com/v1/uploads/file_5e2a9c7d1b4f0836/complete \
  -H "Authorization: Bearer $SYNEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "parts": [
      {"part_number": 1, "etag": "\"9f8c1e0a…\""},
      {"part_number": 2, "etag": "\"4b7d2f61…\""}
    ]
  }'

Response

{
  "object": "file",
  "id": "file_5e2a9c7d1b4f0836",
  "filename": "cells_q3.csv",
  "extension": "csv",
  "size": 734003200,
  "metadata": {},
  "created_at": "2026-07-28T10:31:44Z"
}

DELETE/v1/uploads/:upload

Abort a multipart upload

Drops the parts and the Drive row, and frees the filename. Requires files:write.

That last part is the point: a client that gives up on a failed export.csv and retries should get export.csv back, not export (2).csv shadowed by a dead entry it can no longer see.

The cost of that choice, stated plainly: nothing about the upload survives, so a later call against the id is 404 resource_missing rather than a distinct "aborted" state.

Request

DELETE
/v1/uploads/file_5e2a9c7d1b4f0836
curl -X DELETE https://api.synexcloud.com/v1/uploads/file_5e2a9c7d1b4f0836 \
  -H "Authorization: Bearer $SYNEX_API_KEY"

Response

{
  "object": "upload",
  "id": "file_5e2a9c7d1b4f0836",
  "deleted": true
}

POST/v1/folders

Create a folder

Creates a directory. Requires files:write.

A folder is a stored row, not an object-storage prefix. The Drive keys items by directory and name, so a directory exists because something says it does — which is why uploading into a folder does not create it, and why this endpoint exists at all.

. and .. are rejected as names: they pass the character rule but could never be addressed again, because path parameters reject them as segments — so such a folder would be created unlistable. A name already taken in that directory is 409 folder_already_exists.

Folders emit no event.

Required attributes

  • Name
    name
    Type
    string
    Description

    The folder name. . and .. are rejected.

Optional attributes

  • Name
    parent_path
    Type
    string
    Description

    The directory to create it in. The Drive root when omitted.

Request

POST
/v1/folders
curl https://api.synexcloud.com/v1/folders \
  -H "Authorization: Bearer $SYNEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "2026", "parent_path": "/exports/"}'

Response

{
  "object": "folder",
  "name": "2026",
  "path": "/exports/2026/",
  "created_at": "2026-07-28T10:40:11Z"
}

Was this page helpful?