Update an employee expense (v2)
Replaces the amounts, date, currency, type, payment method, billability, comment, project AND contract of an unapproved employee expense (v2 route).
A full replacement (PUT), like PUT /v1/employee-expense, with one contract difference: v2 also takes ProjectSubContractID, and it must be sent — an omitted contract is 0 and fails with 37042. Because the contract is part of the body, v2 can move an expense to another contract or another project (send a contract of that project); v1 cannot. The owner cannot be changed. The response body is JSON null.
Use it to:
- Expense app letting an employee correct an expense before approval, including its project / contract
- Receipt-capture integration filling in amount / VAT / currency after OCR of the uploaded receipt
Not for:
- Changing the owner — delete and re-create the expense
- Changing receipts — the attach / delete image and file operations
Rules across fields:
- R1: ProjectSubContractID must be a contract of ProjectID (0 / omitted included → rejected). To change the project, send a contract of the new project.
- R2: ProjectSubContractID must not be a prepaid-services contract. An inactive contract is accepted on update.
- R3: ExpenseTypeID must belong to the owner's legal entity.
- R4: Date must be inside the owner's employment and not in a closed accounting period.
Behaviour:
- Idempotent.
- Same body → same stored values; last write wins (no concurrency token).
- Safe to retry.
- Duplicates: n/a
- Changes are visible to the next read immediately.
Call first: GET /v2/employee-expense/{employeeExpenseID}, GET /v1/contract
Next: GET /v2/employee-expense/{employeeExpenseID}
See also: PUT /v1/employee-expense, DELETE /v2/employee-expense/{employeeExpenseID}
Not yet documented: examples; request → cross field rules → R2.
curl -X PUT "https://app3.timelog.com/your-account/api/v2/employee-expense" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-d '{
"EmployeeExpenseID": 123,
"Date": "2024-12-25T10:00:00Z",
"Amount": 3.14,
"VatAmount": 3.14,
"IsBillable": true,
"Comment": "null",
"ExchangeRate": 3.14,
"CurrencyID": 123,
"ExpenseTypeID": 123,
"PaymentMethodID": 123,
"ProjectID": 123,
"ProjectSubContractID": 123
}'
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 = {
"EmployeeExpenseID": 123,
"Date": "2024-12-25T10:00:00Z",
"Amount": 3.14,
"VatAmount": 3.14,
"IsBillable": true,
"Comment": "null",
"ExchangeRate": 3.14,
"CurrencyID": 123,
"ExpenseTypeID": 123,
"PaymentMethodID": 123,
"ProjectID": 123,
"ProjectSubContractID": 123
}
response = requests.put(url, headers=headers, json=data)
print(response.json())
const response = await fetch("https://app3.timelog.com/your-account/api/v2/employee-expense", {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN"
},
body: JSON.stringify({
"EmployeeExpenseID": 123,
"Date": "2024-12-25T10:00:00Z",
"Amount": 3.14,
"VatAmount": 3.14,
"IsBillable": true,
"Comment": "null",
"ExchangeRate": 3.14,
"CurrencyID": 123,
"ExpenseTypeID": 123,
"PaymentMethodID": 123,
"ProjectID": 123,
"ProjectSubContractID": 123
})
});
const data = await response.json();
console.log(data);
package main
import (
"fmt"
"net/http"
"bytes"
"encoding/json"
)
func main() {
data := []byte(`{
"EmployeeExpenseID": 123,
"Date": "2024-12-25T10:00:00Z",
"Amount": 3.14,
"VatAmount": 3.14,
"IsBillable": true,
"Comment": "null",
"ExchangeRate": 3.14,
"CurrencyID": 123,
"ExpenseTypeID": 123,
"PaymentMethodID": 123,
"ProjectID": 123,
"ProjectSubContractID": 123
}`)
req, err := http.NewRequest("PUT", "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::Put.new(uri)
request['Content-Type'] = 'application/json'
request['Authorization'] = 'Bearer YOUR_API_TOKEN'
request.body = '{
"EmployeeExpenseID": 123,
"Date": "2024-12-25T10:00:00Z",
"Amount": 3.14,
"VatAmount": 3.14,
"IsBillable": true,
"Comment": "null",
"ExchangeRate": 3.14,
"CurrencyID": 123,
"ExpenseTypeID": 123,
"PaymentMethodID": 123,
"ProjectID": 123,
"ProjectSubContractID": 123
}'
response = http.request(request)
puts response.body
"null"
{
"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": "Internal Server Error",
"message": "An unexpected error occurred on the server",
"code": 500,
"requestId": "req_1234567890"
}
/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.
> 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 expense to update (from GET /v2/employee-expense). Identifier of this object, unique per TimeLog site. - ≤ 0 — 400, Code 200. - unknown ID — 500, Code 100 (the expense lookup throws a data-access exception).
The expense (receipt) date. Date only — no time component. - a time component is sent — Truncated to the date (DateTime.Date). Must be inside the owner's employment (38022) and not in a closed accounting period (37699). > Not yet documented: constraints → timezone.
The expense amount, in the currency CurrencyID. Unit: currency of CurrencyID - omitted — Written as 0 (full replacement). NotNull on a non-nullable double never fails: 0 and negative amounts pass.
The VAT part, in the currency CurrencyID. Unit: currency of CurrencyID - omitted — Written as 0 (full replacement).
Whether the expense is billable to the customer. - omitted — Written as false (full replacement). > Not yet documented: behaviour.
Free-text comment. Null: Omitted or null clears the stored comment (full replacement). > Not yet documented: constraints → max length.
Exchange rate from CurrencyID to the owner's legal-entity currency.
A suggested value for a date: GET /v2/exchange-rate (GET /v2/exchange-rate) — the call the TimeLog expense form makes.
> 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}.
The expense type; must belong to the owner's legal entity.
Refers to ExpenseTypeRead.ExpenseTypeID. Look up valid values with GET /v1/expense-type/{status}.
Valid values come from GET /v1/expense-type/{status}.
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.
Refers to ProjectHeaderRead.ProjectID. Look up valid values with GET /v1/project-header/search-for-expense-travel-registration-order-by-recent-registration.
- changed — Accepted when ProjectSubContractID is a contract of the NEW project (otherwise 37042).
- unchanged — Project must not be Completed (38006).
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. Send the expense's current contract to keep it, or another contract of ProjectID to move it.
Refers to ContractRead.ProjectSubContractID. Look up valid values with GET /v1/contract.
- omitted or 0 — Written as 0 (non-nullable int, no validator rule); no contract 0 exists on the project → 500 Code 102 / 37042.
- not a contract of ProjectID — 37042.
- a prepaid-services contract — 31134 (rest.projectmanagement.expense-contract-prepaid).
- an inactive (not Active) contract of ProjectID — The active-status rule is skipped on update.
Requirement is effective, not declared: the C# property is a plain int and the validator has no rule for it.
Valid values come from GET /v1/contract.
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 expense to update (from GET /v2/employee-expense).
Identifier of this object, unique per TimeLog site.
-
≤ 0 — 400, Code 200.
-
unknown ID — 500, Code 100 (the expense lookup throws a data-access exception).
The expense (receipt) date.
Date only — no time component.
- a time component is sent — Truncated to the date (DateTime.Date).
Must be inside the owner's employment (38022) and not in a closed accounting period (37699).
Not yet documented: constraints → timezone.
The expense amount, in the currency CurrencyID.
Unit: currency of CurrencyID
- omitted — Written as 0 (full replacement).
NotNull on a non-nullable double never fails: 0 and negative amounts pass.
The VAT part, in the currency CurrencyID.
Unit: currency of CurrencyID
- omitted — Written as 0 (full replacement).
Whether the expense is billable to the customer.
- omitted — Written as false (full replacement).
Not yet documented: behaviour.
Free-text comment.
Null: Omitted or null clears the stored comment (full replacement).
Not yet documented: constraints → max length.
Exchange rate from CurrencyID to the owner's legal-entity currency.
A suggested value for a date: GET /v2/exchange-rate (GET /v2/exchange-rate) — the call the TimeLog expense form makes.
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}.
The expense type; must belong to the owner's legal entity.
Refers to ExpenseTypeRead.ExpenseTypeID. Look up valid values with GET /v1/expense-type/{status}.
Valid values come from GET /v1/expense-type/{status}.
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.
Refers to ProjectHeaderRead.ProjectID. Look up valid values with GET /v1/project-header/search-for-expense-travel-registration-order-by-recent-registration.
-
changed — Accepted when ProjectSubContractID is a contract of the NEW project (otherwise 37042).
-
unchanged — Project must not be Completed (38006).
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. Send the expense's current contract to keep it, or another contract of ProjectID to move it.
Refers to ContractRead.ProjectSubContractID. Look up valid values with GET /v1/contract.
-
omitted or 0 — Written as 0 (non-nullable int, no validator rule); no contract 0 exists on the project → 500 Code 102 / 37042.
-
not a contract of ProjectID — 37042.
-
a prepaid-services contract — 31134 (rest.projectmanagement.expense-contract-prepaid).
-
an inactive (not Active) contract of ProjectID — The active-status rule is skipped on update.
Requirement is effective, not declared: the C# property is a plain int and the validator has no rule for it.
Valid values come from GET /v1/contract.
Responses
*Success without content: HTTP 200 with the literal JSON body null. Body is the JSON literal null.
Order: none
Empty result: none*