Create a trip via the API
End-to-end walkthrough: authenticate, find or create the traveler, complete their profile, and submit the trip — with a ready-to-run Postman collection that chains each step automatically.
Run it in Postman
Prefer to click through it? The same happy path is available as a Postman collection that chains the token, user ID, and trip ID automatically between requests.
- Download premote-api-workflow.postman_collection.json — or, in Postman, choose Import → Link and paste that same URL.
- Set the collection variables
clientIdandclientSecret(from Settings → API access). - Run the requests top-to-bottom, or use the Collection Runner.
1. Get a bearer token
Exchange your API credentials (generated in Settings → API access) for a short-lived bearer token using the OAuth 2.0 client-credentials flow.
POST /auth/token — API Reference
curl -X POST https://api.premote.io/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{
"client_id": "prm_live_...",
"client_secret": "prm_secret_..."
}'Response:
{ "access_token": "eyJhbGciOi..." }Send the token as Authorization: Bearer <access_token> on every subsequent request.
2. Find the traveler
Before creating a user, check whether they already exist (users are also provisioned via HRIS sync, so most travelers are already present).
GET /users/list — API Reference
curl https://api.premote.io/v1/users/list \
-H 'Authorization: Bearer <token>'Match the traveler by email in the returned list and note their id.
3. Create the traveler (if not found)
If the traveler doesn't exist, create them. This endpoint requires a company super-admin token.
POST /users — API Reference
curl -X POST https://api.premote.io/v1/users \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"email": "[email protected]",
"firstName": "Jane",
"lastName": "Doe"
}'email,firstName,lastName— required, and camelCase (this is a typed request body, not a field-value map — see the callout at the top).roleId— (optional) role to assign. Falls back to your company's default role; the request fails with400if neither is available.platformAccess— (optional, defaulttrue) whether the employee can sign in. Creating a user never sends an invitation either way. Sendfalsefor an employee whose trips are managed entirely by admins or by your integration — the record exists for compliance filings but can never log in. Change it later withPATCH /users/{id}/access— API Reference.
The response is the created user. Keep its id — you'll use it as the travelerId.
POST /userscreates the account onlyIt writes only the account properties above. It cannot write profile fields such as date of birth or nationality — those go through
POST /users/{id}/datain the next step, with different (snake_case) keys.
4. Complete the traveler's profile
Trips can only be created once the traveler's required profile fields are filled. Save profile values with:
POST /users/{id}/data — API Reference
curl -X POST https://api.premote.io/v1/users/123/data \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"nationality": "DE",
"birthday": "1990-04-12"
}'The body is a flat map of field_key → value. It is an upsert: keys you omit are left untouched, so you can send partial updates. The keys are UserField.key values — snake_case, and partly specific to your company. See User fields and company-specific fields for how to discover them.
A 201 does not mean everything was written
201 does not mean everything was writtenThe response tells you what actually happened:
{
"skipped": [],
"needsApproval": [],
"unknown": []
}skipped— keys refused by the per-field permission policy (a read-only field, or a write-once field that already has a value). Only ever non-empty for self-service and personal-assistant callers; admin and super-admin tokens bypass the policy.needsApproval— the field requires approval, so an approval request was filed instead of writing the value. Each entry carriesuserFieldIdand the submittedvalue.unknown— keys that match no field for your company. Almost always a typo or a field that has not been created yet. These are dropped silently — treat a non-emptyunknownas an integration error.
Assert that all three arrays are empty before you consider the profile written.
Reading values back
GET /users/{id}/data — API Reference — returns a flat array, one entry per field, with the current value:
[
{ "id": 5, "fieldKey": "birthday", "categoryKey": "personal", "fieldType": "DATE", "value": "1990-04-12" },
{ "id": 6, "fieldKey": "nationality", "categoryKey": "personal", "fieldType": "REFERENCE_COUNTRIES", "value": "DE,RU" }
]Send fieldKey back as the key when writing. Uploading a document (passport, work permit) is a separate two-step flow — see Working with files.
5. List the profile fields available to you
GET /user-fields — API Reference — returns the profile fields configured for your company. GET /user-fields/profile/USERID — API Reference — returns the same thing narrowed to the fields that a specific traveler's active compliance processes actually need.
curl https://api.premote.io/v1/user-fields \
-H 'Authorization: Bearer <token>'Both return categories, with the field definitions nested in each category's items:
{
"userFields": [
{
"id": 1,
"key": "personal",
"label": { "en": "Personal data" },
"items": [
{ "id": 5, "key": "birthday", "label": { "en": "Birthday" }, "type": "DATE", "permission": "EDIT" },
{ "id": 6, "key": "nationality", "label": { "en": "Nationality" }, "type": "REFERENCE_COUNTRIES", "permission": "VIEW" }
]
}
]
}Every items[].key is a key you can send in step 4. These endpoints return definitions, not values — for current values use GET /users/{id}/data, and for what is still missing use step 8.
6. Discover the trip fields
GET /trip-fields — API Reference — returns the trip fields available to your company, grouped by category exactly like GET /user-fields. Every items[].key is a key you can send in the trip's values object. Optionally narrow them to a trip type.
curl 'https://api.premote.io/v1/trip-fields?tripType=BUSINESS_TRIP' \
-H 'Authorization: Bearer <token>'For a dropdown field, the accepted values are in items[].options[].value:
{
"id": 11,
"key": "travel_reason",
"label": { "en": "Travel reason" },
"type": "REFERENCE_MULTI",
"options": [
{ "value": "client_meetings", "label": "Client meeting" },
{ "value": "training", "label": "Training" }
]
}type tells you how many values the field takes:
type | Send |
|---|---|
REFERENCE | one option value: "client_meetings" |
REFERENCE_MULTI, REFERENCE_MULTI_CHECK | an array of option values: ["client_meetings", "training"] |
travel_reasonis a multi-select for most companiesOn most accounts
travel_reasonisREFERENCE_MULTI, so a trip can carry several reasons. A few companies keep it single-select. Checktypefor your company rather than assuming either. A comma-separated string ("client_meetings,training") is accepted too, but an array is the clearer contract. Reads always return the stored comma-separated string — split it on,if you need a list back.
Dropdown values are company-specificSend
options[].value, neveroptions[].label. The option lists are configured per company — two customers can use entirely different value sets for the same field key, and some use numeric codes. Read them from this endpoint rather than hardcoding them.
7. Narrow down to what's required for this route
GET /trip-fields (step 6) returns every field your company has ever configured for a trip type — it doesn't tell you which of them are required for a specific route. Requiredness is computed from the origin/destination pair (which government filings apply), not stored as a fixed property on the field, so the catalog alone can't answer it.
GET /trip-fields/mandatory — API Reference
curl 'https://api.premote.io/v1/trip-fields/mandatory?origin=PT&destination=FR' \
-H 'Authorization: Bearer <token>'Two sibling endpoints answer the same question for the other field types a trip can require:
Use this to build your own pre-flight validation — greying out fields the traveler doesn't need, before you have a draft trip to check completeness against. It doesn't replace step 8 — that step also accounts for the traveler's existing profile data and company-specific policy overrides. Treat this endpoint as "what does the route need," and completeness as "what does this person, on this route, still need."
Verify the query parameters before publishingThis example assumes
origin/destinationquery parameters — confirm the exact parameter names against the API Reference link above; they haven't been hand-tested for this guide.
8. Check trip completeness
Before creating the trip, check which trip and profile fields a set of draft values is still missing for a given traveler:
POST /trips/completeness/USERID — API Reference
curl -X POST https://api.premote.io/v1/trips/completeness/123 \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"type": "BUSINESS_TRIP",
"destination": "FR",
"start_date": "2026-09-01",
"end_date": "2026-09-05"
}'Response:
{
"missingUserFields": [],
"missingTripFields": [],
"missingEntityFields": [],
"wrongUserFields": [],
"wrongTripFields": [],
"notApplicableTripFieldKeys": []
}When every missing* and wrong* array is empty, the trip is ready to create. missingUserFields is the authoritative answer to "is this traveler's profile complete?" — go back to step 4 for anything it lists. missingEntityFields covers company/entity-level data (e.g. a required company registration number) — don't skip it just because it's traveler-independent. notApplicableTripFieldKeys lists fields that exist on your catalog but don't apply to this specific origin/destination — safe to ignore, not a sign of misconfiguration.
Two completeness endpoints existThis guide uses
POST /trips/completeness/USERID(requires an admin or travel-manager token). A second, unguarded route,POST /trips/completeness, checks completeness for the calling user by default, or for another traveler via atravelerIdkey in the request body instead of a path parameter. Both accept the same flat body and return the same response shape — pick whichever fits your integration's auth model, but don't expect them to be interchangeable in code that hardcodes one shape.
9. Create the trip
POST /trips/multiple — API Reference — creates one trip per traveler ID. travelerIds is required — pass a single-item array containing the user you found or created in steps 2–3.
curl -X POST https://api.premote.io/v1/trips/multiple \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"travelerIds": [123],
"type": "BUSINESS_TRIP",
"values": {
"origin": "DE",
"destination": "FR",
"start_date": "2026-09-01",
"end_date": "2026-09-05",
"travel_reason": ["client_meetings"]
},
"externalId": "acme-trip-4471"
}'travelerIds— array of traveler user IDs (one trip per ID). Required.type— the trip type. Required.values— trip field values, keyed by field key (snake_case, from step 6). Required.externalId— (optional) your own identifier, stored against the trip.processKeys— (optional) which compliance processes this trip may trigger, by process key (e.g.["a1-de"]). Omit it, or sendnull, to run every process that applies to the route — the default. Send a list to run only those, or[]to run none. Unknown keys are rejected with400. List your company's keys withGET /processes— API Reference.
Note the split: travelerIds, type and externalId are camelCase properties of the request schema; everything inside values is a snake_case field key.
The value shown for travel_reason above is an example only — read the accepted values, and whether the field takes one value or several, from step 6.
A process you leave out of processKeys is not filed. GET /trips/{id}/processes — API Reference — reports it as suppressed_by_request, and POST /processes/rerun/{tripId} starts it later and adds it to the trip's list.
The response is the created trip(s).
10. Fetch the result
Retrieve the full trip object — including its risk assessment — by ID:
GET /trips/{id}/object — API Reference
curl https://api.premote.io/v1/trips/456/object \
-H 'Authorization: Bearer <token>'The response is the trip flattened into a single object: stable envelope fields (id, status, type, travelerId, origin, destination, startDate, endDate, riskResult, …) plus the trip's field values inlined as additional camelCase keys (these vary per company — the keys come from step 6).
Reusing values across trips: field settings & fixed values
Some values — a contact person's email, a fixed cost-center code, an entity's registration number — are the same on every trip and shouldn't need to be sent on every POST /trips/multiple call. You can configure a fixed value per trip field, per process, that's resolved automatically at trip creation and at completeness-check time. Your integration never has to send these fields at all once they're configured.
How it works
A fixed value is a template string containing one or more {{placeholder.paths}}, resolved against the trip's traveler, entity, or (where configured) a linked customer record. For example, setting the contact_person_email trip field's fixed value to {{entity.contactPerson.email}} means every trip filed under that entity automatically gets the entity's configured contact person's email — no contact_person_email key needed in your values object.
What's aprocess?A process is a reusable filing-type definition your company has enabled — e.g. "PWD" (Posted Worker Notification) or "A1 via SV-Meldeportal" — shared across every trip that needs it, not a per-trip object. Every trip on a given origin/destination route resolves to the same process; each such trip gets its own run against that one process. List the process keys available to your company with
GET /processes— API Reference; use them inprocessKeys(step 9) to choose which processes a trip triggers. For the numeric processids the field-settings routes below take, useGET /companies/{companyId}/processes. That catalog is small and changes rarely — there's no need to look it up per trip.
List available placeholders:
GET /processes/{processId}/field-settings/placeholders — API Reference
curl https://api.premote.io/v1/processes/42/field-settings/placeholders \
-H 'Authorization: Bearer <token>'Returns a catalog grouped by context — traveler.*, entity.*, manager.*, personalAssistant.*, customer.*, trip.*.
The catalog is company-wide, not process-specificThe
{processId}path segment doesn't change the response — the placeholder list depends only on your company's configured field schemas (plus whether a "customer" custom object type exists). Any valid process id returns the identical catalog; call it once and reuse the result rather than refetching per process.
List current settings for a process:
GET /processes/{processId}/field-settings — API Reference
Set a fixed value on a trip field, scoped to one process (find valid processIds via GET /companies/{companyId}/processes):
curl -X PUT https://api.premote.io/v1/processes/42/field-settings/91 \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"fixedValue": "{{entity.contactPerson.email}}"
}'91 here is the trip field's numeric id — the same id returned for that field by GET /trip-fields (step 6), not its key. To set a fixed value on contact_person_email, first look up that field's id in the trip-fields catalog, then PUT to .../field-settings/{that id}.
fixedValue has three meaningful states:
| Value | Effect |
|---|---|
| a template string | Prefills the field from the resolved placeholder |
null | Removes your override; falls back to the field's system default |
"" (empty string) | Explicitly means "always ask — never prefill," even if a system default exists |
Reset a field to its default: DELETE /processes/{processId}/field-settings/{tripFieldId}
The contact person specifically
entity.contactPerson is itself a reference to one of your employees, configured once per entity (in the dashboard, not via this API). Once that's set, and once your trip-level contact fields (contact_person_name, contact_person_email, contact_person_phone, contact_person_address, …) have fixed values pointing at {{entity.contactPerson.*}}, every trip filed under that entity inherits the same contact person automatically. If an individual trip genuinely needs a different contact person, just pass the trip field explicitly in values on step 9 — an explicit value always overrides the configured default.
Order of precedence: value you send in values → configured fixedValue → field's own system default → missing (reported by step 8).
Placeholder catalog sample not yet verifiedThe six-group structure and its company-only scoping above are confirmed against the implementation, but a real captured response is not yet in this doc. Paste one here once available.
Fields that reference another record by ID
A small number of fields don't hold a value directly — they hold the id of another record, and that record's details are resolved server-side wherever they're needed (a filing PDF, a placeholder substitution, a dashboard display). You'll see this as field type REFERENCE (or REFERENCE_MULTI) in the GET /trip-fields / GET /entity-fields catalogs — though only one of the two lets you identify it programmatically today:
| Catalog | How to identify a reference field |
|---|---|
GET /trip-fields | Each item carries a fully dereferenced customObjectType object. Non-null means the field's values are ids into that Custom Object Type; null means it's a plain dropdown (check optionsAreStatic / optionsAreDynamic for where its options come from). |
GET /entity-fields | No equivalent flag exists yet — you cannot currently tell from the response alone whether an entity REFERENCE field links to a custom object. Ask your Premote contact if your integration needs this. |
Two flavors exist:
-
References into Premote's own data — e.g. an entity's configured contact person points at a User (an employee). You never construct these yourself; they're set in the dashboard and consumed via the placeholder mechanism above.
-
References into your own business objects — if you want a trip to link to something you maintain (a customer account, a cost center, a project code), your company can define a Custom Object Type for it and load records into it. A trip field can then be configured to reference that type, and your integration sends just the id:
{ "values": { "customer": "83686" } }The referenced record is resolved wherever the field's value is displayed or filed, the same way
entity.contactPersonis. Setting this up (defining the Custom Object Type, loading records) is a one-time admin/dashboard task — talk to your Premote contact if you have a business object you'd like to reference this way instead of duplicating its details onto every trip.
Nested references (a custom object's own fields)If a Custom Object Type's own fields can themselves reference another Custom Object Type, that's a separate catalog:
GET /custom-object-type-fields?customObjectTypeId=...returns each field definition with an explicitreferencesCustomObjectsboolean (alongsideoptionsAreStatic/optionsAreDynamic) — filter onreferencesCustomObjects === trueto find them. This describes the custom object type's own internal schema; it's unrelated to whether a trip or entity field references that type, which is the table above.
Recap
| Step | Endpoint |
|---|---|
| 1. Authenticate | POST /auth/token |
| 2. Find traveler | GET /users/list |
| 3. Create traveler | POST /users (camelCase body) |
| 4. Save profile data | POST /users/{id}/data (snake_case field keys) |
| 5. List profile fields | GET /user-fields |
| 6. List trip fields | GET /trip-fields |
| 7. Narrow to what's required | GET /trip-fields/mandatory (+ entity/user siblings) |
| 8. Check completeness | POST /trips/completeness/USERID |
| 9. Create trip | POST /trips/multiple |
| 10. Fetch result | GET /trips/{id}/object |
See also
Updated 3 days ago
