Skip to content

Request an integration

Tell us which system you want to connect. We review every request weekly and publish a step-by-step guide or build the connection.

Sources and conflicts

How TimeTracking merges raw claims from the app, kiosk, client email, imports, the office and the client portal into one value per day, and how conflicts are detected and resolved.

Last updated: · Markdown version

TimeTracking never overwrites hours. Each input is stored as a claim with a source. For every placement and date, the claims are resolved into one day value.

Sources

Source Created by Who may send it via API
app Employee in the mobile/web app employee token
kiosk Clock-out on a kiosk tablet kiosk endpoints (device token)
mail Client timesheet parsed from an email or upload office credentials; inbound pipeline
import File/system import office credentials (write key)
manual_office Office entry office credentials (write key)
client_portal Client correction via the approval link approval link; office credentials
predicted Suggestion for the employee never counts on its own

Resolution rules

The rules are implemented in resolveDay in @tt/core:

  1. Claims with confidence below 0.85 (minConfidence) never count. Parsed email rows below that threshold go to the inbox instead.
  2. predicted claims never count.
  3. Per source, only the newest claim counts. A resubmission from the same source replaces the earlier one.
  4. No usable claims → the day is empty (minutes: null).
  5. One usable source → resolution: "auto_single_source".
  6. Several sources that agree → resolution: "auto_match". The winner is picked by priority: client_portal > manual_office > mail > import > kiosk > app.
  7. Several sources that disagree → conflict. The day gets minutes: null, has_conflict: true and a conflict record with every candidate. A conflict.created webhook fires.

Sources agree when every pair is within tolerance:

Setting Default Meaning
totalToleranceMin 0 Max difference in daily total minutes
clockToleranceMin 15 Max difference in start or end time (only compared when both claims have times)

Admins change both tolerances in "Instellingen" (Settings).

Example: a conflict, captured from the API

The office records 07:00–15:30 with a 30-minute break (480 min). The client's email says 450 minutes:

curl -s -X POST "$TT/claims" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"placementId":"00000000-0000-4000-8000-0000000000e4","workDate":"2026-12-01","minutes":450,"source":"mail","sourceRef":"docs-capture"}'

The day now has minutes: null and has_conflict: true, with both claim IDs in claim_ids. GET /conflicts?week=2026-11-30 returns:

{
  "data": [
    {
      "id": "c51f9c0d-da32-4edf-86a8-0ba2a86d93a7",
      "dayId": "0ce9b17d-40ab-4542-937a-de57896a7358",
      "workDate": "2026-12-01",
      "personName": "Sanne Janssen",
      "clientName": "Bakkerij Ruijters",
      "candidates": [
        { "source": "manual_office", "claimId": "7d78dbdb-d70c-4960-8796-4cedfa3454e6", "minutes": 480 },
        { "source": "mail", "claimId": "ddce5803-8f75-4ace-9d5c-e0bf9fae1d8a", "minutes": 450 }
      ]
    }
  ]
}

Resolving a conflict

To resolve it, pick the winning claim and add a note:

curl -s -X POST "$TT/conflicts/c51f9c0d-da32-4edf-86a8-0ba2a86d93a7/resolve" \
  -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"claimId":"7d78dbdb-d70c-4960-8796-4cedfa3454e6","note":"Matches the signed paper timesheet"}'
# → {"data":{"resolved":true}}

The day takes the chosen claim's minutes, start, end and break, with resolution: "office_choice". A conflict.resolved webhook fires with {conflictId, claimId}. Resolving the same conflict again returns 409 {"code":"conflict","message":"Conflict already resolved"}. The claimId must belong to the conflict's placement and date, otherwise you get 422.

Other ways a conflict ends:

  • The client enters corrected hours on the approval link. That creates a client_portal claim and sets resolution: "client_correction".
  • A new claim brings the sources back within tolerance.

Conflicted days cannot be approved (POST /days/approve counts them as skipped). The client link does not accept "approved" for them either; the client must enter corrected hours.

Weekly totals from client emails

Some client emails give a weekly total per person, not daily hours. These are compared with the app hours for that week. When they match, the item is stored as matched_week. When they differ, it becomes a week_conflict review item in the inbox with a note like App: 2400 min, opdrachtgever: 2280 min. Weekly totals never create claims.

Reading raw claims

GET /claims?from=2026-12-01&to=2026-12-13&placement_id=…&source=import returns every raw claim with source_ref, confidence and created_at. It is paginated. Use it for audits or to check that your import landed. See the reference.

Designing an integration

  • Send hours with the source that matches reality: import for system-to-system feeds, manual_office for office entries. Put your own record ID in sourceRef (max 200 chars).
  • Send a client-generated UUID as id to make retries safe at claim level. A repeat with the same id returns the existing day and creates nothing. You can also send an Idempotency-Key header (see idempotency).
  • Watch conflict.created to alert a human and conflict.resolved to close the alert. Don't auto-resolve material differences.