Create a project expense
Books a company cost (supplier invoice, purchase, subcontractor) on a project contract, with its cost and the price to re-invoice, and returns it.
Creates a PROJECT expense: a cost the company paid on a project — typically one line of a supplier invoice — as opposed to an employee's out-of-pocket expense. You send the cost INCLUDING VAT and the VAT in the purchase currency, the project and contract (no default contract is chosen for you), the expense type and payment method, and either a sales price in project currency or a profit ratio (neither = sold at cost). TimeLog stores the net cost in purchase, project and system currency, derives the sales price, and — when IsBillable is true — makes it available for invoicing on the contract. The expense is always registered as the CALLER's (no UserID); the response is the stored expense (ProjectExpenseRead). Attach the supplier-invoice PDF in a second call.
Use it to:
- BPP / purchase-invoice automation (e.g. a document-capture or AP add-on for Business Central) posting each project-related supplier-invoice line as a project cost, then attaching the invoice PDF
- ERP purchase-order receipt booked as a billable project cost with a mark-up (ProfitRatio) or a fixed resale price (SalesPriceAmountProjectCurrency)
- Re-invoicing a third-party cost to the customer at a negotiated price (amount 0 + sales price = pure resale line)
Not for:
- An employee's own outlay to be reimbursed —
POST /v1/employee-expense - Mileage —
POST /v1/mileage-registration - Pre-checking without saving —
POST /v1/project-expense/validate-project-expense(which skips the licence and exchange-rate checks)
Rules across fields:
- R1: Send at most one of ProfitRatio and SalesPriceAmountProjectCurrency.
- R2: ProjectSubContractID must be an open contract of ProjectID.
- R3: IsBillable must be true when the contract is fixed-price.
- R4: Without ProjectExpenseExchangeRate, the currency matrix must hold an intercompany rate for PurchaseDate (purchase currency → caller's and project's legal-entity currency).
Behaviour:
- Not idempotent.
- Not safe to retry blindly. Each call creates a new expense with a new GUID; ExternalID is not deduplicated. After a lost response, list the project's expenses and match ExternalID before retrying.
- Side effect: Recalculates the project's estimation figures for the expense (TSP_EstimationRecalculate_Expense).
- Side effect: When the payment method is the employee's own money, the cost is also recorded as a reimbursement to the caller.
- On failure: No transaction wraps the insert and the follow-up writes; a failure after the insert (currency data, read-back) returns 500 while the expense row stays.
- Changes are visible to the next read immediately.
Call first: GET /v1/contract, GET /v1/expense-type/all/list, GET /v1/currency/{status}, POST /v1/project-expense/validate-project-expense
Next: GET /v1/project-expense/{expenseID}
See also: POST /v1/employee-expense
Not yet documented: errors; examples.
curl -X POST "https://app3.timelog.com/your-account/api/v1/project-expense" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-d '{
"ProjectID": 123,
"ProjectSubContractID": 123,
"PurchaseDate": "2024-12-25T10:00:00Z",
"PaymentMethodID": 123,
"ExpenseTypeID": 123,
"AmountIncludingVatExpenseCurrency": 3.14,
"VatAmountExpenseCurrency": 3.14,
"ExpenseCurrencyISO": "example_string",
"ProjectExpenseExchangeRate": "null",
"IsBillable": true,
"SalesPriceAmountProjectCurrency": "null",
"ProfitRatio": "null",
"Comment": "null",
"ExpenseNo": "null",
"SupplierID": "null",
"SupplierInvoiceNo": "null",
"ExternalID": "null"
}'
import requests
import json
url = "https://app3.timelog.com/your-account/api/v1/project-expense"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN"
}
data = {
"ProjectID": 123,
"ProjectSubContractID": 123,
"PurchaseDate": "2024-12-25T10:00:00Z",
"PaymentMethodID": 123,
"ExpenseTypeID": 123,
"AmountIncludingVatExpenseCurrency": 3.14,
"VatAmountExpenseCurrency": 3.14,
"ExpenseCurrencyISO": "example_string",
"ProjectExpenseExchangeRate": "null",
"IsBillable": true,
"SalesPriceAmountProjectCurrency": "null",
"ProfitRatio": "null",
"Comment": "null",
"ExpenseNo": "null",
"SupplierID": "null",
"SupplierInvoiceNo": "null",
"ExternalID": "null"
}
response = requests.post(url, headers=headers, json=data)
print(response.json())
const response = await fetch("https://app3.timelog.com/your-account/api/v1/project-expense", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN"
},
body: JSON.stringify({
"ProjectID": 123,
"ProjectSubContractID": 123,
"PurchaseDate": "2024-12-25T10:00:00Z",
"PaymentMethodID": 123,
"ExpenseTypeID": 123,
"AmountIncludingVatExpenseCurrency": 3.14,
"VatAmountExpenseCurrency": 3.14,
"ExpenseCurrencyISO": "example_string",
"ProjectExpenseExchangeRate": "null",
"IsBillable": true,
"SalesPriceAmountProjectCurrency": "null",
"ProfitRatio": "null",
"Comment": "null",
"ExpenseNo": "null",
"SupplierID": "null",
"SupplierInvoiceNo": "null",
"ExternalID": "null"
})
});
const data = await response.json();
console.log(data);
package main
import (
"fmt"
"net/http"
"bytes"
"encoding/json"
)
func main() {
data := []byte(`{
"ProjectID": 123,
"ProjectSubContractID": 123,
"PurchaseDate": "2024-12-25T10:00:00Z",
"PaymentMethodID": 123,
"ExpenseTypeID": 123,
"AmountIncludingVatExpenseCurrency": 3.14,
"VatAmountExpenseCurrency": 3.14,
"ExpenseCurrencyISO": "example_string",
"ProjectExpenseExchangeRate": "null",
"IsBillable": true,
"SalesPriceAmountProjectCurrency": "null",
"ProfitRatio": "null",
"Comment": "null",
"ExpenseNo": "null",
"SupplierID": "null",
"SupplierInvoiceNo": "null",
"ExternalID": "null"
}`)
req, err := http.NewRequest("POST", "https://app3.timelog.com/your-account/api/v1/project-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/project-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 = '{
"ProjectID": 123,
"ProjectSubContractID": 123,
"PurchaseDate": "2024-12-25T10:00:00Z",
"PaymentMethodID": 123,
"ExpenseTypeID": 123,
"AmountIncludingVatExpenseCurrency": 3.14,
"VatAmountExpenseCurrency": 3.14,
"ExpenseCurrencyISO": "example_string",
"ProjectExpenseExchangeRate": "null",
"IsBillable": true,
"SalesPriceAmountProjectCurrency": "null",
"ProfitRatio": "null",
"Comment": "null",
"ExpenseNo": "null",
"SupplierID": "null",
"SupplierInvoiceNo": "null",
"ExternalID": "null"
}'
response = http.request(request)
puts response.body
{
"Properties": {
"ProjectExpenseID": 123,
"ExpenseNo": "example_string",
"ProjectID": 123,
"EmployeeID": 123,
"PurchaseDate": "2024-12-25T10:00:00Z",
"InvoiceNo": "example_string",
"CustomerID": 123,
"ExpenseCurrencyISO": "example_string",
"AmountExpenseCurrency": 3.14,
"VatExpenseCurrency": 3.14,
"ExpenseDirectProjectExchangeRate": 3.14,
"ProjectCurrencyISO": "example_string",
"ExpenseCostProjectCurrency": 3.14,
"ExpenseCostSystemCurrency": 3.14,
"ProfitRatio": 3.14,
"SalesPriceProjectCurrency": 29.99,
"SalesPriceSystemCurrency": 29.99,
"SalesPriceProjectExchangeRate": 29.99,
"InvoicedAmountSystemCurrency": 3.14,
"InvoicedAmountProjectCurrency": 3.14,
"Comment": "example_string",
"PaymentMethodID": 123,
"ExpenseTypeID": 123,
"IsBillable": true,
"IsApproved": true,
"ApprovedBy": 42,
"InvoiceLineID": 123,
"UnitType": 42,
"ProductNo": "example_string",
"DisbursementGuid": "123e4567-e89b-12d3-a456-426614174000",
"ExternalID": "example_string",
"ProjectSubContractID": 123,
"InstallmentID": 123,
"LegalEntityID": 123,
"FileUrls": [
"null"
],
"IsFileUrlLoaded": true,
"HasReceiptPictures": true
},
"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
}
{
"Code": 100,
"Details": [],
"Message": "Unknown error occur in system, please contact system administrator.",
"Parameters": "createModel: null",
"Url": "https://app{server-number}.timelog.com/{account}/api/v1/project-expense"
}
/v1/project-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 project the cost is booked on. Must exist (37014) and be active (37689).
Refers to ProjectRead.ProjectID.
- ≤ 0 — 400 Code 200 "'Project ID' must be greater than '0'."
- unknown ID — 37014 (adapter, before any other business rule).
- inactive project — 37689.
> Not yet documented: values → lookup op.
The contract of ProjectID the cost is booked on. Required — there is NO default-contract resolution for project expenses.
Refers to ContractRead.ContractID. Look up valid values with GET /v1/contract.
- ≤ 0 — 400 Code 200.
- not a contract of ProjectID — 37042.
- contract closed — 37806.
- fixed-price contract and IsBillable false — 31140.
Valid values come from GET /v1/contract.
The purchase / supplier-invoice date. Date only — no time component. - a time component is sent — NOT truncated: stored in a SMALLDATETIME (minute precision). Send a date without time — the per-day expense totals match on the exact value.
How the cost was paid. A payment method flagged as the employee's own money makes the expense reimbursable to the caller.
Refers to PaymentMethodRead.PaymentMethodID.
- ≤ 0 — 400 Code 200.
- unknown ID — 37018.
> Not yet documented: values → lookup op.
The expense type (cost category). Drives the product number on the invoice line when product numbers are active.
Refers to ExpenseTypeRead.ExpenseTypeID. Look up valid values with GET /v1/expense-type/all/list.
- ≤ 0 — 400 Code 200.
- unknown ID — 37019.
- type of another legal entity than the project's — Accepted — the legal-entity rule sets violation 37998 but returns true, so it is never raised.
Valid values come from GET /v1/expense-type/all/list.
The cost INCLUDING VAT, in the purchase currency ExpenseCurrencyISO. The net cost is this minus VatAmountExpenseCurrency. Unit: currency ExpenseCurrencyISO (purchase currency), VAT included - more decimals than the account's invoice setting — 400 "Number of decimals are wrong on AmountIncludingVatExpenseCurrency…" - 0 — A zero-cost line: the sales price is taken from SalesPriceAmountProjectCurrency as is (pure re-invoicing). No sign rule: negative amounts (credit notes) are not rejected by the API.
The VAT part of AmountIncludingVatExpenseCurrency, in the purchase currency. Unit: currency ExpenseCurrencyISO Not checked against the amount or the expense type's VAT %; VAT is never invoiced (cost and sales price are net).
ISO code of the purchase currency (e.g. "DKK", "EUR") — a currency abbreviation known to the account.
Refers to CurrencyRead.CurrencyABB. Look up valid values with GET /v1/currency/{status}.
- empty — 400 Code 200.
- unknown abbreviation — 30020 "Unknown Currency Abbreviation" (500 Code 102).
Valid values come from GET /v1/currency/{status}.
Rate from the purchase currency to the CALLER's legal-entity currency, per 100 units (100 = same value; 745.5 = 100 EUR cost 745.50 DKK). Stored as the expense's base-reference rate and used for the employee-side cost and reimbursement amounts. Null: Omitted / null → looked up in the currency matrix (intercompany exchange rate) for PurchaseDate; none found → 400. Unit: caller legal-entity currency per 100 units of ExpenseCurrencyISO - omitted and no currency-matrix rate exists for the date — 400 "No exchange rate found for the expense date between expense currency and employee legal entity currency." - omitted — Matrix rate (create only — the validate route does not look it up). - omitted and SalesPriceAmountProjectCurrency sent — The profit ratio is computed with a rate of 0 → stored as 0. The project-side conversion (cost and sales price in project currency, project legal-entity cost) does NOT use this value: it uses the account's live / matrix rates for the date.
Whether the cost is re-invoiced to the customer. When false the sales price in project currency is stored as 0. - false on a fixed-price contract — 31140. - omitted — false (non-nullable bool) — the expense is NOT billable unless you send true.
The price to invoice the customer, in the PROJECT's currency, net of VAT. Alternative to ProfitRatio. Null: Null (and ProfitRatio null) → sales price = net cost converted to project currency (no mark-up). Unit: project currency, VAT excluded - sent together with ProfitRatio — 400 "SalesPriceAmountCurrency must be null when ProfitRatio is added." - sent, amount ≠ 0 — ProfitRatio is derived from it ((sales − cost) / cost × 100, in system currency) — only when ProjectExpenseExchangeRate is also sent. - 0 — Treated as not sent: recomputed from cost × (1 + ProfitRatio / 100).
Mark-up in percent on the net cost: sales price = cost × (1 + ProfitRatio / 100). Alternative to SalesPriceAmountProjectCurrency. Null: Null → 0 % (the adapter always sends a ratio, so the account's default ExpenseProfitPercent is NOT applied to API-created project expenses). Unit: percent - sent together with SalesPriceAmountProjectCurrency — 400.
Free-text description of the cost; also used in the invoice line text.
The expense number. - ExpenseNo feature off, or the series auto-generates — Adapter drops the value. - ExpenseNumberSeries on — Next number from the series. > Not yet documented: behaviour.
The supplier — a CRM customer whose status is shown in suppliers. Returned as ProjectExpenseRead.CustomerID.
Refers to CustomerRead.CustomerID. Look up valid values with GET /v1/customer.
Null: Null or 0 = no supplier.
- unknown customer — 37021.
- customer without a supplier status — 37940.
Valid values come from GET /v1/customer.
The supplier's invoice number. Returned as ProjectExpenseRead.InvoiceNo. - longer than 50 characters — Silently cut to 50.
Your system's identifier for this cost. Stored and returned; not unique, not used for lookups. Key from your own system. Uniqueness is not enforced. - longer than 50 characters — The insert fails → 500 Code 100. Retries are NOT deduplicated on ExternalID — check before re-sending a create whose response was lost.
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 project the cost is booked on. Must exist (37014) and be active (37689).
Refers to ProjectRead.ProjectID.
-
≤ 0 — 400 Code 200 "'Project ID' must be greater than '0'."
-
unknown ID — 37014 (adapter, before any other business rule).
-
inactive project — 37689.
Not yet documented: values → lookup op.
The contract of ProjectID the cost is booked on. Required — there is NO default-contract resolution for project expenses.
Refers to ContractRead.ContractID. Look up valid values with GET /v1/contract.
-
≤ 0 — 400 Code 200.
-
not a contract of ProjectID — 37042.
-
contract closed — 37806.
-
fixed-price contract and IsBillable false — 31140.
Valid values come from GET /v1/contract.
The purchase / supplier-invoice date.
Date only — no time component.
- a time component is sent — NOT truncated: stored in a SMALLDATETIME (minute precision). Send a date without time — the per-day expense totals match on the exact value.
How the cost was paid. A payment method flagged as the employee's own money makes the expense reimbursable to the caller.
Refers to PaymentMethodRead.PaymentMethodID.
-
≤ 0 — 400 Code 200.
-
unknown ID — 37018.
Not yet documented: values → lookup op.
The expense type (cost category). Drives the product number on the invoice line when product numbers are active.
Refers to ExpenseTypeRead.ExpenseTypeID. Look up valid values with GET /v1/expense-type/all/list.
-
≤ 0 — 400 Code 200.
-
unknown ID — 37019.
-
type of another legal entity than the project's — Accepted — the legal-entity rule sets violation 37998 but returns true, so it is never raised.
Valid values come from GET /v1/expense-type/all/list.
The cost INCLUDING VAT, in the purchase currency ExpenseCurrencyISO. The net cost is this minus VatAmountExpenseCurrency.
Unit: currency ExpenseCurrencyISO (purchase currency), VAT included
-
more decimals than the account's invoice setting — 400 "Number of decimals are wrong on AmountIncludingVatExpenseCurrency…"
-
0 — A zero-cost line: the sales price is taken from SalesPriceAmountProjectCurrency as is (pure re-invoicing).
No sign rule: negative amounts (credit notes) are not rejected by the API.
The VAT part of AmountIncludingVatExpenseCurrency, in the purchase currency.
Unit: currency ExpenseCurrencyISO
Not checked against the amount or the expense type's VAT %; VAT is never invoiced (cost and sales price are net).
ISO code of the purchase currency (e.g. "DKK", "EUR") — a currency abbreviation known to the account.
Refers to CurrencyRead.CurrencyABB. Look up valid values with GET /v1/currency/{status}.
-
empty — 400 Code 200.
-
unknown abbreviation — 30020 "Unknown Currency Abbreviation" (500 Code 102).
Valid values come from GET /v1/currency/{status}.
Rate from the purchase currency to the CALLER's legal-entity currency, per 100 units (100 = same value; 745.5 = 100 EUR cost 745.50 DKK). Stored as the expense's base-reference rate and used for the employee-side cost and reimbursement amounts.
Null: Omitted / null → looked up in the currency matrix (intercompany exchange rate) for PurchaseDate; none found → 400.
Unit: caller legal-entity currency per 100 units of ExpenseCurrencyISO
-
omitted and no currency-matrix rate exists for the date — 400 "No exchange rate found for the expense date between expense currency and employee legal entity currency."
-
omitted — Matrix rate (create only — the validate route does not look it up).
-
omitted and SalesPriceAmountProjectCurrency sent — The profit ratio is computed with a rate of 0 → stored as 0.
The project-side conversion (cost and sales price in project currency, project legal-entity cost) does NOT use this value: it uses the account's live / matrix rates for the date.
Whether the cost is re-invoiced to the customer. When false the sales price in project currency is stored as 0.
-
false on a fixed-price contract — 31140.
-
omitted — false (non-nullable bool) — the expense is NOT billable unless you send true.
The price to invoice the customer, in the PROJECT's currency, net of VAT. Alternative to ProfitRatio.
Null: Null (and ProfitRatio null) → sales price = net cost converted to project currency (no mark-up).
Unit: project currency, VAT excluded
-
sent together with ProfitRatio — 400 "SalesPriceAmountCurrency must be null when ProfitRatio is added."
-
sent, amount ≠ 0 — ProfitRatio is derived from it ((sales − cost) / cost × 100, in system currency) — only when ProjectExpenseExchangeRate is also sent.
-
0 — Treated as not sent: recomputed from cost × (1 + ProfitRatio / 100).
Mark-up in percent on the net cost: sales price = cost × (1 + ProfitRatio / 100). Alternative to SalesPriceAmountProjectCurrency.
Null: Null → 0 % (the adapter always sends a ratio, so the account's default ExpenseProfitPercent is NOT applied to API-created project expenses).
Unit: percent
- sent together with SalesPriceAmountProjectCurrency — 400.
Free-text description of the cost; also used in the invoice line text.
The expense number.
-
ExpenseNo feature off, or the series auto-generates — Adapter drops the value.
-
ExpenseNumberSeries on — Next number from the series.
Not yet documented: behaviour.
The supplier — a CRM customer whose status is shown in suppliers. Returned as ProjectExpenseRead.CustomerID.
Refers to CustomerRead.CustomerID. Look up valid values with GET /v1/customer.
Null: Null or 0 = no supplier.
-
unknown customer — 37021.
-
customer without a supplier status — 37940.
Valid values come from GET /v1/customer.
The supplier's invoice number. Returned as ProjectExpenseRead.InvoiceNo.
- longer than 50 characters — Silently cut to 50.
Your system's identifier for this cost. Stored and returned; not unique, not used for lookups.
Key from your own system. Uniqueness is not enforced.
- longer than 50 characters — The insert fails → 500 Code 100.
Retries are NOT deduplicated on ExternalID — check before re-sending a create whose response was lost.
Responses
A cost the company incurred on a project (supplier invoice, purchase, subcontractor), with its cost in three currencies, its sales price to the customer, the profit ratio between them, approval and invoicing state, and — on GET by ID only — short-lived download URLs for attached PDFs. Costs and sales prices are NET of VAT; only AmountExpenseCurrency includes VAT. The TAF self link points to GET /v1/project-expense/{ProjectExpenseID}.