Integration API
A read-only JSON API over HTTPS for outside systems, such as an ERP or a
payroll tool, to read one postti.me account's projects, team, clients, tasks
and timesheet entries.
| |
|---|
| Base URL | https://postti.me/api/v1 |
| Methods | GET only |
| Format | JSON, UTF-8 |
| Authentication | Authorization: Bearer <key> |
Authentication
An account's owner or admin generates a key at Ustawienia → Integracje
(/settings/integrations) and picks the account it is for. The key is shown
once; the server keeps only its hash, so a lost key cannot be recovered -
generate a new one and deactivate the old one.
Authorization: Bearer postti.me-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- A key works for one account, the one it was generated for. No endpoint
takes an account id; it always comes from the key.
- A key acts on behalf of the user who generated it, and works only
while that user is still an active owner or admin of the account. If they
lose that role or are deactivated, every request answers
403 KEY_OWNER_NOT_AUTHORIZED until the role is restored or a new key is
generated by a current admin.
- A deactivated key answers
401 INVALID_API_KEY, the same as an unknown
one.
Endpoints
| Endpoint | Returns |
|---|
GET /projects | the account's projects |
GET /members | the account's team members |
GET /clients | the account's clients |
GET /projects/{projectId}/tasks | a project's tasks |
GET /projects/{projectId}/members | a project's team members |
GET /projects/{projectId}/timesheets?year=&month= | a project's timesheet entries of one month |
GET /projects/{projectId}/tasks/{taskId}/timesheets?year=&month= | one task's timesheet entries of one month |
GET /clients/{clientId}/timesheets?year=&month= | timesheet entries on a client's projects of one month |
GET /members/{memberId}/timesheets?year=&month= | one member's timesheet entries of one month |
Paths are relative to the base URL. Every list comes whole, with no paging,
and is bounded (see Limits).
- Inactive projects, tasks, members and clients are included, with
isActive / active saying so. Deleted ones are left out, and
naming a deleted one in a path answers 404.
- Timesheet listings require
year (four digits) and month (1-12,
03 also accepted). Entries are listed by date, then id.
- Timesheet entries are listed whatever their status, and whatever the state
of their task or project.
- A path id that does not exist, belongs to another account or is deleted
answers
404 NOT_FOUND, without saying which. A task must also belong to
the project in its path.
- A client's entries are matched by the entry's
clientId. A trigger copies
it from the project shortly after an entry is saved, so an entry written in
the last few seconds may not yet appear there.
Responses
Success is HTTP 200:
{ "status": "ok", "data": [ ... ] }
Every timestamp is an ISO 8601 string in UTC. A field that has no value is
null, never missing.
Project
| Field | Type | |
|---|
id | string | the project's id |
name | string | |
description | string | null | |
clientId, clientName | string | |
isActive | boolean | |
hourBudgetInMinutes | number | null | null = no budget |
isHourBudgetHardLimit | boolean | |
plannedTaskMinutes | number | sum of the tasks' limits |
reportedMinutes | number | time reported on the project |
memberIds, groupIds, pmMemberIds | string[] | members, groups and PMs as assigned |
assignedMemberIds | string[] | everyone on the project: direct, PM and through a group |
createdAt, updatedAt | string | null | |
Task
| Field | Type | |
|---|
id, projectId | string | |
clientId | string | null | |
name | string | |
description | string | null | |
isActive, isRestricted | boolean | isRestricted: only the assigned members may report on it |
hourLimitInMinutes | number | null | |
isHourLimitHardLimit | boolean | |
reportableFrom, reportableTo | string | null | YYYY-MM-DD |
reportedMinutes | number | |
assignedMemberIds | string[] | |
createdAt, updatedAt | string | null | |
Member
| Field | Type | |
|---|
id | string | the member's id - what memberId refers to everywhere |
name, surname | string | null | |
email | string | |
isActive, isOwner | boolean | |
userId | string | null | null until the person registers |
groupIds | string[] | |
isProjectManager | boolean | only in /projects/{projectId}/members |
Client
| Field | Type | |
|---|
id, name | string | |
active | boolean | |
currencyCode | string | ISO 4217 |
timesheetEmail, address, notes | string | null | |
createdAt, updatedAt | string | null | |
Timesheet entry
One user's time on one task on one day.
| Field | Type | |
|---|
id | string | the entry's id |
date | string | YYYY-MM-DD |
projectId, taskId, memberId, userId | string | |
clientId | string | null | |
status | string | DRAFT, SUBMITTED, APPROVED, REJECTED, WITHDRAWN |
durationMinutes | number | the time that counts; an approval may lower it |
reportedDurationMinutes | number | the time the author entered |
hours | number | durationMinutes in hours, two decimal places |
description | string | null | |
statusChangedAt, createdAt, updatedAt | string | null | |
Errors
{ "status": "error", "code": "INVALID_API_KEY", "message": "The API key is invalid or inactive." }
| HTTP | code | When |
|---|
| 400 | VALIDATION_ERROR | a missing or malformed year / month, or a malformed path id |
| 401 | INVALID_API_KEY | no Authorization: Bearer header, or an unknown, malformed or deactivated key |
| 403 | KEY_OWNER_NOT_AUTHORIZED | the key's creator no longer owns or administers the account |
| 404 | NOT_FOUND | an unknown endpoint, or a path id that is missing, deleted or in another account |
| 404 / 405 | METHOD_NOT_ALLOWED | anything but GET - usually refused with a plain 404 before it reaches the API |
| 422 | TOO_MANY_ENTRIES | a listing exceeds its cap |
| 429 | RATE_LIMITED | too many requests (a 429 from the edge may come without a JSON body) |
| 500 | INTERNAL | an unexpected failure, logged on the server |
message is for people. Match on code.
Limits
| Default |
|---|
| Requests per key | 60 / min, 1 000 / h |
| Requests per IP address | about 60 / min |
| Documents in one project, member, client or task listing | 5 000 |
| Entries in one month's timesheet listing | 10 000 |
Past a rate limit the API answers 429; wait and retry with backoff. A
listing over its cap answers 422 TOO_MANY_ENTRIES instead of returning a
partial list. Responses carry Cache-Control: no-store. The API sends no
CORS headers: it is meant for servers, and a browser page cannot call it.
Example
KEY="postti.me-..."
curl -s -H "Authorization: Bearer $KEY" https://postti.me/api/v1/projects
curl -s -H "Authorization: Bearer $KEY" \
"https://postti.me/api/v1/projects/PROJECT_ID/timesheets?year=2026&month=10"