Create an employee expense (v2)
Creates an employee expense for the caller or on behalf of another user, and returns it (v2 route).
The v2 route of POST /v1/employee-expense — same body (EmployeeExpenseCreate, UserID and ProjectSubContractID included), same on-behalf rule, same business rules and errors. The response is the created expense in the v2 shape (no Status). Receipts are attached in a second call to the returned EmployeeExpenseID.
Use it to:
- Receipt-capture app booking a receipt line, then uploading the receipt to the returned ID (v2 client)
Not for:
- Mileage —
POST /v1/mileage-registration - Company costs on a project —
POST /v1/project-expense
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; nothing deduplicates. After a lost response, list the owner's expenses for the date (GET /v2/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 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, GET /v1/expense-type/{status}, GET /v1/payment-method/{status}
Next: POST /v2/employee-expense/{employeeExpenseID}/image, POST /v2/employee-expense/{employeeExpenseID}/file, GET /v2/employee-expense/{employeeExpenseID}
See also: POST /v1/employee-expense, POST /v1/project-header/employee-expense
curl -X POST "https://app3.timelog.com/your-account/api/v2/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/v2/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/v2/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/v2/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/v2/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,
"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": [
{}
]
}
{
"Code": 200,
"Message": "Invalid model state error.",
"Details": [
"…"
],
"Parameters": "…",
"Url": "https://app{server-number}.timelog.com/{account}/api/v2/employee-expense"
}
{
"Message": "Authorization has been denied for this request."
}
{
"Code": 103,
"Message": "…",
"Details": [],
"Parameters": "…",
"Url": "https://app{server-number}.timelog.com/{account}/api/v2/employee-expense"
}
{
"Code": 100,
"Message": "Unknown error occur in system, please contact system administrator.",
"Details": [],
"Parameters": "…",
"Url": "https://app{server-number}.timelog.com/{account}/api/v2/employee-expense"
}
/v2/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.
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.The media type of the request body
The date of the expense (receipt date). - 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).
Expense number. Accepted in the body but NOT used. - always — The adapter sets the number to an empty string whatever is sent.
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.
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).
Free-text comment.
Exchange rate from CurrencyID to the owner's legal-entity currency.
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}.
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.
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).
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.
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.
Body
The date of the expense (receipt date).
- 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).
Expense number. Accepted in the body but NOT used.
- always — The adapter sets the number to an empty string whatever is sent.
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.
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).
Free-text comment.
Exchange rate from CurrencyID to the owner's legal-entity currency.
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}.
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.
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).
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.
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) and short-lived download URLs for the attached receipts — but, unlike v1, not the approval status. Optional nested objects (Project, Currency, ExpenseType, PaymentMethod) are filled only on $expand. The TAF self link points to GET /v2/employee-expense/{EmployeeExpenseID}; each entity also carries the expense TAF actions.