jonta

Developers

Jonta Integration API

Read timesheets and usage stats, record deployments from CI and raise tickets from other systems, using scoped API keys.

Overview

The Integration API lets another system read data from Jonta or write into it, using its own API key instead of a user's login session. It's built for things like a payroll system pulling timesheets, a CI pipeline recording deploys, or an external tool raising tickets.

  • Base URL: https://<your-instance>/api/integrations
  • Format: JSON (one endpoint also supports CSV)
  • Auth: a per-integration API key (see Authentication), never a user session or cookie
MethodEndpointScope requiredPurpose
GET/timesheetstime.exportExport logged time, paginated
GET/statsstats.readAggregate, non-identifying usage counts
GET/projectstime.export, deploy.write or ticket.write (any one)List the projects this key can reach
GET/ticket-typesticket.writeList valid ticket types, for the typeId of POST /tickets
POST/deploymentsdeploy.writeRecord a deploy (for example from CI)
POST/ticketsticket.writeCreate a ticket in a project

Each key is scoped to exactly one of these, so a timesheet key can never create tickets and a deploy key can never read stats.

This reference covers the endpoints above. Jonta also exposes POST /api/integrations/git/:provider/:connectionId for git host webhooks (GitHub, GitLab and so on). That route is authenticated by the git host's own request signature, not an API key, and isn't meant to be called by an external integration directly.

Getting started

  1. An admin creates an API key in Settings → Integrations → API keys, scoped to the one thing the integration needs to do.
  2. Copy the key immediately. It's shown once and can't be retrieved again.
  3. Make a first call with it (GET /api/integrations/stats is the simplest) and confirm you get a 200, not a 401.
  4. Handle errors the same way everywhere: every failure is {"error": "..."} with a status code from Conventions. There's no endpoint-specific error shape to special-case.

Authentication

Every request carries a bearer token in the Authorization header:

Authorization: Bearer ptk_xxxxxxxxxxxxxxxxxxxxxxxx

A request with no key, a malformed key, or a revoked key gets 401 Unauthorized. A request with a valid key that isn't allowed to do what it's asking gets 403 Forbidden.

Creating a key

An admin creates keys from Settings → Integrations → API keys inside Jonta. When you create one, you choose:

  • Name: something that identifies the system using it (for example "Payroll export")
  • Scope: exactly one of time.export, stats.read, deploy.write, ticket.write
  • Projects: either every project, or a specific list; the key is refused outside that list

The full key (ptk_...) is shown once, at creation. Only its hash is stored, so if it's lost it can't be recovered: revoke it and create a new one.

Scopes

ScopeGrants
time.exportRead timesheet entries (GET /timesheets)
stats.readRead aggregate usage stats (GET /stats)
deploy.writeRecord deploys (POST /deployments)
ticket.writeCreate tickets (POST /tickets)

A key can only do the one thing it was scoped for. Revoking a key takes effect immediately: the next request with it fails with 401.

Some endpoints accept a key with any one of several scopes. For example, GET /projects works for a time.export, deploy.write or ticket.write key, filtered to whatever that key can already reach.

Some keys are provisioned by Jonta itself (for example, a stats.read key tied to your licence) rather than created by an admin. These show as protected and can't be revoked from the admin screen, since doing so would break that integration.

Conventions

Errors. Every error is {"error": "a human-readable message"} with one of these statuses:

StatusMeaning
400The request body or query parameters failed validation; the message says what
401No key, a malformed key, or a revoked key
403The key's scope doesn't cover this, or it's restricted to projects that don't include this one
404The project, ticket type or resource named doesn't exist
429Too many requests (see rate limits below)

Dates and times. Calendar dates are YYYY-MM-DD. Timestamps are ISO 8601 (for example 2026-10-01T00:00:00Z).

Identifying a project. Anywhere an endpoint needs a project, send either projectId (its UUID) or projectKey (its short code, for example DDG, case-insensitive). If a key is restricted to specific projects, requests for any other project get 403.

Looking up projects and ticket types. Use GET /projects (works for a time.export, deploy.write or ticket.write key) and GET /ticket-types (ticket.write) to discover valid ids without needing separate access to the Jonta UI.

Pagination. GET /timesheets is the only paginated endpoint: pass limit and offset, and keep requesting while the response's hasMore is true, using nextOffset as the next request's offset.

