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"
}'
import requests
import json
url = "https://app3.timelog.com/your-account/api/v1/employee-expense"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN"
}
data = {
"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"
}
response = requests.post(url, headers=headers, json=data)
print(response.json())
const response = await fetch("https://app3.timelog.com/your-account/api/v1/employee-expense", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN"
},
body: JSON.stringify({
"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"
})
});
const data = await response.json();
console.log(data);
package main
import (
"fmt"
"net/http"
"bytes"
"encoding/json"
)
func main() {
data := []byte(`{
"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"
}`)
req, err := http.NewRequest("POST", "https://app3.timelog.com/your-account/api/v1/employee-expense", bytes.NewBuffer(data))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer YOUR_API_TOKEN")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Response Status:", resp.Status)
}
require 'net/http'
require 'json'
uri = URI('https://app3.timelog.com/your-account/api/v1/employee-expense')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request['Authorization'] = 'Bearer YOUR_API_TOKEN'
request.body = '{
"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"
}'
response = http.request(request)
puts response.body
{
"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": [
{}
]
}
{
"error": "Bad Request",
"message": "The request contains invalid parameters or malformed data",
"code": 400,
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
{
"error": "Unauthorized",
"message": "Authentication required. Please provide a valid API token",
"code": 401
}
{
"error": "Forbidden",
"message": "You don't have permission to access this resource",
"code": 403
}
{
"error": "Internal Server Error",
"message": "An unexpected error occurred on the server",
"code": 500,
"requestId": "req_1234567890"
}
/v1/employee-expense
Target server for requests. Edit to use your own host.
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.
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.The media type of the request body
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.
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.
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.
The VAT part of the expense, in the currency CurrencyID. Unit: currency of CurrencyID
Whether the expense is billable to the customer. - false on a fixed-price contract — 31140 (HTTP 500, Code 102). > Not yet documented: default.
Free-text comment. > Not yet documented: constraints → max length.
Exchange rate from CurrencyID to the owner's legal-entity currency. > Not yet documented: constraints → unit.
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.
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.
How the employee paid.
Refers to PaymentMethodRead.PaymentMethodID. Look up valid values with GET /v1/payment-method/{status}.
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.
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.
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.
Request Preview
Response
Response will appear here after sending the request
Authentication
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
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.
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.
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.
The VAT part of the expense, in the currency CurrencyID.
Unit: currency of CurrencyID
Whether the expense is billable to the customer.
- false on a fixed-price contract — 31140 (HTTP 500, Code 102).
Not yet documented: default.
Free-text comment.
Not yet documented: constraints → max length.
Exchange rate from CurrencyID to the owner's legal-entity currency.
Not yet documented: constraints → unit.
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.
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.
How the employee paid.
Refers to PaymentMethodRead.PaymentMethodID. Look up valid values with GET /v1/payment-method/{status}.
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.
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.
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
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.