ExpensesCreate an employee expense

Create an employee expense

Creates an employee expense (out-of-pocket cost to be reimbursed) for the caller or on behalf of another user, and returns it.

Registers an expense an employee paid — amount and VAT in the receipt currency, the exchange rate to the employee's legal-entity currency, expense type, payment method — on a project, optionally billable to its customer. The contract is the one you send, or the project's default expense contract. The response is the stored expense (EmployeeExpenseRead) with its new EmployeeExpenseID and assigned Number. Receipts are attached in a second call (POST …/{id}/image or …/{id}/file), so a receipt-capture integration is always create → attach.

Use it to:

  • Expense or receipt-capture app (e.g. a scan-and-submit flow) booking a receipt line, then uploading the receipt image/PDF to the returned ID

Rules across fields:

  • R1: ProjectSubContractID, when > 0, must belong to ProjectID, be Active and not be prepaid services.
  • R2: ExpenseTypeID must belong to the owner's (UserID or caller) legal entity.
  • R3: IsBillable must be true when the resolved contract is fixed-price.
  • R4: Date must be inside the owner's employment and not in a closed accounting period.

Behaviour:

  • Not idempotent.
  • Not safe to retry blindly. Each call creates a new expense with a new GUID; nothing deduplicates. After a lost response, list the owner's expenses for the date (GET /v1/employee-expense?userID=&startDate=&endDate=) before retrying.
  • Duplicates: Not deduplicated.
  • Side effect: Creates the employee expense with the conversion rates to the project's and the owner's legal-entity currencies.
  • On failure: No transaction wraps the insert and the follow-up writes. A failure after the insert — e.g. in the currency-data notification or the read-back — returns 500 while the expense row stays.
  • Changes are visible to the next read immediately.

Call first: GET /v1/project-header/search-for-expense-travel-registration-order-by-recent-registration, GET /v1/currency/{status}, GET /v1/user

Next: POST /v1/employee-expense/{employeeExpenseID}/image, POST /v1/employee-expense/{employeeExpenseID}/file, GET /v1/employee-expense/{employeeExpenseID}

See also: POST /v1/project-header/employee-expense

Not yet documented: examples.

curl -X POST "https://app3.timelog.com/your-account/api/v1/employee-expense" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -d '{
  "Date": "2024-12-25T10:00:00Z",
  "Number": "example_string",
  "Amount": 3.14,
  "VatAmount": 3.14,
  "IsBillable": true,
  "Comment": "example_string",
  "ExchangeRate": 3.14,
  "CurrencyID": 123,
  "ExpenseTypeID": 123,
  "PaymentMethodID": 123,
  "ProjectID": 123,
  "ProjectSubContractID": 123,
  "UserID": "null"
}'
{
  "Properties": {
    "EmployeeExpenseID": 123,
    "Date": "2024-12-25T10:00:00Z",
    "Amount": 3.14,
    "VatAmount": 3.14,
    "IsBillable": true,
    "Number": "example_string",
    "Comment": "null",
    "ExchangeRate": 3.14,
    "CurrencyID": 123,
    "EmployeeUserID": 123,
    "ExpenseTypeID": 123,
    "PaymentMethodID": 123,
    "ProjectID": 123,
    "ProjectSubContractID": 123,
    "FileUrls": [
      "null"
    ],
    "IsFileUrlLoaded": true,
    "ImageUrls": [
      "null"
    ],
    "IsImageUrlLoaded": true,
    "HasReceiptPictures": true,
    "Status": "Pending",
    "Project": {
      "ProjectID": 123,
      "ID": "123e4567-e89b-12d3-a456-426614174000",
      "Name": "John Doe",
      "No": "example_string",
      "ExpenseIsBillable": "null",
      "CustomerID": 123,
      "Customer": {
        "CustomerID": 123,
        "ID": "123e4567-e89b-12d3-a456-426614174000",
        "Name": "John Doe",
        "No": "example_string",
        "DefaultMileageDistance": 25,
        "CustomerStatusID": 123,
        "VATNo": "example_string",
        "OrganizationNo": "example_string"
      },
      "MainContractId": 123
    },
    "Currency": {
      "CurrencyID": 123,
      "CurrencyABB": "example_string",
      "DescriptiveName": "John Doe",
      "CurrentRate": 3.14,
      "IsActive": true
    },
    "ExpenseType": {
      "ExpenseTypeID": 123,
      "ExpenseTypeName": "John Doe",
      "ShowInTravel": true,
      "AccountNo": "example_string",
      "VAT": 3.14,
      "ProductNo": "example_string",
      "SalaryCode": "example_string",
      "IsActive": true,
      "IsEquipment": true,
      "CostPrice": 29.99,
      "SalesPrice": 29.99,
      "EquipmentText": "example_string"
    },
    "PaymentMethod": {
      "PaymentMethodID": 123,
      "Name": "John Doe",
      "IsPaidByEmployee": true,
      "IsDefaultForEmployeeExpense": true,
      "IsDefaultForProjectExpense": true,
      "IsActive": true,
      "IsDefaultForImport": true,
      "IsPersonalCreditCard": true,
      "CreditCardEmployeeUserID": "null"
    }
  },
  "Links": [
    {
      "Href": "example_string",
      "Rel": "example_string"
    }
  ],
  "Actions": [
    {}
  ]
}
POST
/v1/employee-expense
POST
Base URLstring