Rate limits. Two layers apply: 600 requests per minute per IP address across the whole API, and 300 requests per minute per API key on top of that, so one noisy key can't exhaust another key's quota, even sharing an IP. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers for whichever layer is closer to triggering. Going over either gets 429: {"error": "Too many requests. Please wait a moment and try again."} for the IP limit, or {"error": "Too many requests for this key. Please wait a moment and try again."} for the key limit.

Request size. Request bodies are capped at 1 MB.

GET/timesheets

Scope: time.export · Read-only, paginated. Returns logged time entries across every project the key can reach.

Required header: Authorization: Bearer <api key>

Parameters

ParameterMandatoryDescriptionExample
projectIdNoRestrict to one project (UUID)projectId=11111111-...
fromNoOnly entries worked on or after this datefrom=2026-09-01
toNoOnly entries worked on or before this dateto=2026-09-30
updatedSinceNoOnly entries changed after this time, for incremental syncupdatedSince=2026-09-01T00:00:00Z
limitNoMaximum rows per page; default 500, maximum 1000limit=100
offsetNoPage offset; default 0offset=100
formatNocsv returns the same rows as CSV instead of JSONformat=csv

Example request

GET /api/integrations/timesheets?from=2026-09-01&to=2026-09-30&limit=100
Authorization: Bearer ptk_xxxxxxxxxxxxxxxxxxxx

Example response: 200 OK

{
  "entries": [
    {
      "id": "a1b2c3d4-0000-0000-0000-000000000001",
      "projectKey": "DDG",
      "projectName": "Dodgeball",
      "ticketKey": "DDG-142",
      "ticketTitle": "Fix login redirect",
      "person": "Jess Okafor",
      "personEmail": "jess@example.com",
      "minutes": 90,
      "hours": 1.5,
      "workDate": "2026-09-12",
      "sprint": "Sprint 24",
      "release": "2.4.0",
      "source": "timer",
      "externalId": null,
      "createdAt": "2026-09-12T15:04:00.000Z",
      "updatedAt": "2026-09-12T15:04:00.000Z"
    }
  ],
  "hasMore": true,
  "nextOffset": 100
}

Response fields

FieldTypeDescription
entries[].idUUIDTime entry id
entries[].projectKeystringProject's short key
entries[].projectNamestringProject name
entries[].ticketKeystringFor example DDG-142
entries[].ticketTitlestringTicket title
entries[].personstring or nullName of whoever logged the time; null if it was logged by someone outside Jonta
entries[].personEmailstring or null
entries[].minutesintegerMinutes logged
entries[].hoursnumberMinutes divided by 60, rounded to 2 decimal places
entries[].workDatedateYYYY-MM-DD
entries[].sprintstring or null
entries[].releasestring or nullFix version name
entries[].sourcestringFor example timer, manual, import
entries[].externalIdstring or null
entries[].createdAt, updatedAttimestampISO 8601
hasMorebooleantrue if more rows exist beyond this page
nextOffsetinteger or nullPass as offset on the next request; null once hasMore is false

Keep requesting with offset set to nextOffset while hasMore is true. If person is null, the time was logged by someone outside Jonta (externalId and source identify them instead).

Possible errors

StatusWhenExample
400projectId isn't a valid id{"error": "projectId must be a project id"}
400from or to isn't YYYY-MM-DD{"error": "from and to must be dates as YYYY-MM-DD"}
400updatedSince isn't a parseable ISO time{"error": "updatedSince must be an ISO time, e.g. 2026-10-01T00:00:00Z"}
401Key missing, malformed or revoked{"error": "An API key is required"}

GET/stats

Scope: stats.read · Read-only, no parameters. Returns a small set of aggregate, non-identifying counts for this Jonta instance. It's meant for polling on a schedule (for example a licence or usage check), not for reporting.

Required header: Authorization: Bearer <api key>

It never returns client names, people's names, emails, or anything else that identifies who's using the instance.

Example request

GET /api/integrations/stats
Authorization: Bearer ptk_xxxxxxxxxxxxxxxxxxxx

Example response: 200 OK

{
  "instanceName": "Jonta",
  "seatCount": 18,
  "activeProjectCount": 7
}

Response fields

FieldMeaning
instanceNameThe name configured for this deployment
seatCountActive internal users not tied to a client company
activeProjectCountProjects that aren't archived

Possible errors

StatusWhenExample
401Key missing, malformed or revoked{"error": "An API key is required"}

POST/deployments

