ExpensesValidate a project expense

Validate a project expense

Dry-runs the business rules of project-expense creation on a body, without saving anything.

Takes the same body as POST /v1/project-expense and checks input validation, project existence, currency code, and the expense business rules (expense type, active project, contract on project and not closed, fixed-price → billable, supplier, payment method, closed accounting period of the caller). Answers 200 with a literal null when they pass.

Use it to:

  • BPP / AP automation pre-checking each supplier-invoice line before posting, to route failures to a human queue
  • Form validation in a client before the user confirms

Not for:

  • Guaranteeing that create will succeed (licence, exchange rate and length errors are only detected by create)

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.

Behaviour:

  • Idempotent.
  • Safe to retry.
  • Changes are visible to the next read immediately.

Call first: GET /v1/contract, GET /v1/expense-type/all/list

Next: POST /v1/project-expense

Not yet documented: examples.

curl -X POST "https://app3.timelog.com/your-account/api/v1/project-expense/validate-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"
}'
"null"
POST
/v1/project-expense/validate-project-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
ProjectIDinteger
Required

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.

Format: int32
ProjectSubContractIDinteger
Required

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.

Format: int32
PurchaseDatestring
Required

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.

Format: date-time
PaymentMethodIDinteger
Required

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.

Format: int32
ExpenseTypeIDinteger
Required

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.

Format: int32
AmountIncludingVatExpenseCurrencynumber

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.

Format: double
VatAmountExpenseCurrencynumber

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).

Format: double
ExpenseCurrencyISOstring
Required

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}.

ProjectExpenseExchangeRatenumbernull

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.

Format: double
IsBillableboolean

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.

SalesPriceAmountProjectCurrencynumbernull

The price to invoice the customer, in the PROJECT's currency, net of VAT. Alternative to ProfitRatio. Conditional. Must be null when ProfitRatio is sent. 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).

Format: double
ProfitRationumbernull

Mark-up in percent on the net cost: sales price = cost × (1 + ProfitRatio / 100). Alternative to SalesPriceAmountProjectCurrency. Conditional. Must be null when SalesPriceAmountProjectCurrency is sent. 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.

Format: double
Commentstringnull

Free-text description of the cost; also used in the invoice line text.

Max length: 255
ExpenseNostringnull

The expense number. Conditional. Used only when the ExpenseNo feature is on, the number series does not auto-generate, and AllowEmptyExpenseNo is on; otherwise ignored (auto number or NULL). - ExpenseNo feature off, or the series auto-generates — Adapter drops the value. - ExpenseNumberSeries on — Next number from the series. > Not yet documented: behaviour.

Max length: 100
SupplierIDintegernull

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.

Format: int32
SupplierInvoiceNostringnull

The supplier's invoice number. Returned as ProjectExpenseRead.InvoiceNo. - longer than 50 characters — Silently cut to 50.

Max length: 50
ExternalIDstringnull

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.

Max length: 50
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
ProjectIDinteger
Required

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.

ProjectSubContractIDinteger
Required

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.

PurchaseDatestring
Required

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.
PaymentMethodIDinteger
Required

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.

ExpenseTypeIDinteger
Required

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.

AmountIncludingVatExpenseCurrencynumber

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.

VatAmountExpenseCurrencynumber

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).

ExpenseCurrencyISOstring
Required

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}.

ProjectExpenseExchangeRatenumber,null

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.

IsBillableboolean

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.

SalesPriceAmountProjectCurrencynumber,null

The price to invoice the customer, in the PROJECT's currency, net of VAT. Alternative to ProfitRatio.

Conditional. Must be null when ProfitRatio is sent.

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).

ProfitRationumber,null

Mark-up in percent on the net cost: sales price = cost × (1 + ProfitRatio / 100). Alternative to SalesPriceAmountProjectCurrency.

Conditional. Must be null when SalesPriceAmountProjectCurrency is sent.

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.
Commentstring,null

Free-text description of the cost; also used in the invoice line text.

ExpenseNostring,null

The expense number.

Conditional. Used only when the ExpenseNo feature is on, the number series does not auto-generate, and AllowEmptyExpenseNo is on; otherwise ignored (auto number or NULL).

  • ExpenseNo feature off, or the series auto-generates — Adapter drops the value.

  • ExpenseNumberSeries on — Next number from the series.

Not yet documented: behaviour.

SupplierIDinteger,null

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.

SupplierInvoiceNostring,null

The supplier's invoice number. Returned as ProjectExpenseRead.InvoiceNo.

  • longer than 50 characters — Silently cut to 50.
ExternalIDstring,null

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

Success without content: HTTP 200 with the literal JSON body null. Literal JSON null — all checked rules pass.