Working with files

Retrieve the compliance documents premote generates for your trips, and upload your own — including the second call that attaches a file to a traveler's profile field.

How files work

Three things to know before you integrate:

  1. Generated documents appear asynchronously. Submitting a trip kicks off workflows that file requests with the relevant authorities and store the resulting documents against the trip. A certificate is only retrievable once its workflow has completed successfully — until then, the file simply isn't there yet (see Handling "not ready yet" below). Poll for it; there is currently no file webhook.
  2. Download links are short-lived. Every download URL the API hands back is a signed URL valid for 5 minutes. Request a fresh link each time you need the file — never store the URL itself.
  3. Files carry a type. Each file has a fileType telling you what it is. Some are written by premote's own automation and some you set when uploading:

You set these when uploading

fileTypeWhat it is
PASSPORT, ID_CARD, PERMANENT_RESIDENCETraveler identity documents
USER_UPLOADAny other file a user attaches
WORKFLOW_ATTACHEDA document supporting a compliance filing (work permit, assignment letter)
INSURANCEInsurance document
MISCAnything else

Written by premote — don't set these by hand

fileTypeWhat it is
SSN_REQUESTSocial security request submitted to an authority (e.g. an A1 application)
SSN_REPLYThe authority's reply — e.g. the issued A1 certificate
NOTIFICATION_REQUESTPosted-worker (PWD) notification submitted to an authority
NOTIFICATION_REPLYConfirmation returned for a posted-worker notification
SOCIAL_SECURITY_STATEMENTSocial security statement generated for a trip
RISK_ASSESSMENTRisk assessment report
TRIP_EXPORTGenerated trip data export (see step 5)
POLICY, WORKATION_POLICYCompany policy documents
LOGOCompany branding asset
ERRORDiagnostic artifact captured when a filing fails

fileType is optional. It classifies the document and drives retention and visibility rules — passport uploads in particular are gated on a company-level setting and carry consent-driven retention.


1. Download all files for selected trips

POST /trips/download/files — API Reference

Returns the generated compliance documents for the trips you select, bundled as a ZIP. The response is the binary ZIP stream itself — not JSON — so write it straight to disk:

curl -X POST "https://api.premote.io/v1/trips/download/files?type=BUSINESS_TRIP" \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{}' \
  --output trip-files.zip
import { writeFile } from 'node:fs/promises'

const res = await fetch(
  'https://api.premote.io/v1/trips/download/files?type=BUSINESS_TRIP',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer <token>',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({}),
  },
)
if (!res.ok) throw new Error(`Download failed: ${res.status}`)

// The body is binary — read it as bytes, never as text
await writeFile('trip-files.zip', Buffer.from(await res.arrayBuffer()))

