ConventionsWriting data

Writing data

How updates, retries, validate-before-write operations and external keys behave in the TimeLog REST API, and what a successful write returns.

Overview

Writing to TimeLog through the REST API is straightforward, but four behaviours catch integrators out:

  • Most PUT operations replace the whole object. A field you leave out is reset, not kept. Some PUT operations behave differently, and the reference page of each one says how.
  • Retrying a write is not always safe. Some creates make a duplicate when you retry them. Others let you detect that the first attempt already succeeded.
  • Validate operations are dry runs, but not perfect ones. Several of them skip checks that the real write performs, and one of them writes data.
  • External keys link TimeLog objects to your IDs, but they are not unique. TimeLog does not prevent two objects from carrying the same key.

This page explains each behaviour and what your client should do about it.

Updating objects

PUT replaces the whole object

The API-wide rule is that PUT is a full replace. The body you send becomes the new state of the object. A field you leave out does not keep its stored value. It is reset, for example cleared, set to false or set to 0.

This is why you must never send only the fields you want to change. Use this sequence instead:

Read the object

Call the operation's GET to read the current object.

Change the fields you need

Change only those fields in the object you read. Leave every other field as it is.

Send the whole object back

Send the complete object in the PUT body, including the fields you did not change.

Read it back if the response has no body

Many PUT operations answer 200 with the body null. Call GET again to see the stored result.

Full-replace operations

These PUT operations replace the object. The right-hand column shows what happens to fields you leave out, as far as it is documented.

OperationWhat a missing field does
PUT /v1/time-registrationFields are reset: Comment and AdditionalComment are cleared, Billable becomes false, BillableHours becomes 0. Two exceptions: an omitted MonthlyPeriod keeps its value, and a Factor that is not in the allowed list keeps the stored value.
PUT /v1/time-trackerThe six editable fields are overwritten. Omitted fields become null, 0 or "not specified". Start time, minutes and status do not change.
PUT /v1/employee-expenseThe ten writable fields are overwritten. An omitted Comment is cleared and an omitted Amount becomes 0. The owner and the contract cannot be changed.
PUT /v2/employee-expenseSame as v1, but the body also carries the contract (ProjectSubContractID). You must send it: an omitted contract is 0 and fails with error 37042.
PUT /v1/mileage-registrationEvery field in the body overwrites the stored value. The owner and the contract cannot be changed.
PUT /v2/competencesName, GroupId and IsActive are all written. An omitted IsActive deactivates the competence. An omitted GroupId fails.
PUT /v2/employee-competencesOnly the value changes. An omitted Value writes 0.
PUT /v1/external-systems/{externalSystemID}Both Name and Description are required and both are overwritten. Send the current value of the one you are not changing.

For expense and mileage updates, the last write wins: there is no concurrency token.

Not yet documented. Whether any other operation detects concurrent updates has not been established; assume the last write wins.

PUT operations that do not fully replace

Some PUT operations do not follow the API-wide rule. Do not assume any rule for a PUT operation that is not listed on this page: check its reference page.

OperationBehaviour
PUT /v1/allocation/{allocationID}Partial update. Only AllocatedHours, HourlyRateID, StartDate and EndDate can change, and you must send at least one of them.
PUT /v1/payment/{paymentID}Partial update. Only the fields you send change. An empty object is accepted and changes nothing.
PUT /v1/task/{taskID}Partial update. Only the fields you send change. The task's contract cannot be changed.
PUT /v1/project/{projectID}Mixed. Name, CustomerID, ContactID, Description and ProjectManagerID are always written, so an omitted Description or ContactID clears the stored value. The other fields are written only when you send them. To change the status, use PUT /v1/project/{projectID}/status.
PUT /v1/project/{projectID}/statusAlways send ProjectStatus. An omitted value is 0, which is Quote.
PUT /v1/customer/updateMixed. Eleven text fields are cleared when omitted: Name, CustomerNo, NickName, OrganizationNo, Phone, Fax, Email, Website, VatNo, CreditorID and InvoicingName. The other fields are left unchanged when omitted. In an address block, if you send any field of the block, the fields you leave out keep their stored values. Invoice settings cannot be changed here.
PUT /v1/userChanges names, licence type, extra roles and approval manager only. FirstName and LastName are always written, so always send both: an omitted name is cleared.
PUT /v2/competence-groupsChanges the name only.

