REST API v1
ProBG Time Tracking API v1 provides read-only access to clients, projects, and tracked time that belong to the token's Account. It is intended for CRM/ERP integrations, BI reporting, external reporting, and data synchronization.
The API never accepts an external
account_idto select tenancy. The Account is determined exclusively by the Bearer token.
Quick start
- Open API in the administration as an Account Administrator.
- Create a separate token for the integration.
- Grant only the required scopes:
clients:read,projects:read, and/ortime:read. - Copy the token immediately — its full value is displayed only once.
- Send the token with every request using HTTP Bearer authentication.
curl -H "Authorization: Bearer <TOKEN>" \
-H "Accept: application/json" \
"https://www.tracker.probg.com/api/v1/clients"
Plan availability
The API works only while the Account has the api entitlement enabled.
The default plan presets enable API access for:
- Agency
- Enterprise
A Platform Administrator may change the entitlement at plan level or by Account override.
An existing token stops working immediately when:
- it is revoked;
- it expires;
- the Account is suspended;
- the current subscription no longer includes API access.
Authentication
Base URL:
https://www.tracker.probg.com/api/v1
Required headers:
Authorization: Bearer ptt_...
Accept: application/json
Tokens are Account-scoped. Only a SHA-256 hash is stored; the plaintext token cannot be recovered.
Token expiration
When creating a token, choose:
- 30 days;
- 90 days;
- 365 days;
- no automatic expiration.
A token without automatic expiration must be revoked manually when the integration is no longer used.
Scopes
| Scope | Grants |
|---|---|
clients:read |
Client list and client details |
projects:read |
Project list and project details |
time:read |
Time-entry list and details |
A token without the required scope receives HTTP 403 with error.code = insufficient_scope.
Use least privilege: do not grant all three scopes when an integration needs only one resource.
Rate limits
Per token:
- 120 requests / minute;
- 3000 requests / hour.
Additional IP safety ceiling:
- 600 requests / minute;
- 10,000 requests / hour.
When a limit is reached, Laravel returns HTTP 429 Too Many Requests.
Pagination
All list endpoints are paginated.
| Parameter | Type | Default | Limit |
|---|---|---|---|
page |
integer | 1 | Page number |
per_page |
integer | 25 | 1–100 |
Example:
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 25,
"last_page": 1,
"total": 0
},
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": null
}
}
Follow links.next until it becomes null.
Data conventions
- Date-only values use
YYYY-MM-DD. - Date/time values use ISO 8601.
- Durations are seconds.
- Decimal money values are returned as decimal strings or
nullto preserve precision. - All responses are JSON.
- IDs are integers.
Clients
GET /clients
Requires clients:read.
Returns clients in the token's Account.
Filters
| Parameter | Type | Description |
|---|---|---|
q |
string, max 100 | Searches name, company, contact, email, and company ID |
status |
enum | active (default), inactive, archived, all |
updated_since |
date/datetime | Records updated at or after the supplied value |
per_page |
integer | 1–100 |
Example:
curl -H "Authorization: Bearer <TOKEN>" \
-H "Accept: application/json" \
"https://www.tracker.probg.com/api/v1/clients?status=all&updated_since=2026-10-01T00:00:00%2B03:00&per_page=50"
Client fields
| Field | Type |
|---|---|
id |
integer |
name |
string |
company |
string/null |
contact_name |
string/null |
email |
string/null |
phone |
string/null |
address |
string/null |
company_id |
string/null |
is_active |
boolean |
projects_count |
integer |
tracked_seconds |
integer |
archived_at |
ISO 8601/null |
created_at |
ISO 8601/null |
updated_at |
ISO 8601/null |
GET /clients/{id}
Requires clients:read.
Returns one client. Archived clients remain addressable by ID.
An ID from another Account returns 404 without disclosing whether that resource exists.
Example response:
{
"data": {
"id": 42,
"name": "Example Client",
"company": "Example Ltd",
"contact_name": "Jane Doe",
"email": "jane@example.com",
"phone": "+359...",
"address": "Sofia",
"company_id": "BG123456789",
"is_active": true,
"projects_count": 3,
"tracked_seconds": 184500,
"archived_at": null,
"created_at": "2026-09-10T09:20:00+03:00",
"updated_at": "2026-10-05T17:14:00+03:00"
}
}
Projects
GET /projects
Requires projects:read.
Filters
| Parameter | Type | Description |
|---|---|---|
q |
string, max 100 | Searches project name and description |
client_id |
integer | Projects for one client |
status |
enum | active (default), completed, archived, all |
updated_since |
date/datetime | Records updated at or after the supplied value |
per_page |
integer | 1–100 |
Example:
curl -H "Authorization: Bearer <TOKEN>" \
-H "Accept: application/json" \
"https://www.tracker.probg.com/api/v1/projects?client_id=42&status=all"
Project fields
| Field | Type | Notes |
|---|---|---|
id |
integer | |
client_id |
integer | |
client |
object/null | id, name, company |
name |
string | |
description |
string/null | |
status |
string | |
billing_model |
string | hourly, fixed_fee, retainer, non_billable |
hourly_rate |
decimal string/null | Stored project rate |
effective_hourly_rate |
decimal string/null | Effective rate for Hourly projects |
billing_currency |
string | Project billing currency |
fixed_fee_amount |
decimal string/null | |
retainer_amount |
decimal string/null | |
estimated_seconds |
integer/null | |
tracked_seconds |
integer | |
starts_at |
YYYY-MM-DD/null | |
ends_at |
YYYY-MM-DD/null | |
created_at |
ISO 8601/null | |
updated_at |
ISO 8601/null |
Internal labor-cost snapshots are not exposed.
GET /projects/{id}
Requires projects:read.
Returns one project from the current Account.
Time entries
GET /time-entries
Requires time:read.
Filters
| Parameter | Type | Description |
|---|---|---|
client_id |
integer | Entries for one client |
project_id |
integer | Entries for one project |
task_id |
integer | Entries for one task |
user_id |
integer | Entries for one user |
from |
YYYY-MM-DD | Inclusive start date |
to |
YYYY-MM-DD | Inclusive end date; must be >= from |
billable |
boolean | true/false or 1/0; empty means no filter |
billed |
boolean | true/false or 1/0; empty means no filter |
q |
string, max 100 | Searches title and description |
updated_since |
date/datetime | Records updated at or after the supplied value |
per_page |
integer | 1–100 |
Project-month export example:
curl -H "Authorization: Bearer <TOKEN>" \
-H "Accept: application/json" \
"https://www.tracker.probg.com/api/v1/time-entries?project_id=12&from=2026-10-01&to=2026-10-31&per_page=100"
Time-entry fields
| Field | Type |
|---|---|
id |
integer |
user_id |
integer/null |
client_id |
integer/null |
project_id |
integer/null |
task_id |
integer/null |
user |
object/null — id, name |
client |
object/null — id, name |
project |
object/null — id, name |
task |
object/null — id, title |
work_date |
YYYY-MM-DD/null |
started_at |
ISO 8601/null |
ended_at |
ISO 8601/null |
duration_seconds |
integer |
source |
string |
title |
string |
description |
string/null |
is_billable |
boolean |
is_billed |
boolean |
hourly_rate |
decimal string/null |
amount |
decimal string/null |
created_at |
ISO 8601/null |
updated_at |
ISO 8601/null |
The API intentionally excludes:
cost_rate;cost_amount;- private notes.
GET /time-entries/{id}
Requires time:read.
Returns one time entry from the current Account.
Error responses
401 — missing Bearer token
{
"error": {
"code": "unauthenticated",
"message": "A bearer token is required."
}
}
401 — invalid, expired, or revoked token
{
"error": {
"code": "invalid_token",
"message": "The API token is invalid, expired, or revoked."
}
}
403 — Account unavailable
{
"error": {
"code": "account_unavailable",
"message": "The account is not available."
}
}
403 — subscription no longer includes API access
{
"error": {
"code": "feature_unavailable",
"message": "API access is not included in the current subscription."
}
}
403 — missing scope
{
"error": {
"code": "insufficient_scope",
"message": "The API token does not have the required scope [projects:read]."
}
}
404 — resource unavailable
Unknown IDs, zero, IDs outside the supported positive integer range, and IDs that belong to another Account return 404.
422 — validation error
Invalid query parameters use Laravel's standard JSON validation response:
{
"message": "The given data was invalid.",
"errors": {
"per_page": [
"The per page field must not be greater than 100."
]
}
}
429 — rate limit
Reduce request frequency and apply retry/backoff logic.
Integration recipes
Incremental synchronization
Store a checkpoint after a fully successful import and use updated_since on the next run.
GET /clients?updated_since=2026-10-06T08:00:00Z
GET /projects?updated_since=2026-10-06T08:00:00Z
GET /time-entries?updated_since=2026-10-06T08:00:00Z
Do not use the client machine's current clock as proof that all changes have been consumed. Advance the checkpoint only after all pages complete successfully.
Projects for one client
GET /projects?client_id=42&status=all
Tracked time for a period
GET /time-entries?from=2026-10-01&to=2026-10-31&per_page=100
Security recommendations
- Create a separate token for every integration.
- Grant the minimum required scopes.
- Never commit the token to Git or expose it in frontend JavaScript, screenshots, or public logs.
- Store it in a secret manager or environment variable.
- Always use HTTPS.
- Revoke compromised or unused tokens immediately.
- Rotate long-lived tokens periodically.
- Do not retry
401responses indefinitely; inspect token lifecycle. - Use exponential backoff after
429.
Versioning
Breaking authentication or response-contract changes require a new namespace such as /api/v2.
Within v1, the API may add:
- optional response fields;
- optional query filters;
- new endpoints.
Integrations must not assume response objects contain only the currently documented fields.
Current limitations
API v1 is currently read-only.
Not yet part of v1:
- create/update/delete operations;
- Tasks endpoint;
- Reports endpoint;
- Invoices endpoint;
- Webhooks;
- OAuth/client credentials;
- bulk export endpoint.
These capabilities require separate versioned contracts and security review.
Public documentation and Postman
The documentation is available without signing in:
/api/docs— English;/api/docs/en— English;/api/docs/bg— Bulgarian;/api/docs/openapi.yaml— OpenAPI 3.1 specification;/api/docs/postman.json— Postman collection.
Using the Postman collection
- Download
/api/docs/postman.json. - Choose Import in Postman and import the JSON file.
- Open the collection variables.
- Set
base_url— the default ishttps://tracker.probg.com/api/v1. - Paste the API token into
token. - Set
client_id,project_id, andtime_entry_idwhen needed. - Send the prepared requests from the Clients, Projects, and Time Entries folders.
Bearer authentication is configured at collection level through {{token}}. Optional query parameters are included in disabled state and can be enabled as needed.
OpenAPI
Use the Download OpenAPI action in the administration documentation page to obtain an OpenAPI 3.1 YAML specification suitable for Postman, Insomnia, Swagger UI/Editor, Redoc, and SDK-generation tools.
Checkpoint dates and timezone
updated_since is inclusive. ISO 8601 offsets (Z, +03:00, etc.) are converted to the application/database storage timezone before comparison. Date-only values mean midnight in that timezone; datetimes without an offset also use it. Stored updated_at values have second precision; a fractional checkpoint after a stored second excludes that second. Equivalent instants always select the same records.
Use status=all when synchronizing clients or projects. These endpoints filter the resource’s own updated_at; changes to related aggregates (such as tracked_seconds) do not necessarily update that timestamp. There is no deletion feed or stable snapshot across pages. Integrations should overlap checkpoints, deduplicate by ID, and periodically reconcile full data.
Billing status checkpoints
Issuing or cancelling an invoice advances updated_at on its linked time entries in the same transaction that changes is_billed. The next inclusive updated_since query therefore includes those changes. This only changes synchronization metadata; locked durations and financial snapshots are preserved. Duplicate actions do not advance the checkpoint; failed issuance rolls it back. Cancellation follows the linked historical entries even after a client/account transfer, while API visibility remains scoped to the current token account.
Existing integrations must perform one full time-entry reconciliation after deploying this correction: billing changes made before deployment are not backfilled. Overlap checkpoints and periodically reconcile data as described above; deletion feeds and stable pagination snapshots remain outside v1.