Scope: deploy.write · Records a deploy against a project, typically called from a CI pipeline's deploy step. Calling it again with the same environment and versionLabel updates that deploy rather than creating a duplicate.

Required headers: Authorization: Bearer <api key> and Content-Type: application/json

Parameters

ParameterMandatoryDescriptionExample
projectId or projectKeyYes (one of)Which project this deploy belongs to"projectKey": "DDG"
environmentYesFor example production or staging; created automatically if new"environment": "production"
versionLabelYesMatched against the project's versions to link tickets"versionLabel": "2.4.0"
gitTagNo"gitTag": "v2.4.0"
commitShaNo7 to 40 hex characters"commitSha": "a1b2c3d"
statusNostarted, succeeded, failed or rolled_back; default succeeded"status": "succeeded"
externalIdNoYour own id for this deploy, for de-duplication and lookup"externalId": "gh-run-10234"
ticketKeysNoTickets shipped"ticketKeys": ["DDG-12", "DDG-18"]

Example request

POST /api/integrations/deployments
Authorization: Bearer ptk_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "projectKey": "DDG",
  "environment": "production",
  "versionLabel": "2.4.0",
  "commitSha": "a1b2c3d4e5f6",
  "status": "succeeded",
  "externalId": "gh-run-10234"
}

Example response: 201 Created

Returns 200 OK instead if it updated an existing record.

{
  "id": "b2c3d4e5-0000-0000-0000-000000000002",
  "environment": "production",
  "versionLabel": "2.4.0",
  "status": "succeeded",
  "created": true,
  "tickets": ["DDG-12", "DDG-18"],
  "warnings": [],
  "moves": []
}

Response fields

FieldTypeDescription
idUUIDDeployment id
environmentstringEnvironment name
versionLabelstringVersion label as sent
statusstringstarted, succeeded, failed or rolled_back
createdbooleantrue if this created a new deployment record; false if it updated an existing one with the same environment and versionLabel
ticketsstring[]Ticket keys linked to this deploy
warningsstring[]Non-fatal issues, for example an unrecognised versionLabel or ticket keys that don't exist
movesarrayReserved for future use; always empty today

warnings carries soft problems that didn't stop the deploy from recording, for example a versionLabel that doesn't match any version in the project, or ticket keys that don't exist.

Possible errors

StatusWhenExample
400Neither projectId nor projectKey sent{"error": "Send projectKey (for example DDG) or projectId"}
400commitSha isn't 7 to 40 hex characters{"error": "commitSha must be a hex commit id"}
404No project with that key or id{"error": "No project with that key or id"}
403This key is restricted to other projects{"error": "This key cannot record deploys for that project"}
401Key missing, malformed or revoked{"error": "An API key is required"}

POST/tickets

Scope: ticket.write · Creates a ticket in a project, for an external system raising an issue on Jonta's behalf (for example a monitoring tool, or a client portal outside Jonta).

Required headers: Authorization: Bearer <api key> and Content-Type: application/json

Parameters

ParameterMandatoryDescriptionExample
projectId or projectKeyYes (one of)Which project to create the ticket in"projectKey": "DDG"
titleYes1 to 300 characters"title": "Payment webhook failing"
descriptionNoPlain text, up to 50,000 characters"description": "Seen 14 times..."
priorityNolowest, low, medium, high or highest; default medium"priority": "high"
dueDateNo"dueDate": "2026-10-20"
typeIdNoA ticket type id (see GET /ticket-types); if omitted, the default "Task" type"typeId": "22222222-..."
externalIdNoYour own id for this ticket. Calling again with the same externalId returns the ticket already created, instead of making a new one"externalId": "jira-INC-4821"

Example request

POST /api/integrations/tickets
Authorization: Bearer ptk_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "projectKey": "DDG",
  "title": "Payment webhook failing with 500",
  "description": "Seen 14 times in the last hour, all for merchant acct 88213.",
  "priority": "high",
  "externalId": "jira-INC-4821"
}

Example response: 201 Created

Returns 200 OK instead if a ticket with that externalId already exists.

{
  "id": "c3d4e5f6-0000-0000-0000-000000000003",
  "key": "DDG-203",
  "projectId": "11111111-0000-0000-0000-000000000000",
  "number": 203,
  "title": "Payment webhook failing with 500",
  "description": "Seen 14 times in the last hour, all for merchant acct 88213.",
  "priority": "high",
  "externalId": "jira-INC-4821",
  "lane": { "id": "...", "name": "Backlog", "isDone": false },
  "type": { "id": "...", "name": "Task", "icon": "task", "colour": "#6b7280" },
  "assignee": null,
  "reporter": null,
  "dueDate": null,
  "createdAt": "2026-10-09T09:12:00.000Z",
  "updatedAt": "2026-10-09T09:12:00.000Z"
}