Two related cases:

  • POST /v1/time-registration/update-time changes a project or travel registration by its integer ID. It reads the current values first, but an omitted Comment or Billable is still replaced.
  • PUT /v1/user answers 200 with null. Read the user back to confirm that the change was saved.

PATCH

The public API has one PATCH operation: PATCH /v2/page-settings/widgets/displayorder. It moves one of the caller's own front-page widgets to a new position. Every other update uses PUT or POST.

Safe retries

A network timeout leaves you not knowing whether a write happened. Whether you can simply send it again depends on the operation. Each operation's reference page states whether it is safe to retry.

Client-supplied GUIDs

Some creates let you choose the object's GUID. The time registration create is the main example: you send the GUID in the body field ID. If the call times out, retry with the same GUID. A 422 with error code 38030 means the first attempt succeeded. See Data types for how that GUID relates to the integer ID.

Where the server generates the GUID instead, a retry creates a second object.

Retry behaviour of common write operations

The table covers the services integrations use most. The full list is in each operation's reference page.

OperationSafe to retryWhat to do
Time registrations
POST /v1/time-registrationConditionalRetry with the same GUID. A 422 with 38030 means the first attempt succeeded.
POST /v1/time-registration/importNoThe server generates the GUID, so a retry after a timeout creates a second registration.
PUT /v1/time-registrationYes
POST /v1/time-registration/update-timeYes
DELETE /v1/time-registration/{id}ConditionalA retry after a lost response returns 500 with Code 100, the same as an unknown ID. Confirm with GET or your own records before you treat it as success.
POST /v1/time-tracker/task/start-time-trackerNoA retry starts a second tracker and stops the first one, turning it into a registration.
POST /v1/time-registration/stop-time-tracker/{timeTrackerId}NoA second call targets a tracker that no longer exists and fails with 500.
Project management
POST /v1/taskNoEvery call inserts a new task. After a timeout or a 500, list the project's tasks before retrying.
PUT /v1/task/{taskID}ConditionalSafe after a 4xx: nothing was written. After a 500 or a timeout, read the task and list the project's tasks first, because part of the change may already be saved.
DELETE /v1/task/{taskID}ConditionalA second delete answers 500 with error 37001. A GET that answers 500 with 37001 means the task is gone.
POST /v1/task/{taskID}/allocationConditionalA retry after success answers 409 with the existing allocation ID. Treat that 409 as "already done".
PUT /v1/allocation/{allocationID}ConditionalSafe after a 2xx or a timeout. After a 400 from the project save or a 500, the allocation may already be updated: read it first.
DELETE /v1/allocation/{allocationID}ConditionalTreat a 404 on retry as "already deleted".
POST /v1/contract/create-…-contract (all five models)NoEvery call inserts a new contract. After a timeout or a 500, list the project's contracts (newest first) before retrying.
POST /v1/paymentNoEvery call adds a new instalment. List the contract's payments before retrying.
PUT /v1/payment/{paymentID}Yes
POST /v1/project/create-from-templateNoEvery call creates a new project. Look the project up before retrying.
PUT /v1/project/{projectID}ConditionalThe same body gives the same state, but an omitted Description or ContactID is cleared every time.
PUT /v1/project/{projectID}/statusYesThe status date is reset to today and the status-change event is raised again.
PUT /v2/projects/statusConditionalThe first failing project stops the call with 500, and the projects before it keep their new status. Send the whole list again: projects already moved are re-applied without harm.
Employees
POST /v1/approval/timesheets/submit-time-registrationsConditionalA retry of a partly failed call refuses the registrations that were already submitted, with 37971. Read the violations instead of retrying blindly.
POST /v1/approval/timesheets/submit-datesConditionalRe-submitting adds another approval dialogue entry and raises the event again for each day. Nothing checks whether a day is already submitted.
POST /v1/normal-working-time/assign-to-employeeYes
POST /v2/employee-competencesConditionalA retry after a lost success fails with 500 and Code 100. Check with GET before retrying.
POST /v2/employee-competences/bulkYesCompetences the employee already has are skipped.
Expenses
POST /v1/employee-expense and /v2/employee-expenseNoEach call creates a new expense. After a lost response, list the owner's expenses for the date before retrying.
PUT /v1/employee-expense and /v2/employee-expenseYes
POST /v1/employee-expense/{employeeExpenseID}/file and /imageNoEach call adds new files, so a retry stores the receipt twice. Read the expense and compare FileUrls or ImageUrls first.
DELETE /v1/employee-expense/{employeeExpenseID}ConditionalAfter a lost response, check with GET that the expense still exists before retrying.
POST /v1/project-expenseNoEach call creates a new expense, and ExternalID is not deduplicated. List the project's expenses and match ExternalID before retrying.
POST /v1/mileage-registrationNoEvery call creates a new registration. Check the owner's registrations before retrying.
PUT /v1/mileage-registrationYes
DELETE /v1/mileage-registration/{mileageRegistrationID}ConditionalA retry after success fails with Code 100 but changes nothing.
CRM
POST /v1/customer/createConditionalA retry with the same Name and CustomerNo fails with 38017 if the first attempt created the customer. With an empty CustomerNo and a number series active, each attempt gets a new number, so a retry creates a duplicate.
PUT /v1/customer/updateYes
POST /v1/contact/createNo