Target server for requests. Edit to use your own host.

Bearer Token
Bearer Tokenstring
Required

OAuth 2.0 bearer token, sent as Authorization: Bearer <token>. JWT and reference tokens are both accepted. The token must carry the required scope (default tlp). The token's sub claim is the acting user. A token WITHOUT sub (client-credentials) acts as the system service user. If the sub user cannot log in (inactive, locked, deleted), the request is rejected with 401 — it is no longer let through without a user. > Not yet documented: how to obtain a token. > Not yet documented: token lifetime.

OAuth 2.0 bearer token, sent as Authorization: Bearer <token>. JWT and reference tokens are both accepted. The token must carry the required scope (default tlp). The token's sub claim is the acting user. A token WITHOUT sub (client-credentials) acts as the system service user. If the sub user cannot log in (inactive, locked, deleted), the request is rejected with 401 — it is no longer let through without a user. > Not yet documented: how to obtain a token. > Not yet documented: token lifetime.
Content-Typestring
Required

The media type of the request body

Options: application/json
Datestring

The date of the expense (receipt date). Time zone: not yet documented. - a time component is sent — Truncated to the date (DateTime.Date) by the adapter. Must be inside the owner's employment (38022) and not in a closed accounting period (37699). > Not yet documented: constraints → timezone.

Format: date-time
Numberstring

Expense number. Accepted in the body but NOT used. - always — The adapter sets the number to an empty string whatever is sent. > Not yet documented: behaviour.

Amountnumber

The expense amount, in the currency CurrencyID. Unit: currency of CurrencyID The validator's NotNull rule cannot fail on a non-nullable number: 0 and negative amounts are accepted. > Not yet documented: description.

Format: double
VatAmountnumber

The VAT part of the expense, in the currency CurrencyID. Unit: currency of CurrencyID

Format: double
IsBillableboolean

Whether the expense is billable to the customer. - false on a fixed-price contract — 31140 (HTTP 500, Code 102). > Not yet documented: default.

Commentstring

Free-text comment. > Not yet documented: constraints → max length.

Max length: 255
ExchangeRatenumber

Exchange rate from CurrencyID to the owner's legal-entity currency. > Not yet documented: constraints → unit.

Format: double
CurrencyIDinteger

Currency of Amount and VatAmount. Refers to CurrencyRead.CurrencyID. Look up valid values with GET /v1/currency/{status}. Valid values come from GET /v1/currency/{status}. > Not yet documented: behaviour.

Format: int32
ExpenseTypeIDinteger

The expense type. Intended to belong to the owner's legal entity, but that rule is not enforced. Refers to ExpenseTypeRead.ExpenseTypeID. - unknown ID — 37019. - type of another legal entity than the owner's — Accepted — ExpenseTypeIsValid sets 37998 but returns true, so the violation is never added. > Not yet documented: values → lookup op.

Format: int32
PaymentMethodIDinteger

How the employee paid. Refers to PaymentMethodRead.PaymentMethodID. Look up valid values with GET /v1/payment-method/{status}.

Format: int32
ProjectIDinteger

The project the expense is booked on. Must exist and be active. Refers to ProjectHeaderRead.ProjectID. Look up valid values with GET /v1/project-header/search-for-expense-travel-registration-order-by-recent-registration. Valid values come from GET /v1/project-header/search-for-expense-travel-registration-order-by-recent-registration.

Format: int32
ProjectSubContractIDinteger

The project contract the expense is booked on. Refers to ContractRead.ProjectSubContractID. - omitted or 0 — The project's default-for-expenses contract, else its main contract. If that contract is prepaid services the value stays 0 and the save fails a contract rule. - > 0 — Must be a contract of ProjectID (37042), Active (31132) and not prepaid services (31134). > Not yet documented: values → lookup op. > Not yet documented: behaviour.

