Log time
POSThttps://
Books minutes against a project on one calendar day and returns the entry with a Location header.
Access
- Token scope
- api:
write: times Without it:
403 insufficient-token-scope. - Permission
- None. The record's own rules still apply.
- Rate cost
- 1 unit, write bucket. A write costs the same whatever
viewyou ask for. - Also
- Needs
Idempotency-Key. Never cached:private, no-store. Unknown body keys are refused, never dropped.
Both are checked, scope first. A token never does more than its user. How access is checked
user_id is not accepted: the hours belong to the authenticated user unless on_behalf_of_user_id names someone else, and logging time for another user additionally requires times::update_others, enforced by that field. Every check inside the write (whether that person may log time on the project, owns the task and may log time on the work item) is evaluated against the person the hours are for, not against you, so is_overtime is dropped when they lack times::write_overtime even if you hold it. One exception, which plnnrly's own form shares: the check that project, task and budget fit together weighs the task against you, so filing hours on somebody else's task needs times::write_on_others_tasks from the caller as well. A task or work item that does not belong to the submitted project is a 422 on its own field; a task or work item you may read but may not book against is a 403. budget_id is derived from the task and never accepted. is_not_billable is inferred from the task when omitted; sending a value that differs from the inference requires billable_change_reason, answering 422 on /billable_change_reason without it. Sending a reason marks the entry as exceeding its budget whether or not exceeded_budget is sent alongside. Either an exceeded budget or a billability override puts the entry in a reviewer's queue, and while it sits there every write to it by anyone but its author or a reviewer answers 409. Idempotency-Key is required here: a retried time entry is real, invoiced damage. The API is stricter than plnnrly here: a task is accepted only if you can also see it on the tasks collection, so a caller holding times::write_on_others_tasks but not kanban::manage_for_others gets a 422 exists for a task plnnrly would accept.
Query parameters
Unknown parameters are refused with 400, naming the parameter.
Request headers
A correlation id of your own. It is recorded in our logs next to the server's request id but never returned: the response header always carries the server's id. A value that does not match the pattern is discarded silently, never rejected.
Request body
Unknown keys are refused with 422, naming the key.
Project the hours are booked against; null once that project is deleted.
Task the hours are booked on; null when the entry is not attached to one, or once that task is deleted.
Work item the hours are attached to; null when the entry is not attached to one, or once that work item is deleted.
Whether the hours count as overtime. Sending it without the times::write_overtime permission leaves it false and adds an input_ignored warning rather than failing the write.
Whether these hours are excluded from invoicing. Inferred from the task's is_billable flag (false without a task) on create and again whenever task_id is sent. Sending a value that differs from the inferred or current one requires billable_change_reason and puts the entry in a reviewer's queue; without it the write is a 422 on /billable_change_reason.
Why the inferred billability was overridden. Required whenever is_not_billable is sent with a value that differs from the inferred or current one, on create and on PATCH alike; the override puts the entry in a reviewer's queue, reopening one a reviewer had already settled.
Whether the entry was booked knowing it exceeds its budget. Set on create only, and true whenever a reason is sent; setting it puts the entry in a reviewer's queue.
Why the budget overrun was accepted. Sending it on create marks the entry as exceeding its budget. Once a reviewer has settled the entry it can no longer be changed: a PATCH that does answers 409 reason_reviewed.
Write-only, on create only: the employee to file the hours for. Omit it to file them for yourself. Naming anyone else requires the times::update_others permission, and every check inside the write (whether that person may log on the project and on that task, and whether overtime may be recorded) is then evaluated against them, not against you.
Response 201
Returns a Time in the detail view.
Headers
ETag- A strong validator of exactly this representation.
Idempotency-Replayedtruewhen this response was replayed from a storedIdempotency-Keyrather than produced by a new write.Location- The absolute URL of the created or queued resource.
RateLimit- The IETF
httpapi-ratelimit-headersform of your remaining allowance on the tightest bucket this request touched. RateLimit-Policy- The ceiling and window of that bucket, as
<limit>;w=<seconds>. X-Request-Id- The server-generated ULID correlating this response with our logs.
Every response also carries Cache-Control, Vary, X-Content-Type-Options, X-Plnnrly-Api-Version, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Headers and versioning
The project the hours are booked against, always expanded. Its customer_id is the customer to fetch with GET /api/v2/customers/{customer}; the customer itself is not nested.
Budget the hours are invoiced against. Derived from the task, never accepted on a write; null when no task is set or the budget is deleted.
The budget the hours are invoiced against. Expanding it with ?include=budget additionally requires the budgets::read permission and a token scope covering the budgets prefix.
Task the hours are booked on; null when the entry is not attached to one, or once that task is deleted.
The task the hours are booked on. Expanding it with ?include=task additionally requires the kanban::manage_self permission and a token scope covering the kanban prefix, and is limited to the tasks you can see on the tasks collection.
Work item the hours are attached to; null when the entry is not attached to one, or once that work item is deleted.
The work item the hours are attached to, always expanded.
Employee the hours belong to. Assigned on create (from the authenticated user, or from on_behalf_of_user_id) and never writable afterwards; reassigning a worklog is a DELETE plus a POST.
The employee the hours belong to. Expanding it with ?include=user additionally requires the users::read permission and a token scope covering the users prefix.
Whether these hours are excluded from invoicing. Inferred from the task's is_billable flag (false without a task) on create and again whenever task_id is sent. Sending a value that differs from the inferred or current one requires billable_change_reason and puts the entry in a reviewer's queue; without it the write is a 422 on /billable_change_reason.
Whether the hours count as overtime. Sending it without the times::write_overtime permission leaves it false and adds an input_ignored warning rather than failing the write.
Whether the entry was booked knowing it exceeds its budget. Set on create only, and true whenever a reason is sent; setting it puts the entry in a reviewer's queue.
Why the budget overrun was accepted. Sending it on create marks the entry as exceeding its budget. Once a reviewer has settled the entry it can no longer be changed: a PATCH that does answers 409 reason_reviewed.
Why the inferred billability was overridden. Required whenever is_not_billable is sent with a value that differs from the inferred or current one, on create and on PATCH alike; the override puts the entry in a reviewer's queue, reopening one a reviewer had already settled.
The reviewer who settled the entry, once one has. Set by the review queue in plnnrly; this API exposes no way to write it.
The invoice these hours were billed on, once they have been. While it is set, the entry is locked: a write answers 409 with conflict.reason locked_by_invoice.
The sales-invoice line these hours were billed on, once they have been. Set by the invoicing flow.
Errors
409 Conflict.
415 Unsupported media type.
422 Unprocessable content.
7 errors every operation can return: 400, 401, 404, 406, 429, 500, 503
400 Bad request.
idempotency-key-requiredinvalid-cursorinvalid-query-parametermalformed-requestunsupported-parameter
401 Unauthenticated.
404 Not found.
406 Not acceptable.
429 Too many requests.
500 Server error.
503 Service unavailable.
This path allows GET, POST. Anything else answers 405.
Something wrong or missing on this page? Tell us