If your HTTP client defaults to text decoding (axios, got), request the response as a buffer or stream instead (e.g. axios' responseType: 'arraybuffer') — decoding the ZIP as text corrupts the archive irrecoverably.

  • selectedTrips (body, optional) — narrow the download to specific trips, e.g. {"selectedTrips": [<tripId>, <tripId>]}. Omit the field to include every trip of the selected type. Note: an empty array ("selectedTrips": []) selects no trips and returns 404 — leave the field out entirely instead.
  • filters (query, optional) — same shape as the trips list filters (e.g. startDate / endDate) to select by date range.
  • type (query) — the trip type to include. Ignored when fetchLive=true (see step 2).
🚧

The ZIP contains generated documents only

This endpoint bundles documents produced by premote's workflows (certificates, confirmations, authority replies). Files uploaded by users are not included — retrieve those individually (step 3).

Handling "not ready yet"

If none of the selected trips have a successfully generated document yet, the endpoint returns 404 "No files for this trip". That's the expected state right after trip submission, while workflows are still running — treat it as "try again later", not as an error in your integration.


2. Get the latest files for ongoing trips with fetchLive

Pass fetchLive=true to skip trip selection by type and instead target trips that are currently underway — approved trips whose travel dates contain today:

curl -X POST "https://api.premote.io/v1/trips/download/files?fetchLive=true" \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{}' \
  --output live-trip-files.zip

Use this when you want the current compliance documents for everyone traveling right now — e.g. a daily sync into your HR system.

📘

What fetchLive does — and doesn't — do

fetchLive selects which trips are included: approved trips with start date ≤ today ≤ end date. It does not trigger a new retrieval from the authorities — documents are generated asynchronously by premote's workflows, and this endpoint always serves the latest stored version. When fetchLive=true, the type parameter is ignored.


3. Get a download link for an individual file

GET /files/{key} — API Reference

Trip responses (e.g. GET /trips/{id}) include the trip's files with their metadata (id, fileName, fileType, and a path). The path is a link of the form https://api.premote.io/v1/files/{key} — resolve it to get the actual download:

# Default: 302-redirects to the signed download URL — -L follows it
curl -L "https://api.premote.io/v1/files/{key}" \
  -H 'Authorization: Bearer <token>' \
  --output a1-certificate.pdf

# Or fetch the signed URL itself as plain text
curl "https://api.premote.io/v1/files/{key}?redirect=false" \
  -H 'Authorization: Bearer <token>'

The returned URL is valid for 5 minutes; request a fresh one per download.


4. Generate documents on demand

Three document types are generated at request time and returned as a signed download link:

DocumentEndpoint
Social security statementGET /trips/{tripId}/download-statement — API Reference
Risk assessment reportGET /risk-rules/{tripId}/download-assessment — API Reference
Health insurance documentGET /trips/{tripId}/download-health-insurance — API Reference
curl "https://api.premote.io/v1/trips/{tripId}/download-statement" \
  -H 'Authorization: Bearer <token>'
📘

Health insurance documents are only available for international trips — the endpoint returns 400 for domestic trips.


5. Export trip data

POST /trips/download/data — API Reference

For spreadsheet-style exports of trip data (rather than documents). It accepts the same selectedTrips / type / fetchLive / filters parameters as step 1, queues the export, and returns a jobId.

Then poll GET /trips/exports — API Reference — to pick up the finished file:

curl "https://api.premote.io/v1/trips/exports?jobId=<jobId>" \
  -H 'Authorization: Bearer <token>'

Each export entry includes a signed url (valid 5 minutes). Exports remain listed for 7 days.


6. Upload a file

Two things decide how you upload:

  • What the file is attached to — a trip, or a traveler's profile field.
  • How big it is — above roughly 3.5 MB you must use the chunked flow.
📘

Uploading is two steps, and the second is not optional

Every upload route returns a File id. For a traveler profile field, that id then has to be written to the field in a separate call (step 6c). A file uploaded but never attached exists in storage and appears nowhere on the traveler's profile.

Which route?

RouteUse it whenPractical size limit
POST /users/file/uploadRecommended default. One call, and only the API host needs to be reachable — works behind a CASB, LNA or egress proxy that blocks direct storage access.~3.5 MB
POST /users/file/upload-chunked/*The file is larger than ~3.5 MB.no practical limit
PUT /users/file / PUT /trips/fileYour client can reach Amazon S3 directly and you want the bytes to bypass our API servers.limited only by S3
🚧

The limit is ~3.5 MB, not 25 MB

The API rejects anything above 25 MB, but the deployed API runs behind AWS Lambda, whose request payload limit puts the real ceiling far lower. Our own dashboard switches to the chunked flow above 3.5 MB — use the same threshold. Above it, POST /users/file/upload fails at the gateway before the request ever reaches the API.

6a. Proxy upload — recommended

POST /users/file/upload — API Reference

multipart/form-data; the file streams through the API to storage.

curl -X POST https://api.premote.io/v1/users/file/upload \
  -H 'Authorization: Bearer <token>' \
  -F '[email protected]' \
  -F 'fileType=PASSPORT' \
  -F 'userId=<userId>' \
  -F 'userFieldId=<userFieldId>'
Form fieldRequiredNotes
file✅The binary. The form field must be named file.
fileType—One of the values in How files work. Invalid values return 400.
userId—The traveler the file belongs to.
userFieldId—Validates the extension only, against that field's allowed types. It does not attach the file — see 6c.
metadata—Free-form metadata as a JSON string. Malformed JSON returns 400.

Response: { "id": 501 }

6b. Chunked upload — files over ~3.5 MB

Three calls: init → one chunk per part, sequentially → complete.

# 1. init — returns the chunk size to split on
curl -X POST https://api.premote.io/v1/users/file/upload-chunked/init \
  -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
  -d '{ "fileName": "work-permit.pdf", "fileType": "WORKFLOW_ATTACHED", "userId": <userId> }'
# → { "fileId": 501, "sessionId": "a1b2c3d4-...", "chunkSize": 4194304 }

# 2. one call per part — form field must be named `chunk`, partNumber is 1-indexed
curl -X POST https://api.premote.io/v1/users/file/upload-chunked/chunk \
  -H 'Authorization: Bearer <token>' \
  -F '[email protected]' -F 'sessionId=a1b2c3d4-...' -F 'partNumber=1'

# 3. complete
curl -X POST https://api.premote.io/v1/users/file/upload-chunked/complete \
  -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
  -d '{ "sessionId": "a1b2c3d4-...", "totalParts": 3 }'
# → { "id": 501 }

Split on the chunkSize the init call returns — don't hardcode it.

Reference: init · chunk · complete

6c. Presigned upload — direct to storage

The API registers the file row and hands you a pre-signed URL; you PUT the bytes straight to storage.

Scope the file to a trip with PUT /trips/file (API Reference), to a traveler with PUT /users/file (API Reference), or use the generic POST /files/signed-upload (API Reference):

curl -X PUT https://api.premote.io/v1/trips/file \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "fileName": "assignment-letter.pdf",
    "fileType": "USER_UPLOAD",
    "tripId": <tripId>
  }'

Response:

{
  "id": 1024,
  "url": "https://files.premote.io/7/assignment-letter.pdf?X-Amz-Signature=..."
}

Then upload the bytes to the returned url within 5 minutes:

curl -X PUT "<url from above>" \
  -H 'Content-Type: application/pdf' \
  --data-binary @assignment-letter.pdf
  • PUT the URL exactly as returned and add no extra headers.
  • On PUT /users/file, passing userFieldId validates the extension against that field's allowed types before the URL is issued — a rejected extension returns 400 with the accepted list.
🚧

The file row exists as soon as this call returns

If your upload of the bytes then fails, you are left with a registered file and nothing behind it. Verify the upload succeeded before attaching the id, or use the proxy route (6a), where the row is only created once the bytes have landed.

6d. Attach the file to a traveler's profile field

Trip-scoped uploads are done at this point. A file destined for a traveler profile field needs one more call, writing the id to the field by its key:

POST /users/{id}/data — API Reference

curl -X POST https://api.premote.io/v1/users/<userId>/data \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{ "passport": "501" }'
  • For a FILE field, the value is the file id as a string.
  • For a FILES (multi-file) field, the value is a comma-separated list of ids: "501,502". Sending one id replaces the whole list — read the current value from GET /users/{id}/data first and append.

Check the response body — a 201 alone does not mean the value was stored:

{ "skipped": [], "needsApproval": [], "unknown": [] }
FieldMeaning
skippedYour role may not edit that field; nothing was written.
needsApprovalQueued as an edit request — it applies once an admin approves.
unknownThe key matches no field for your company; nothing was written.

A key in unknown is almost always a typo or a field key from a different company. See User fields and company-specific fields for how to discover the keys you have.


Recap

StepActionEndpoint
1Download files for selected trips (ZIP)POST /trips/download/files
2Files for currently ongoing tripsPOST /trips/download/files?fetchLive=true
3Download link for a single fileGET /files/{key}
4On-demand documents (statement / risk / insurance)GET /trips/{tripId}/download-statement etc.
5Trip data exportPOST /trips/download/data → GET /trips/exports
6Upload a file (≤3.5 MB)POST /users/file/upload
6Upload a file (>3.5 MB)POST /users/file/upload-chunked/init → chunk → complete
6Attach an uploaded file to a profile fieldPOST /users/{id}/data

See also


Did this page help you?