Validate before you write

Many create, update and delete operations have a matching validate operation. It takes the same path and body as the write, runs checks without saving, and answers 200 with the body null when the checks pass.

Use them to show TimeLog's errors to a user early, or to check an import batch before you send it. A 200 from a validate operation does not guarantee that the write succeeds, except where the table says so. Most of them skip some checks, and one of them writes data.

Validate operationWhat it checksWhat it does not check
POST /v1/time-registration/validate-importThe project, that the contract is on the project, that the task is on the contract, that the user is active, and the time registration rules. This is stricter than the import itself, which does not check that TaskID belongs to ContractID and ProjectID.HourlyRate, so error 38037 cannot be checked in advance. It is not free of side effects: if the employee is not allocated to the task, it adds the employee to the project's resource group and the contract's team and creates a 0-hour allocation. This change is kept.
POST /v1/task/validate-new-taskThe body, the project, contract, hourly rate and product number, and the task save rules.Whether TaskTypeID exists, the depth of the task tree and the task's own validation. It ignores ParentTaskID and always validates a root task, so it cannot check a sub-task.
POST /v1/task/{taskID}/validate-update-taskThe body, access, and every rule the update checks before its first write: fields, moves and status.The status change's own processing and the project save rules.
POST /v1/task/{taskID}/validate-delete-taskThe same checks as the delete, before its first write. Where the delete reports one violation, the validate operation reports all of them.
POST /v1/task/{taskID}/allocation/validate-new-allocationThe same checks as the create, up to the write.The allocation's own validation, the rate chosen when HourlyRateID is omitted, and the project save rules.
POST /v1/allocation/{allocationID}/validate-deleteThe allocation exists, project privileges, and that it has no time registrations or running time tracker.
POST /v1/contract/validate-…-contract (five contract models)The checks the create runs before it inserts the contract: body, project, owner, dates, contract type.The account setting that makes a contract type required (37957), and everything from the insert on: contract-customer rules, hourly-rate creation, the first instalment, the project save rules and database constraints.
POST /v1/contract/validate-update-recurring-contract-payment-plan-amountThe contract rules on the changed amount.The save that regenerates the instalments. A contract that is not recurring also answers 200.
POST /v1/payment/validate-new-paymentThe body, project, access, task, product number and billing-plan rules.It is stricter than the create in one case: it rejects (37511) a second fixed-price instalment for a task, which the create accepts.
POST /v1/payment/{paymentID}/validate-update-paymentThe update's refusals and the save rules, applied to a copy of the payment.
POST /v1/payment/{paymentID}/validate-delete-paymentThe delete's first checks: the payment, access, contract model, and that the instalment is not booked or on a draft invoice.
POST /v1/project/validate-create-from-templateThe body, users, price list, template and project business rules.Some checks the create runs, including a business-process check and the mapping of the invoicing reference and default invoice language.
POST /v1/customer/validate-new-customerThe body, references, customer rules and invoice-setting rules.Whether you may create customers (the create checks this), and the customer-number uniqueness rule (30169).
POST /v1/contact/validate-new-contactThe body, the customer lookup and the country rule.Whether you may add contacts to the customer (the create checks this).
POST /v1/user/validate-new-userEverything the create checks, including the caller's privilege and unique user name and initials. A 200 here means the create will pass validation. It can still fail on a database error.
POST /v1/project-expense/validate-project-expenseThe body, project, currency code and the expense rules.The caller's licence, a missing exchange rate and errors that only the database insert detects, such as field length.
POST /v1/contract-hourly-rate/validate-hourly-rateUses different rules from the create. It also refuses fixed-price-style contracts (38003) and unknown product numbers (37939).It matches an existing Standard rate only on the same name and amount, so it does not warn that the create would re-price a Standard rate with the same name.

