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.

  1. Download premote-api-workflow.postman_collection.json — or, in Postman, choose Import → Link and paste that same URL.
  2. Set the collection variables clientId and clientSecret (from Settings → API access).
  3. 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 with 400 if neither is available.
  • platformAccess — (optional, default true) whether the employee can sign in. Creating a user never sends an invitation either way. Send false for 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 with PATCH /users/{id}/access — API Reference.

The response is the created user. Keep its id — you'll use it as the travelerId.

🚧

POST /users creates the account only

It writes only the account properties above. It cannot write profile fields such as date of birth or nationality — those go through POST /users/{id}/data in 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

The 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 carries userFieldId and the submitted value.
  • 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-empty unknown as 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:

typeSend
REFERENCEone option value: "client_meetings"
REFERENCE_MULTI, REFERENCE_MULTI_CHECKan array of option values: ["client_meetings", "training"]
🚧

travel_reason is a multi-select for most companies

On most accounts travel_reason is REFERENCE_MULTI, so a trip can carry several reasons. A few companies keep it single-select. Check type for 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-specific

Send options[].value, never options[].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:

EndpointAnswersAPI Reference
GET /trip-fields/mandatoryWhich trip fields are requiredReference
GET /entity-fields/mandatoryWhich entity fields are requiredReference
GET /user-fields/mandatoryWhich traveler profile fields are requiredReference

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 publishing

This example assumes origin/destination query 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 exist

This 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 a travelerId key 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 send null, 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 with 400. List your company's keys with GET /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 a process?

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 in processKeys (step 9) to choose which processes a trip triggers. For the numeric process ids the field-settings routes below take, use GET /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-specific

The {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:

ValueEffect
a template stringPrefills the field from the resolved placeholder
nullRemoves 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 verified

The 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:

CatalogHow to identify a reference field
GET /trip-fieldsEach 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-fieldsNo 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.contactPerson is. 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 explicit referencesCustomObjects boolean (alongside optionsAreStatic / optionsAreDynamic) — filter on referencesCustomObjects === true to 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

StepEndpoint
1. AuthenticatePOST /auth/token
2. Find travelerGET /users/list
3. Create travelerPOST /users (camelCase body)
4. Save profile dataPOST /users/{id}/data (snake_case field keys)
5. List profile fieldsGET /user-fields
6. List trip fieldsGET /trip-fields
7. Narrow to what's requiredGET /trip-fields/mandatory (+ entity/user siblings)
8. Check completenessPOST /trips/completeness/USERID
9. Create tripPOST /trips/multiple
10. Fetch resultGET /trips/{id}/object

See also


Did this page help you?