postti.me

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 URLhttps://postti.me/api/v1
MethodsGET only
FormatJSON, UTF-8
AuthenticationAuthorization: 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

EndpointReturns
GET /projectsthe account's projects
GET /membersthe account's team members
GET /clientsthe account's clients
GET /projects/{projectId}/tasksa project's tasks
GET /projects/{projectId}/membersa 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

FieldType
idstringthe project's id
namestring
descriptionstring | null
clientId, clientNamestring
isActiveboolean
hourBudgetInMinutesnumber | nullnull = no budget
isHourBudgetHardLimitboolean
plannedTaskMinutesnumbersum of the tasks' limits
reportedMinutesnumbertime reported on the project
memberIds, groupIds, pmMemberIdsstring[]members, groups and PMs as assigned
assignedMemberIdsstring[]everyone on the project: direct, PM and through a group
createdAt, updatedAtstring | null

Task

FieldType
id, projectIdstring
clientIdstring | null
namestring
descriptionstring | null
isActive, isRestrictedbooleanisRestricted: only the assigned members may report on it
hourLimitInMinutesnumber | null
isHourLimitHardLimitboolean
reportableFrom, reportableTostring | nullYYYY-MM-DD
reportedMinutesnumber
assignedMemberIdsstring[]
createdAt, updatedAtstring | null

Member

FieldType
idstringthe member's id - what memberId refers to everywhere
name, surnamestring | null
emailstring
isActive, isOwnerboolean
userIdstring | nullnull until the person registers
groupIdsstring[]
isProjectManagerbooleanonly in /projects/{projectId}/members

Client

FieldType
id, namestring
activeboolean
currencyCodestringISO 4217
timesheetEmail, address, notesstring | null
createdAt, updatedAtstring | null

Timesheet entry

One user's time on one task on one day.

FieldType
idstringthe entry's id
datestringYYYY-MM-DD
projectId, taskId, memberId, userIdstring
clientIdstring | null
statusstringDRAFT, SUBMITTED, APPROVED, REJECTED, WITHDRAWN
durationMinutesnumberthe time that counts; an approval may lower it
reportedDurationMinutesnumberthe time the author entered
hoursnumberdurationMinutes in hours, two decimal places
descriptionstring | null
statusChangedAt, createdAt, updatedAtstring | null

Errors

{ "status": "error", "code": "INVALID_API_KEY", "message": "The API key is invalid or inactive." }
HTTPcodeWhen
400VALIDATION_ERRORa missing or malformed year / month, or a malformed path id
401INVALID_API_KEYno Authorization: Bearer header, or an unknown, malformed or deactivated key
403KEY_OWNER_NOT_AUTHORIZEDthe key's creator no longer owns or administers the account
404NOT_FOUNDan unknown endpoint, or a path id that is missing, deleted or in another account
404 / 405METHOD_NOT_ALLOWEDanything but GET - usually refused with a plain 404 before it reaches the API
422TOO_MANY_ENTRIESa listing exceeds its cap
429RATE_LIMITEDtoo many requests (a 429 from the edge may come without a JSON body)
500INTERNALan unexpected failure, logged on the server

message is for people. Match on code.

Limits

Default
Requests per key60 / min, 1 000 / h
Requests per IP addressabout 60 / min
Documents in one project, member, client or task listing5 000
Entries in one month's timesheet listing10 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"