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:
- 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.
- 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.
- Files carry a type. Each file has a
fileTypetelling you what it is. Some are written by premote's own automation and some you set when uploading:
You set these when uploading
fileType | What it is |
|---|---|
PASSPORT, ID_CARD, PERMANENT_RESIDENCE | Traveler identity documents |
USER_UPLOAD | Any other file a user attaches |
WORKFLOW_ATTACHED | A document supporting a compliance filing (work permit, assignment letter) |
INSURANCE | Insurance document |
MISC | Anything else |
Written by premote — don't set these by hand
fileType | What it is |
|---|---|
SSN_REQUEST | Social security request submitted to an authority (e.g. an A1 application) |
SSN_REPLY | The authority's reply — e.g. the issued A1 certificate |
NOTIFICATION_REQUEST | Posted-worker (PWD) notification submitted to an authority |
NOTIFICATION_REPLY | Confirmation returned for a posted-worker notification |
SOCIAL_SECURITY_STATEMENT | Social security statement generated for a trip |
RISK_ASSESSMENT | Risk assessment report |
TRIP_EXPORT | Generated trip data export (see step 5) |
POLICY, WORKATION_POLICY | Company policy documents |
LOGO | Company branding asset |
ERROR | Diagnostic 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.zipimport { 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 returns404— 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 whenfetchLive=true(see step 2).
The ZIP contains generated documents onlyThis 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
fetchLivePass 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.zipUse this when you want the current compliance documents for everyone traveling right now — e.g. a daily sync into your HR system.
WhatfetchLivedoes — and doesn't — do
fetchLiveselects which trips are included: approved trips withstart 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. WhenfetchLive=true, thetypeparameter 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:
| Document | Endpoint |
|---|---|
| Social security statement | GET /trips/{tripId}/download-statement — API Reference |
| Risk assessment report | GET /risk-rules/{tripId}/download-assessment — API Reference |
| Health insurance document | GET /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 returns400for 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 optionalEvery upload route returns a
Fileid. 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?
| Route | Use it when | Practical size limit |
|---|---|---|
POST /users/file/upload | Recommended 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/file | Your 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 MBThe 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/uploadfails 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 field | Required | Notes |
|---|---|---|
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.pdfPUTthe URL exactly as returned and add no extra headers.- On
PUT /users/file, passinguserFieldIdvalidates the extension against that field's allowed types before the URL is issued — a rejected extension returns400with the accepted list.
The file row exists as soon as this call returnsIf 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
FILEfield, 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 fromGET /users/{id}/datafirst and append.
Check the response body — a 201 alone does not mean the value was stored:
{ "skipped": [], "needsApproval": [], "unknown": [] }| Field | Meaning |
|---|---|
skipped | Your role may not edit that field; nothing was written. |
needsApproval | Queued as an edit request — it applies once an admin approves. |
unknown | The 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
| Step | Action | Endpoint |
|---|---|---|
| 1 | Download files for selected trips (ZIP) | POST /trips/download/files |
| 2 | Files for currently ongoing trips | POST /trips/download/files?fetchLive=true |
| 3 | Download link for a single file | GET /files/{key} |
| 4 | On-demand documents (statement / risk / insurance) | GET /trips/{tripId}/download-statement etc. |
| 5 | Trip data export | POST /trips/download/data → GET /trips/exports |
| 6 | Upload a file (≤3.5 MB) | POST /users/file/upload |
| 6 | Upload a file (>3.5 MB) | POST /users/file/upload-chunked/init → chunk → complete |
| 6 | Attach an uploaded file to a profile field | POST /users/{id}/data |
See also
Updated about 2 months ago