External keys

An external key links a TimeLog object to its ID in another system, such as an ERP, payroll or CRM system. Store your own ID on the TimeLog object, and you can trace the object back to the record it came from.

External keys are available on time registrations, projects and tasks. Each key belongs to an external system.

Register your external system

Do this once per integration and account:

  1. Call GET /v1/external-systems to see the systems already registered. TimeLog's built-in integrations register their own systems.
  2. If yours is not there, call POST /v1/external-systems with a Name and a Description. Store the returned ID in your own configuration.
  3. Use that ID as ExternalSystemID whenever you write a key.

POST /v1/external-systems does not check for duplicates. Every call creates a new system, even with a name that already exists. If the call times out, list the systems before you retry. An integration that syncs with two systems registers two external systems, so their keys never collide.

To rename a system, use PUT /v1/external-systems/{externalSystemID}. Its keys are not affected.

Set a key

ObjectOperation
Time registrationPUT /v1/time-registration/{timeRegistrationID}/external-keys
ProjectPUT /v1/project/{projectID}/external-keys
TaskPUT /v1/task/{taskID}/external-keys

The body has two fields: ExternalID (your ID, a string of up to 250 characters) and ExternalSystemID. The call is an upsert. An object has at most one key per external system, so if it already has a key for that system, the key is overwritten. The response is the stored key. The call is safe to retry.

Read keys

Use GET on the same paths:

  • Without externalSystemID, you get a paged list of all keys on the object.
  • With ?externalSystemID=…, you get the single key for that system.

If the object has no key for that system, the single read answers with business-rule error 37046 and HTTP 500. It does not answer 404 or an empty body.

What external keys do not do

  • They are not unique. TimeLog does not stop the same ExternalID from being stored on several objects of the same external system. If your integration needs one-to-one mapping, enforce it in your own system.
  • You cannot look up an object by its external key. There is no operation that finds a time registration, project or task from an ExternalID. Keep the mapping (your ID to TimeLog ID) in your own system.

Success without content

Many write operations do not return the object they changed. They answer one of two ways:

  • 200 with the literal JSON body null. Examples: PUT /v1/time-registration, PUT /v1/project/{projectID}, and every validate operation when it passes.
  • 204 with an empty body. Examples: PUT /v2/competences, PUT /v2/filter-view/{pageIdentifier}.

Check the status code before you parse the body. When you need the stored result, call GET. For details, see Response envelope.

When a write is refused

A write that breaks a TimeLog business rule is refused with an error body that carries a TimeLog error code, such as 38030 or 37046. Business-rule failures do not always use the same HTTP status, so read the error code in the body rather than relying on the status. See Errors for the error body, the status codes and the error catalogue.