User fields and company-specific fields
Discover your company's profile field keys, understand why request bodies are camelCase but field maps are snake_case, and read the response that tells you whether a write actually landed.
The account vs the profile
Two different endpoints, two different key styles. This is the single most common source of confusion.
POST /users | POST /users/{id}/data | |
|---|---|---|
| Creates | the account | nothing — writes values onto an existing account |
| Body | a fixed, documented schema | a free-form map of field key → value |
| Keys | camelCase — firstName, lastName, email, roleId | snake_case field keys — birthday, nationality |
| Configurable? | no, same for every company | yes, partly company-specific |
| Response | the created user | { skipped, needsApproval, unknown } |
So firstName and first_name are both real, and they are not interchangeable:
# Create the account
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" }'
# Fill in the profile
curl -X POST https://api.premote.io/v1/users/123/data \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{ "birthday": "1990-04-12", "nationality": "DE" }'There are three profile-write endpoints — pick by whose profile you are writing:
| Endpoint | Writes | Permission model |
|---|---|---|
POST /users/{id}/data | another user's profile | admin / super-admin write through; a personal assistant is held to the same per-field permissions as the profile owner |
POST /users/data | the calling user's own profile | per-field permissions apply (super-admins excepted) |
POST /users/bulk-import | many users at once | super-admin |
POST /users/bulk-importuses snake_case for the name tooBulk import is the one place where the account fields are also snake_case:
first_name,last_name,employee_number. Every other key in the row is treated as a user-field key. It reports only a count — it does not tell you what was skipped, held for approval or unrecognised — so usePOST /users/{id}/datawhen you need that feedback. Empty values are ignored there rather than clearing the field.
Why is it aPOSTand not aPATCH?Because it is an upsert keyed by field, not a partial update of a resource: keys you omit are left untouched, and a key may produce an approval request instead of a write. There is no
PATCHalias — usePOST.
Discovering the fields for your company
GET /user-fields — API Reference
curl https://api.premote.io/v1/user-fields \
-H 'Authorization: Bearer <token>'The response is a list of categories; the field definitions live in each category's items:
{
"userFields": [
{
"id": 1,
"key": "personal",
"label": { "en": "Personal data", "de": "Persönliche Daten" },
"customizable": false,
"items": [
{
"id": 5,
"key": "birthday",
"label": { "en": "Birthday" },
"type": "DATE",
"permission": "EDIT",
"options": []
},
{
"id": 6,
"key": "nationality",
"label": { "en": "Nationality" },
"type": "REFERENCE_COUNTRIES",
"permission": "VIEW",
"options": [{ "value": "DE", "label": "Germany" }]
}
]
}
]
}Every items[].key is a key you may send to POST /users/{id}/data.
key vs label — always integrate against key
key vs label — always integrate against keykeyis the stable, snake_case identifier. This is what you send and what you match on.labelis human-readable and localised ({ en, de }). It is renamed freely by administrators and must never appear in your code.
The same applies to dropdown options: send options[].value, never options[].label.
Global vs company-specific fields
Some fields exist for every premote customer (birthday, nationality); others are created by your company (employee_number, cost_centre). Items carry customizable: true and a companyId when the definition belongs to your company.
You cannot infer the list from another tenant, from a fixture, or from this page — call GET /user-fields per company and cache the result. A company may also rename or re-configure a global field, so even the shared keys can differ in label, options and permissions.
Narrowing to what a specific traveler actually needs
GET /user-fields/profile/USERID — API Reference — returns the same grouped structure, filtered to the fields that traveler's active compliance processes require, with a countryCodes hint per field.
Both endpoints return definitions, not values.
Reading current values
GET /users/{id}/data — API Reference — a flat array, one entry per field:
[
{ "id": 5, "fieldKey": "birthday", "categoryKey": "personal", "fieldType": "DATE", "value": "1990-04-12" },
{ "id": 6, "fieldKey": "nationality", "categoryKey": "personal", "fieldType": "REFERENCE_COUNTRIES", "value": "DE,RU" }
]Note the shape difference: the definitions endpoints are grouped by category, this one is flat. Send fieldKey back as the key when writing.
To find out what is still missing for a given trip, use POST /trips/completeness/USERID — its missingUserFields array is the authoritative answer.
Writing values
curl -X POST https://api.premote.io/v1/users/123/data \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{ "birthday": "1990-04-12", "employee_number": "AC-4471" }'A 201 does not mean it was written
201 does not mean it was writtenAlways inspect the response:
{ "skipped": [], "needsApproval": [], "unknown": [] }| Array | Meaning | What to do |
|---|---|---|
unknown | the key matches no field for this company — a typo, or a field that has not been created yet | fix your mapping; the value was dropped |
skipped | the per-field permission refused the write (VIEW, or EDIT_IF_EMPTY on a field that already has a value) | expected for self-service callers; unexpected for an admin token |
needsApproval | the field is EDIT_REQUEST: no value was written, an approval request was filed instead | wait for an administrator to approve; the value is not live yet |
Treat any non-empty array as a condition your integration must surface, not as success.
Per-field permissions
| Permission | End users may | Notes |
|---|---|---|
EDIT | write freely | the default |
VIEW | not write | value appears in skipped |
EDIT_IF_EMPTY | set it once while empty | later writes appear in skipped |
EDIT_REQUEST | submit for approval | value appears in needsApproval, is not stored yet |
Admin and super-admin tokens bypass this policy entirely and write through — so skipped and needsApproval are always empty for them. If you are integrating with an admin token and expecting approval workflows, you will not get them.
Value formats
Every value is sent and stored as a string. What that string must contain depends on the field's type:
type | Send |
|---|---|
TEXT, TEXTAREA, WYSIWYG | the text |
NUMBER | the number as a string — "42" |
DATE | ISO date — "1990-04-12" |
TIME | "14:30" |
BOOLEAN | "true" / "false" |
REFERENCE_COUNTRY | one ISO-3166 ALPHA-2 code — "DE" |
REFERENCE_COUNTRIES | comma-separated ISO-3166 ALPHA-2 codes — "DE,RU" (dual nationals are normal) |
REFERENCE, REFERENCE_MULTI, AUTOCOMPLETE, SEARCH | one of options[].value from the field definition — company-specific |
FILE | the File id as a string — "501" |
FILES | comma-separated File ids — "501,502" |
For FILE and FILES see Working with files — the id comes from a separate upload call. Sending a FILES value replaces the whole list, so read the current value first and append.
Clearing a valueSend
""(ornull, which is coerced to"") to clear a field. Both profile-write endpoints treat an explicit empty as a clear, and the change is recorded in the profile changelog like any other edit. Omitting the key leaves the value untouched — omission and clearing are different operations.
Recommended integration shape
- On first run per company,
GET /user-fieldsand build a map from your source system's attribute → premotekey. Store it; do not hardcode keys in code that ships to more than one tenant. - Re-fetch periodically — administrators add and rename fields without telling integrators.
- On every write, assert
skipped,needsApprovalandunknownare all empty, and alert on anything else. A non-emptyunknownalmost always means your map has drifted from the company's configuration.
See also
Updated about 2 months ago