Format: int32
UserIDintegernull

The user the expense is registered for. Omit (or send your own UserID) to register for yourself. Refers to UserRead.UserID. Default: the caller - ≤ 0 — 400, Code 200. > Not yet documented: values → lookup op.

Format: int32
Request Preview
Response

Response will appear here after sending the request

Authentication

header
Authorizationstring
Required

Bearer token. OAuth 2.0 bearer token, sent as Authorization: Bearer <token>. JWT and reference tokens are both accepted. The token must carry the required scope (default tlp).

The token's sub claim is the acting user. A token WITHOUT sub (client-credentials) acts as the system service user. If the sub user cannot log in (inactive, locked, deleted), the request is rejected with 401 — it is no longer let through without a user.

Not yet documented: how to obtain a token.

Not yet documented: token lifetime.

Body

application/json
Datestring

The date of the expense (receipt date).

Time zone: not yet documented.

  • a time component is sent — Truncated to the date (DateTime.Date) by the adapter.

Must be inside the owner's employment (38022) and not in a closed accounting period (37699).

Not yet documented: constraints → timezone.

Numberstring

Expense number. Accepted in the body but NOT used.

  • always — The adapter sets the number to an empty string whatever is sent.

Not yet documented: behaviour.

Amountnumber

The expense amount, in the currency CurrencyID.

Unit: currency of CurrencyID

The validator's NotNull rule cannot fail on a non-nullable number: 0 and negative amounts are accepted.

Not yet documented: description.

VatAmountnumber

The VAT part of the expense, in the currency CurrencyID.

Unit: currency of CurrencyID

IsBillableboolean

Whether the expense is billable to the customer.

  • false on a fixed-price contract — 31140 (HTTP 500, Code 102).

Not yet documented: default.

Commentstring

Free-text comment.

Not yet documented: constraints → max length.

ExchangeRatenumber

Exchange rate from CurrencyID to the owner's legal-entity currency.

Not yet documented: constraints → unit.

CurrencyIDinteger

Currency of Amount and VatAmount.

Refers to CurrencyRead.CurrencyID. Look up valid values with GET /v1/currency/{status}.

Valid values come from GET /v1/currency/{status}.

Not yet documented: behaviour.

ExpenseTypeIDinteger

The expense type. Intended to belong to the owner's legal entity, but that rule is not enforced.

Refers to ExpenseTypeRead.ExpenseTypeID.

  • unknown ID — 37019.

  • type of another legal entity than the owner's — Accepted — ExpenseTypeIsValid sets 37998 but returns true, so the violation is never added.

Not yet documented: values → lookup op.

PaymentMethodIDinteger

How the employee paid.

Refers to PaymentMethodRead.PaymentMethodID. Look up valid values with GET /v1/payment-method/{status}.

ProjectIDinteger

The project the expense is booked on. Must exist and be active.

Refers to ProjectHeaderRead.ProjectID. Look up valid values with GET /v1/project-header/search-for-expense-travel-registration-order-by-recent-registration.

Valid values come from GET /v1/project-header/search-for-expense-travel-registration-order-by-recent-registration.

ProjectSubContractIDinteger

The project contract the expense is booked on.

Refers to ContractRead.ProjectSubContractID.

  • omitted or 0 — The project's default-for-expenses contract, else its main contract. If that contract is prepaid services the value stays 0 and the save fails a contract rule.

  • 0 — Must be a contract of ProjectID (37042), Active (31132) and not prepaid services (31134).

Not yet documented: values → lookup op.

Not yet documented: behaviour.

UserIDinteger,null

The user the expense is registered for. Omit (or send your own UserID) to register for yourself.

Refers to UserRead.UserID.

Default: the caller

  • ≤ 0 — 400, Code 200.

Not yet documented: values → lookup op.

Responses

Propertiesobject

An employee expense (out-of-pocket cost the employee wants reimbursed) as stored: amount and VAT in the expense's own currency, the exchange rate used, project / contract / expense type / payment method, the owner (EmployeeUserID), approval status, and short-lived download URLs for the attached receipts. Optional nested objects (Project, Currency, ExpenseType, PaymentMethod) are filled only on $expand. The TAF self link points to GET /v1/employee-expense/{EmployeeExpenseID}; each entity also carries the expense TAF actions.

Linksarray
Actionsobject[]