Validate a contract hourly rate
Checks a contract-hourly-rate create body without writing anything.
A dry run of create-hourly-rate, but with DIFFERENT rules: it additionally refuses fixed-price-style contracts (38003) and unknown product numbers (37939, looked up in the account's default legal entity), and its "existing Standard rate" match requires the same name AND amount, so it does not warn that create would re-price a Standard rate with the same name. When an existing rate matches, no further rule is checked. Success is 200 with the literal body null.
Use it to:
- Pre-flight a data import before calling create-hourly-rate
Not for:
- Predicting create's outcome exactly
Rules across fields:
- R1: ProjectSubContractId must be a contract of ProjectId (37042).
- R2: If an active Standard rate with the same name and amount exists, or (category 1 / 3) a Project / Customer rate with the same name and amount, the call succeeds without checking the contract model or the category.
Behaviour:
- Idempotent.
- Safe to retry.
- Changes are visible to the next read immediately.
Call first: GET /v1/contract
See also: GET /v1/contract-hourly-rate
Not yet documented: examples.
curl -X POST "https://app3.timelog.com/your-account/api/v1/contract-hourly-rate/validate-hourly-rate" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-d '{
"ProjectId": 123,
"ProjectSubContractId": 123,
"ContractHourlyRateName": "John Doe",
"ContractHourlyRateValue": 3.14,
"ProductNo": "example_string",
"ContractHourlyRateCreateCategory": 42
}'
import requests
import json
url = "https://app3.timelog.com/your-account/api/v1/contract-hourly-rate/validate-hourly-rate"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN"
}
data = {
"ProjectId": 123,
"ProjectSubContractId": 123,
"ContractHourlyRateName": "John Doe",
"ContractHourlyRateValue": 3.14,
"ProductNo": "example_string",
"ContractHourlyRateCreateCategory": 42
}
response = requests.post(url, headers=headers, json=data)
print(response.json())
const response = await fetch("https://app3.timelog.com/your-account/api/v1/contract-hourly-rate/validate-hourly-rate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN"
},
body: JSON.stringify({
"ProjectId": 123,
"ProjectSubContractId": 123,
"ContractHourlyRateName": "John Doe",
"ContractHourlyRateValue": 3.14,
"ProductNo": "example_string",
"ContractHourlyRateCreateCategory": 42
})
});
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,
"ContractHourlyRateName": "John Doe",
"ContractHourlyRateValue": 3.14,
"ProductNo": "example_string",
"ContractHourlyRateCreateCategory": 42
}`)
req, err := http.NewRequest("POST", "https://app3.timelog.com/your-account/api/v1/contract-hourly-rate/validate-hourly-rate", 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/contract-hourly-rate/validate-hourly-rate')
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,
"ContractHourlyRateName": "John Doe",
"ContractHourlyRateValue": 3.14,
"ProductNo": "example_string",
"ContractHourlyRateCreateCategory": 42
}'
response = http.request(request)
puts response.body
"null"
{
"error": "Unauthorized",
"message": "Authentication required. Please provide a valid API token",
"code": 401
}
{
"Code": 102,
"Details": [
{
"Message": "No Project with ProjectID: 0 exist",
"ErrorCode": 37014
}
],
"Message": "Business rules exception.",
"Parameters": "contractHourlyRateModel: {"ContractHourlyRateCreateCategory":0,"ProjectId":0,"ProjectSubContractId":0,"ContractHourlyRateName":null,"ContractHourlyRateValue":0.0,"ProductNo":null}",
"Url": "https://app{server-number}.timelog.com/{account}/api/v1/contract-hourly-rate/validate-hourly-rate"
}
/v1/contract-hourly-rate/validate-hourly-rate
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
Project that owns the contract. Only used to check that the project exists and that ProjectSubContractId belongs to it.
Can be set on create only.
Refers to ProjectRead.ProjectID. Look up valid values with GET /v1/project/get-all.
- Omitted, 0 or not an existing project — 37014 "No Project with ProjectID: {0} exist" — 500 Code 102 (captured for {}).
Valid values come from GET /v1/project/get-all.
The contract (ProjectSubContract) that gets the rate.
Can be set on create only.
Refers to ContractRead.ContractID. Look up valid values with GET /v1/contract.
- Not a contract of ProjectId (or does not exist) — 37042 — 500 Code 102.
Valid values come from GET /v1/contract.
Name of the rate. Matched case-insensitively against the contract's existing rates. Can be set on create only. - Equal (case-insensitive) to the name of an active, non-indexed Standard rate on the contract — create: that rate is re-priced to ContractHourlyRateValue (indexation) instead of creating a rate. > Not yet documented: behaviour.
Price per hour of the rate, in the currency of the project's price group (the project's currency list). Can be set on create only. Unit: project price-group currency per hour Contains financial data. - Omitted — 0 (non-nullable decimal); a 0 rate is created — no range check, negative values are not rejected either. - Category AddRateToStandard — Stored as the amount in the project's price group; every OTHER price group of the legal entity gets the new rate with amount 0. - Category AddRateToCustomer — The same number is stored in EVERY customer price group of the legal entity, whatever its currency.
Product number for invoice lines of this rate. Stored as sent (empty / whitespace → null on price-list rates).
Can be set on create only.
Refers to ProductNumberRead.ProductNumber. Look up valid values with GET /v1/product.
- validate-hourly-rate, non-blank and not a product number of the account's DEFAULT legal entity — 37939 — 500 Code 102. Checked against the default legal entity, not the project's.
- create-hourly-rate, any value — Not checked — an unknown product number is stored.
Valid values come from GET /v1/product.
Where the new rate lives. Sent as an integer.
Can be set on create only.
- Omitted / 0 / any other value, on validate — 300132 "User does not have privileges to save hourly rate." — 500 Code 102; the message is misleading.
Allowed values:
| Value | Name | Meaning |
|---|---|---|
| 1 | AddRateToContract | Project-specific rate: created on this contract only (Type Project), in the project's price group. |
| 2 | AddRateToStandard | New hourly-rate service in the project legal entity's STANDARD price list (a rate in every price group of the legal entity: the amount in the project's group, 0 elsewhere), copied to the contract as a Standard rate. |
| 3 | AddRateToCustomer | New customer-specific service for the project's customer (customer price group created if missing; the same amount in every customer price group), copied to the contract as a Customer rate. |
> Not yet documented: values → serialization.
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
Project that owns the contract. Only used to check that the project exists and that ProjectSubContractId belongs to it.
Can be set on create only.
Refers to ProjectRead.ProjectID. Look up valid values with GET /v1/project/get-all.
- Omitted, 0 or not an existing project — 37014 "No Project with ProjectID: {0} exist" — 500 Code 102 (captured for {}).
Valid values come from GET /v1/project/get-all.
The contract (ProjectSubContract) that gets the rate.
Can be set on create only.
Refers to ContractRead.ContractID. Look up valid values with GET /v1/contract.
- Not a contract of ProjectId (or does not exist) — 37042 — 500 Code 102.
Valid values come from GET /v1/contract.
Name of the rate. Matched case-insensitively against the contract's existing rates.
Can be set on create only.
- Equal (case-insensitive) to the name of an active, non-indexed Standard rate on the contract — create: that rate is re-priced to ContractHourlyRateValue (indexation) instead of creating a rate.
Not yet documented: behaviour.
Price per hour of the rate, in the currency of the project's price group (the project's currency list).
Can be set on create only.
Unit: project price-group currency per hour
Contains financial data.
-
Omitted — 0 (non-nullable decimal); a 0 rate is created — no range check, negative values are not rejected either.
-
Category AddRateToStandard — Stored as the amount in the project's price group; every OTHER price group of the legal entity gets the new rate with amount 0.
-
Category AddRateToCustomer — The same number is stored in EVERY customer price group of the legal entity, whatever its currency.
Product number for invoice lines of this rate. Stored as sent (empty / whitespace → null on price-list rates).
Can be set on create only.
Refers to ProductNumberRead.ProductNumber. Look up valid values with GET /v1/product.
-
validate-hourly-rate, non-blank and not a product number of the account's DEFAULT legal entity — 37939 — 500 Code 102. Checked against the default legal entity, not the project's.
-
create-hourly-rate, any value — Not checked — an unknown product number is stored.
Valid values come from GET /v1/product.
Where the new rate lives. Sent as an integer.
Can be set on create only.
- Omitted / 0 / any other value, on validate — 300132 "User does not have privileges to save hourly rate." — 500 Code 102; the message is misleading.
Allowed values:
| Value | Name | Meaning |
|---|---|---|
1 | AddRateToContract | Project-specific rate: created on this contract only (Type Project), in the project's price group. |
2 | AddRateToStandard | New hourly-rate service in the project legal entity's STANDARD price list (a rate in every price group of the legal entity: the amount in the project's group, 0 elsewhere), copied to the contract as a Standard rate. |
3 | AddRateToCustomer | New customer-specific service for the project's customer (customer price group created if missing; the same amount in every customer price group), copied to the contract as a Customer rate. |
Not yet documented: values → serialization.
123Responses
*Success without content: HTTP 200 with the literal JSON body null. Literal JSON null (TafSuccessResult).
Order: none
Empty result: none*