Response fields

The response is the full ticket record. The fields you'll most often use:

FieldTypeDescription
idUUIDTicket id
keystringFor example DDG-203
projectIdUUID
numberintegerTicket number within the project
titlestring
descriptionstring
prioritystringlowest, low, medium, high or highest
laneobject{ id, name, isDone }, the column the ticket starts in
typeobject{ id, name, icon, colour }
assigneeobject or nullnull until someone is assigned
reporterobject or nullnull for a ticket created by a key, because it wasn't raised by a person
dueDatedate or null
createdAt, updatedAttimestampISO 8601
externalIdstring or nullWhatever you sent when creating the ticket, or null if you didn't send one

externalId is echoed back in the response, so you can confirm what was stored without a separate lookup. Only the fields above are ones you set; everything else reflects Jonta's own defaults, such as which lane a new ticket lands in.

If a key is restricted to specific projects, naming any other project here returns 403.

Idempotent retries. Pass externalId with a value you control (your own request id, or an id from the system raising the ticket). Calling this endpoint again with the same externalId for the same project returns the ticket already created, with 200 instead of 201, so it's safe to retry on a timeout. Without externalId there's no de-duplication: a second call makes a second ticket.

Possible errors

StatusWhenExample
400Neither projectId nor projectKey sent{"error": "Send projectKey (for example DDG) or projectId"}
400title missing, empty or over 300 characters{"error": "String must contain at most 300 character(s)"}
400typeId doesn't match a ticket type{"error": "Invalid ticket type"}
404No project with that key or id{"error": "No project with that key or id"}
403This key is restricted to other projects{"error": "This key cannot create tickets for that project"}
401Key missing, malformed or revoked{"error": "An API key is required"}

GET/projects

Scope: time.export, deploy.write or ticket.write (any one) · Read-only. Lists the projects this key can reach: the id or key to use anywhere above that needs a projectId or projectKey.

Required header: Authorization: Bearer <api key>

Parameters

None. This endpoint takes no query parameters.

Example request

GET /api/integrations/projects
Authorization: Bearer ptk_xxxxxxxxxxxxxxxxxxxx

Example response: 200 OK

[
  { "id": "11111111-0000-0000-0000-000000000000", "key": "DDG", "name": "Dodgeball" },
  { "id": "22222222-0000-0000-0000-000000000000", "key": "ACM", "name": "Acme Rebrand" }
]

Response fields

FieldTypeDescription
idUUIDProject id
keystringProject's short key, for example DDG
namestringProject name

Only active (non-archived) projects are returned, filtered to whatever this key is allowed to reach: the same list every other endpoint checks against.

Possible errors

StatusWhenExample
401Key missing, malformed or revoked{"error": "An API key is required"}

GET/ticket-types

Scope: ticket.write · Read-only. Lists every ticket type, for picking a typeId to send to POST /tickets.

Required header: Authorization: Bearer <api key>

Parameters

None. This endpoint takes no query parameters.

Example request

GET /api/integrations/ticket-types
Authorization: Bearer ptk_xxxxxxxxxxxxxxxxxxxx

Example response: 200 OK

[
  { "id": "33333333-0000-0000-0000-000000000000", "name": "Task", "icon": "task", "colour": "#6b7280" },
  { "id": "44444444-0000-0000-0000-000000000000", "name": "Bug", "icon": "bug", "colour": "#ef4444" }
]

Response fields

FieldTypeDescription
idUUIDTicket type id; send as typeId to POST /tickets
namestring
iconstring
colourstringHex colour

Ticket types aren't scoped to a project or filtered by the key's project list. They're shared across the whole instance.

Possible errors

StatusWhenExample
401Key missing, malformed or revoked{"error": "An API key is required"}

Support

Questions about scopes, an unexpected error, or a key you think has leaked? Email contact@jonta.com and we'll reply within one working day. If a key may have leaked, revoke it straight away in Settings → Integrations → API keys, then create a new one.

Free for up to 5 users

Bring support, delivery and your wiki into one place.

Every feature on every plan. £20 a month for up to 50 users after that, and free migration from Jira and Confluence.