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

ConceptDescription
Travel routeAn ordered list of travel stops with departure/arrival times and destinations. Each stop represents a leg of the trip.
DestinationThe country or region for each travel leg. Determines per diem rates and deduction rules.
DeductionsMeals (breakfast, lunch, dinner) or accommodation provided by the employer reduce the per diem allowance. Deduction fields appear per stop.
AccommodationWhether the traveler arranged their own accommodation or it was provided. Affects the allowance calculation.
Travel stopsIntermediate stops on a multi-destination trip. Each stop has its own departure time, destination, and deductions.
Local date/timePer diem uses local time (LOCAL_DATE_TIME) without timezone β€” times represent the traveler's local time at each stop.
Night qualificationSome 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:

OperationMethodEndpoint
Initialize fieldsGET/v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/fields
Update fieldsPUT/v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/fields
Create expensePOST/v1/expense/expenses?format=fields&organizationId={id}&expenseType=SubsistenceAllowance
Update expensePUT/v1/expense/expenses/{expenseId}?format=fields
Get categoriesGET/v1/expense/me/organizations/{id}/expensetypes/SubsistenceAllowance/categories
Get destinationsGET/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 propertycontrolTypeDescription
idTEXTExpense ID (hidden, system field)
categoryIdLISTPer diem category β€” requiresUpdate: true
verification.commentMULTI_LINE_TEXTUser comment / description
expenseReportIdLISTAssign to an expense report
verification.travelRouteFIELD_GROUPTravel route container with departure, stops, and return
Custom dimension fieldsLISTOrganization-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 propertycontrolTypeDescription
verification.departureDateTimeLOCAL_DATE_TIMEDeparture date and time (local) β€” requiresUpdate: true
verification.destinationLISTDestination country/region β€” requiresUpdate: true

Return fields

The return group defines when the trip ends:

Field propertycontrolTypeDescription
verification.returnDateTimeLOCAL_DATE_TIMEReturn date and time (local) β€” requiresUpdate: true

Deduction fields

Each travel stop (and potentially the overall trip) includes deduction toggles:

Field propertycontrolTypeDescription
verification.deduction.breakfastSWITCHBreakfast provided by employer β€” requiresUpdate: true
verification.deduction.lunchSWITCHLunch provided by employer β€” requiresUpdate: true
verification.deduction.dinnerSWITCHDinner provided by employer β€” requiresUpdate: true
verification.deduction.accommodationLIST or SWITCHAccommodation type/status β€” requiresUpdate: true
πŸ“˜

Deduction fields have requiresUpdate: true because toggling them triggers a server-side recalculation of the per diem allowance. The accommodation field can be either a SWITCH (provided/not provided) or a LIST (type of accommodation) depending on the country's rules.

The LOCAL_DATE_TIME control type

Per 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 confirmationRequired is true, display the confirmation text 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 propertycontrolTypeDescription
verification.travelStops[n].departureDateTimeLOCAL_DATE_TIMEDeparture from this stop β€” requiresUpdate: true
verification.travelStops[n].destinationLISTDestination for this leg β€” requiresUpdate: true
verification.travelStops[n].deduction.breakfastSWITCHBreakfast deduction for this leg
verification.travelStops[n].deduction.lunchSWITCHLunch deduction for this leg
verification.travelStops[n].deduction.dinnerSWITCHDinner deduction for this leg
verification.travelStops[n].deduction.accommodationLIST or SWITCHAccommodation 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

FieldWhat changes on update
categoryIdPer diem rules, visible fields, deduction configuration
verification.departureDateTimeTrip duration, allowance calculation, night qualification
verification.returnDateTimeTrip duration, allowance calculation, night qualification
verification.destinationPer diem rates, deduction rules, accommodation options
verification.deduction.breakfastAllowance recalculation (deduction applied/removed)
verification.deduction.lunchAllowance recalculation (deduction applied/removed)
verification.deduction.dinnerAllowance recalculation (deduction applied/removed)
verification.deduction.accommodationAccommodation allowance recalculation
verification.travelStops[n].departureDateTimePer-leg duration, rate calculation
verification.travelStops[n].destinationPer-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 propertycontrolTypeDescription
verification.totalAllowanceDOUBLETotal per diem allowance after deductions
verification.dailyRateDOUBLEPer diem daily rate for the destination
verification.numberOfDaysDOUBLECalculated trip duration in days
verification.deductionTotalDOUBLETotal 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:

CountryKey behavior
SwedenRates based on trip duration (half-day, full-day, overnight). Night travel qualification rules apply. Reduced rate after 3 months at the same destination.
NorwayDestination-specific rates for domestic travel. Accommodation type (hotel, private, camping) affects the rate. Toll cost data integration available.
DenmarkFixed daily rates with standard meal deduction percentages.
FinlandFull-day and partial-day rates. Meal deductions as fixed amounts rather than percentages.
GermanyTwo-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:

informationTypeRendering
normalNeutral text, no special styling
informationBlue/info banner
warningYellow/orange warning banner
errorRed error banner (e.g., rejection comment)
successGreen 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 codeError codeDescription
400INVALID_JSON_BODYFailed to parse the request body. Verify your JSON payload structure.
400INVALID_JSON_PROPERTY_VALUEMissing or invalid type on verification β€” ensure type is set to SubsistenceAllowance.
400NOT_A_SUBSISTENCE_ALLOWANCE_CATEGORYThe category ID is not valid for per diem expenses. Fetch categories for SubsistenceAllowance.
400DEPARTURE_DATE_REQUIREDDeparture date/time is missing.
400RETURN_DATE_REQUIREDReturn date/time is missing.
400RETURN_BEFORE_DEPARTUREReturn date/time is earlier than departure.
400DESTINATION_REQUIREDNo destination selected for one or more travel legs.
400STOP_DEPARTURE_BEFORE_PREVIOUSA stop's departure time is earlier than the previous stop's departure.
400INVALID_STOP_INDEXThe afterIndex or index parameter in an addStop/removeStop command is out of range.
404EXPENSE_RECORD_NOT_FOUNDThe expense ID does not exist or is not accessible by the current user.
404CATEGORY_NOT_FOUNDThe specified category does not exist in the organization.

Next steps