Fields - Per diem expenses
Create and manage per diem and subsistence allowance expenses using the dynamic Fields system in the Expense API.
Create per diem and subsistence allowance expenses using the dynamic Fields system. This guide covers travel routes with multiple stops, destination-based rules, meal and accommodation deductions, and the complex update cycle that ties it all together.
Overview
Per diem expenses use the SubsistenceAllowance expense type. They represent daily allowances for business travel β covering meals, accommodation, and incidental costs based on the destination country, trip duration, and applicable deductions.
This is the most complex expense type in Findity. The server manages country-specific per diem rates, time-based allowance calculations, deduction rules for provided meals and accommodation, and multi-stop travel itineraries. Your client renders the fields and handles the add/remove stop interactions β the server does all the computation.
Key concepts
| Concept | Description |
|---|---|
| Travel route | An ordered list of travel stops with departure/arrival times and destinations. Each stop represents a leg of the trip. |
| Destination | The country or region for each travel leg. Determines per diem rates and deduction rules. |
| Deductions | Meals (breakfast, lunch, dinner) or accommodation provided by the employer reduce the per diem allowance. Deduction fields appear per stop. |
| Accommodation | Whether the traveler arranged their own accommodation or it was provided. Affects the allowance calculation. |
| Travel stops | Intermediate stops on a multi-destination trip. Each stop has its own departure time, destination, and deductions. |
| Local date/time | Per diem uses local time (LOCAL_DATE_TIME) without timezone β times represent the traveler's local time at each stop. |
| Night qualification | Some countries require overnight travel to qualify for per diem. The server determines this based on departure/arrival times. |
API endpoints
All per diem field operations use the standard Fields endpoints with expenseType=SubsistenceAllowance:
| Operation | Method | Endpoint |
|---|---|---|
| Initialize fields | GET | /v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/fields |
| Update fields | PUT | /v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/fields |
| Create expense | POST | /v1/expense/expenses?format=fields&organizationId={id}&expenseType=SubsistenceAllowance |
| Update expense | PUT | /v1/expense/expenses/{expenseId}?format=fields |
| Get categories | GET | /v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/categories |
| Get destinations | GET | /v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/destinations |
Step 1: Initialize fields
Fetch the field structure for a new per diem expense:
curl -X GET "https://stage-api.findity.com/api/v1/expense/me/organizations/{orgId}/expensetypes/SubsistenceAllowance/fields" \
-H "Authorization: Bearer {access_token}"To load an existing per diem expense for editing:
curl -X GET "https://stage-api.findity.com/api/v1/expense/me/organizations/{orgId}/expensetypes/SubsistenceAllowance/fields?expenseRecordId={expenseId}" \
-H "Authorization: Bearer {access_token}"Field structure overview
The response contains these top-level fields:
| Field property | controlType | Description |
|---|---|---|
id | TEXT | Expense ID (hidden, system field) |
categoryId | LIST | Per diem category β requiresUpdate: true |
verification.comment | MULTI_LINE_TEXT | User comment / description |
expenseReportId | LIST | Assign to an expense report |
verification.travelRoute | FIELD_GROUP | Travel route container with departure, stops, and return |
| Custom dimension fields | LIST | Organization-specific dimensions (e.g., cost center, project) |
The bulk of the complexity lives inside verification.travelRoute.
Step 2: Understand the travel route structure
The verification.travelRoute field group is the core of a per diem expense. It contains a dynamic set of nested field groups representing the travel itinerary.
Route structure
A per diem travel route consists of:
Travel Route (FIELD_GROUP)
βββ Departure (FIELD_GROUP)
β βββ Departure date/time (LOCAL_DATE_TIME)
β βββ Destination (LIST)
βββ Stop 1 (FIELD_GROUP) β optional, repeatable
β βββ Departure date/time (LOCAL_DATE_TIME)
β βββ Destination (LIST)
β βββ Deductions (FIELD_GROUP)
β βββ Breakfast (SWITCH)
β βββ Lunch (SWITCH)
β βββ Dinner (SWITCH)
β βββ Accommodation (LIST or SWITCH)
βββ Stop 2 (FIELD_GROUP) β optional, repeatable
β βββ ...
βββ Return (FIELD_GROUP)
βββ Return date/time (LOCAL_DATE_TIME)
Departure fields
The departure group defines where and when the trip starts:
| Field property | controlType | Description |
|---|---|---|
verification.departureDateTime | LOCAL_DATE_TIME | Departure date and time (local) β requiresUpdate: true |
verification.destination | LIST | Destination country/region β requiresUpdate: true |
Return fields
The return group defines when the trip ends:
| Field property | controlType | Description |
|---|---|---|
verification.returnDateTime | LOCAL_DATE_TIME | Return date and time (local) β requiresUpdate: true |
Deduction fields
Each travel stop (and potentially the overall trip) includes deduction toggles:
| Field property | controlType | Description |
|---|---|---|
verification.deduction.breakfast | SWITCH | Breakfast provided by employer β requiresUpdate: true |
verification.deduction.lunch | SWITCH | Lunch provided by employer β requiresUpdate: true |
verification.deduction.dinner | SWITCH | Dinner provided by employer β requiresUpdate: true |
verification.deduction.accommodation | LIST or SWITCH | Accommodation type/status β requiresUpdate: true |
Deduction fields have
requiresUpdate: truebecause toggling them triggers a server-side recalculation of the per diem allowance. The accommodation field can be either aSWITCH(provided/not provided) or aLIST(type of accommodation) depending on the country's rules.
The LOCAL_DATE_TIME control type
LOCAL_DATE_TIME control typePer diem uses LOCAL_DATE_TIME instead of DATE_TIME. Values are formatted as yyyy-MM-dd HH:mm without timezone information:
{ "property": "verification.departureDateTime", "controlType": "LOCAL_DATE_TIME", "value": "2024-06-10 08:30" }This represents the traveler's local time at the point of departure β not UTC. The server uses these local times to calculate trip duration and determine which per diem rates apply.
Step 3: Manage travel stops
Travel stops allow users to create multi-destination itineraries. Each stop represents a change of destination during the trip, potentially with different per diem rates and deduction rules.
Adding a stop
The travel route field group includes commands for adding stops:
{
"commands": [
{
"text": "Add stop",
"confirmationRequired": false,
"command": "action=addStop&afterIndex=0"
}
]
}Execute the command by appending it to the PUT URL:
curl -X PUT "https://stage-api.findity.com/api/v1/expense/me/organizations/{orgId}/expensetypes/SubsistenceAllowance/fields?action=addStop&afterIndex=0" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{ "fields": [...full fields array...] }'The server responds with an updated fields structure that includes a new stop field group inserted at the specified position.
Removing a stop
Each intermediate stop has a deleteCommand that removes it:
{
"property": "verification.travelStops[0]",
"controlType": "FIELD_GROUP",
"deletable": true,
"deleteCommand": {
"text": "Remove stop",
"confirmationRequired": true,
"confirmation": "Are you sure you want to remove this stop?",
"command": "action=removeStop&index=0"
}
}Execute by appending the command to the PUT URL:
PUT /v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/fields?action=removeStop&index=0
When
confirmationRequiredistrue, display theconfirmationtext in a dialog before executing the command. Removing a stop deletes all associated deduction data.
Stop field structure
Each travel stop contains its own set of fields:
| Field property | controlType | Description |
|---|---|---|
verification.travelStops[n].departureDateTime | LOCAL_DATE_TIME | Departure from this stop β requiresUpdate: true |
verification.travelStops[n].destination | LIST | Destination for this leg β requiresUpdate: true |
verification.travelStops[n].deduction.breakfast | SWITCH | Breakfast deduction for this leg |
verification.travelStops[n].deduction.lunch | SWITCH | Lunch deduction for this leg |
verification.travelStops[n].deduction.dinner | SWITCH | Dinner deduction for this leg |
verification.travelStops[n].deduction.accommodation | LIST or SWITCH | Accommodation for this leg |
Step 4: Handle destinations
The destination field determines which country-specific per diem rates and rules apply. Each leg of the trip can have a different destination.
Fetching destinations
GET /v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/destinations
Destinations are typically countries, but some countries have region-specific rates (e.g., different rates for major cities vs. the rest of the country).
Example response
{
"meta": { "count": 3, "total": 195 },
"data": [
{ "id": "SE", "name": "Sweden" },
{ "id": "NO", "name": "Norway" },
{ "id": "DE", "name": "Germany" }
]
}Changing a destination has requiresUpdate: true because it can:
- Change the per diem daily rate
- Alter which deduction fields are visible
- Modify the accommodation field type (SWITCH vs. LIST)
- Update the allowance calculation
Step 5: The update cycle
Per diem expenses have the most active update cycle of all expense types. Nearly every meaningful user interaction triggers a server round-trip:
PUT /v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/fields
Fields that trigger updates
| Field | What changes on update |
|---|---|
categoryId | Per diem rules, visible fields, deduction configuration |
verification.departureDateTime | Trip duration, allowance calculation, night qualification |
verification.returnDateTime | Trip duration, allowance calculation, night qualification |
verification.destination | Per diem rates, deduction rules, accommodation options |
verification.deduction.breakfast | Allowance recalculation (deduction applied/removed) |
verification.deduction.lunch | Allowance recalculation (deduction applied/removed) |
verification.deduction.dinner | Allowance recalculation (deduction applied/removed) |
verification.deduction.accommodation | Accommodation allowance recalculation |
verification.travelStops[n].departureDateTime | Per-leg duration, rate calculation |
verification.travelStops[n].destination | Per-leg rates, deduction rules |
verification.travelStops[n].deduction.* | Per-leg allowance recalculation |
Example update request
curl -X PUT "https://stage-api.findity.com/api/v1/expense/me/organizations/{orgId}/expensetypes/SubsistenceAllowance/fields" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"fields": [
{ "property": "id", "value": null },
{ "property": "categoryId", "value": "c3d4e5f6a1b2" },
{ "property": "verification.travelRoute", "fields": [
{ "property": "verification.departureDateTime", "value": "2024-06-10 08:30" },
{ "property": "verification.destination", "value": "DE" },
{ "property": "verification.returnDateTime", "value": "2024-06-12 18:00" },
{ "property": "verification.deduction.breakfast", "value": true },
{ "property": "verification.deduction.lunch", "value": false },
{ "property": "verification.deduction.dinner", "value": false },
{ "property": "verification.deduction.accommodation", "value": "OWN" }
]}
]
}'Always send the complete fields array β including hidden fields, all travel stops, and fields you didn't modify. The server uses the full state to compute the per diem allowance.
Calculated output fields
After each update, the server returns calculated fields that you display as read-only:
| Field property | controlType | Description |
|---|---|---|
verification.totalAllowance | DOUBLE | Total per diem allowance after deductions |
verification.dailyRate | DOUBLE | Per diem daily rate for the destination |
verification.numberOfDays | DOUBLE | Calculated trip duration in days |
verification.deductionTotal | DOUBLE | Total amount deducted for provided meals/accommodation |
These fields typically have disabled: true β display them to the user but don't allow editing.
Step 6: Handle the summary
The travel route FIELD_GROUP includes a summary array that provides a compact preview of the trip:
{
"summary": [
{ "type": "property_value", "property": "Destination", "value": "Germany" },
{ "type": "property_value", "property": "Departure", "value": "10 Jun 2024 08:30" },
{ "type": "property_value", "property": "Return", "value": "12 Jun 2024 18:00" },
{ "type": "property_value", "property": "Allowance", "value": "1,250.00 SEK" }
]
}Use the summary to display a collapsed view of the travel route β particularly useful in list views or when the route field group is minimized.
Step 7: Save the expense
Once all stops, destinations, and deductions are configured, save the per diem expense.
Using fields format (recommended)
Create:
POST /v1/expense/expenses?format=fields&organizationId={id}&expenseType=SubsistenceAllowance
Update:
PUT /v1/expense/expenses/{expenseId}?format=fields
Send the full fields payload as the request body.
Using standard JSON format
curl -X POST "https://stage-api.findity.com/api/v1/expense/expenses" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"organizationId": "ff808181963979e20196397a2098004b",
"categoryId": "c3d4e5f6a1b2",
"verification": {
"type": "SubsistenceAllowance",
"comment": "Business trip to Berlin",
"departureDateTime": "2024-06-10 08:30",
"returnDateTime": "2024-06-12 18:00",
"destination": "DE",
"travelStops": [],
"deductions": {
"breakfast": [true, false],
"lunch": [false, false],
"dinner": [false, true],
"accommodation": "OWN"
}
}
}'Example response
{
"id": "9f3a5b7c1d2e4f6a8b0c2d4e6f8a0b2c",
"organizationId": "ff808181963979e20196397a2098004b",
"personId": "ff808181963979e20196397a119f002d",
"status": "NORMAL",
"processStatus": "DRAFT",
"categoryId": "c3d4e5f6a1b2",
"reimbursementCurrency": "SEK",
"reimbursementAmount": 1250.00,
"dateCreated": "2024-06-10T09:15:00Z",
"lastUpdated": "2024-06-10T09:15:00Z",
"verification": {
"type": "SubsistenceAllowance",
"comment": "Business trip to Berlin",
"departureDateTime": "2024-06-10 08:30",
"returnDateTime": "2024-06-12 18:00",
"destination": "DE",
"travelStops": []
},
"meta": {
"abbreviation": {
"reimbursementDescription": "1,250.00 SEK",
"dateDescription": "10β12 Jun 2024"
},
"capabilities": {
"canBeEdited": true,
"canBeDeleted": true,
"canBeSentIn": true
}
}
}Multi-stop trips
For trips with multiple destinations, each leg has its own per diem rate and deduction configuration.
Example: Three-country trip
A trip from Stockholm β Berlin β Paris β Stockholm:
Travel Route
βββ Departure: 2024-06-10 08:30
β βββ Destination: Germany
βββ Stop 1: 2024-06-12 09:00
β βββ Destination: France
β βββ Deductions: Lunch provided
βββ Return: 2024-06-14 20:00
Each leg uses the destination's per diem rate for the duration of that leg. The server calculates partial-day allowances for the first and last days based on departure and arrival times.
Building the fields payload for multi-stop
{
"fields": [
{ "property": "id", "value": null },
{ "property": "categoryId", "value": "c3d4e5f6a1b2" },
{ "property": "verification.travelRoute", "fields": [
{ "property": "verification.departureDateTime", "value": "2024-06-10 08:30" },
{ "property": "verification.destination", "value": "DE" },
{ "property": "verification.travelStops", "fields": [
{ "property": "verification.travelStops[0]", "fields": [
{ "property": "verification.travelStops[0].departureDateTime", "value": "2024-06-12 09:00" },
{ "property": "verification.travelStops[0].destination", "value": "FR" },
{ "property": "verification.travelStops[0].deduction.breakfast", "value": false },
{ "property": "verification.travelStops[0].deduction.lunch", "value": true },
{ "property": "verification.travelStops[0].deduction.dinner", "value": false },
{ "property": "verification.travelStops[0].deduction.accommodation", "value": "OWN" }
]}
]},
{ "property": "verification.returnDateTime", "value": "2024-06-14 20:00" },
{ "property": "verification.deduction.breakfast", "value": false },
{ "property": "verification.deduction.lunch", "value": false },
{ "property": "verification.deduction.dinner", "value": false },
{ "property": "verification.deduction.accommodation", "value": "OWN" }
]}
]
}Country-specific behavior
Per diem rules vary significantly by country. The Fields system abstracts this complexity, but understanding the differences helps when building your UI:
| Country | Key behavior |
|---|---|
| Sweden | Rates based on trip duration (half-day, full-day, overnight). Night travel qualification rules apply. Reduced rate after 3 months at the same destination. |
| Norway | Destination-specific rates for domestic travel. Accommodation type (hotel, private, camping) affects the rate. Toll cost data integration available. |
| Denmark | Fixed daily rates with standard meal deduction percentages. |
| Finland | Full-day and partial-day rates. Meal deductions as fixed amounts rather than percentages. |
| Germany | Two-tier rates (arrival/departure day vs. full day). Destination-specific rates for international travel. |
You don't need to implement these rules. The server handles all country-specific logic through the Fields update cycle. This table is provided for context when testing and debugging per diem calculations.
INFORMATION fields
Per diem expenses frequently include INFORMATION fields that display calculated summaries, warnings, or regulatory notes:
{
"property": "verification.perDiemInfo",
"controlType": "INFORMATION",
"informationType": "information",
"value": "Per diem rate for Germany: 420 SEK/day. Trip qualifies for 2.5 days.",
"visible": true,
"dismissible": false
}Render these based on the informationType:
| informationType | Rendering |
|---|---|
normal | Neutral text, no special styling |
information | Blue/info banner |
warning | Yellow/orange warning banner |
error | Red error banner (e.g., rejection comment) |
success | Green success banner |
Common integration patterns
Progressive form disclosure
Don't render all fields at once β per diem forms can be overwhelming. Start with departure date/time and destination. After the user fills these in and the update cycle returns, reveal the deductions and accommodation fields. For multi-stop trips, show an "Add stop" button below the current route.
Debouncing the update cycle
Per diem has many requiresUpdate fields. When the user is editing a date/time picker, don't trigger an update on every keystroke. Debounce the PUT request β wait until the user has finished entering the full date/time value (e.g., on blur or after 500ms of inactivity) before sending the update.
Visualizing the itinerary
Use the summary data and travel stops to build a visual timeline of the trip. Display each leg with its destination, duration, and deduction status. This helps users verify their itinerary before saving β especially important for multi-stop trips where per diem rates change between legs.
Handling overnight travel qualification
Some countries (notably Sweden) have rules where trips must include overnight travel to qualify for per diem. If a trip doesn't qualify, the server may return an INFORMATION field with informationType: warning explaining why. Display this prominently so the user understands why their allowance is zero or reduced.
Cloning previous per diem trips
For users who make recurring trips, consider loading a previous per diem expense's fields and pre-filling the new form with the same destinations and stop structure. Initialize a new expense, then apply the saved values programmatically before the first PUT β the server recalculates rates based on the new dates.
Error handling
Common errors when working with per diem expenses:
| Status code | Error code | Description |
|---|---|---|
400 | INVALID_JSON_BODY | Failed to parse the request body. Verify your JSON payload structure. |
400 | INVALID_JSON_PROPERTY_VALUE | Missing or invalid type on verification β ensure type is set to SubsistenceAllowance. |
400 | NOT_A_SUBSISTENCE_ALLOWANCE_CATEGORY | The category ID is not valid for per diem expenses. Fetch categories for SubsistenceAllowance. |
400 | DEPARTURE_DATE_REQUIRED | Departure date/time is missing. |
400 | RETURN_DATE_REQUIRED | Return date/time is missing. |
400 | RETURN_BEFORE_DEPARTURE | Return date/time is earlier than departure. |
400 | DESTINATION_REQUIRED | No destination selected for one or more travel legs. |
400 | STOP_DEPARTURE_BEFORE_PREVIOUS | A stop's departure time is earlier than the previous stop's departure. |
400 | INVALID_STOP_INDEX | The afterIndex or index parameter in an addStop/removeStop command is out of range. |
404 | EXPENSE_RECORD_NOT_FOUND | The expense ID does not exist or is not accessible by the current user. |
404 | CATEGORY_NOT_FOUND | The specified category does not exist in the organization. |
Next steps
Full reference for the Fields system, control types, and the update cycle
Guide for creating receipt-based expenses using Fields
Guide for creating distance-based mileage expenses using Fields
Guide for creating food benefit expenses using Fields
Updated about 5 hours ago
