Preskočiť na obsah

OAuth access tokens

Besides API keys, the Developer API accepts OAuth access tokens. They are meant for applications that a person connects to their own Dakoty account, such as the Dakoty app for Zapier. A person approves the connection once, chooses the organization, and the application then calls the API as that person. Your own scripts and the Make and n8n integrations keep using API keys, and nothing changes for existing keys.

Only applications registered by Dakoty can ask for a connection. There is no open client registration for the REST API. Each application has a client id that starts with dkapp_, a client secret kept on its server, and a fixed list of scopes it may request. The authorization flow is the standard authorization code flow with PKCE (S256), with client_secret_post or client_secret_basic client authentication on the token endpoint. The metadata that clients read is published at /.well-known/oauth-authorization-server, and the protected resource at /.well-known/oauth-protected-resource/api/v1/public.

An access token starts with dk_oat_ and lives one hour. A refresh token starts with dk_ort_ and is rotated on every use. If a connection stays unused for 180 days, Dakoty ends it. The person can disconnect it at any time, and removing the person from the organization or deactivating the user ends it at once.

Send the access token like an API key:

Terminal window
curl -sS https://api.dakoty.com/api/v1/public/me \
-H "Authorization: Bearer $DAKOTY_ACCESS_TOKEN"

An unknown, expired or revoked token returns 401 UNAUTHORIZED with a WWW-Authenticate header that points to the protected-resource metadata:

WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://api.dakoty.com/.well-known/oauth-protected-resource/api/v1/public"

A 401 is the signal to refresh the token. A temporary failure on our side returns 503, never 401, so a client does not throw its connection away because of it.

Access is the granted scopes and the role of the person

Section titled “Access is the granted scopes and the role of the person”

The scopes on a connection are what the person agreed to hand over. They never give more than the person’s role allows. On every request Dakoty checks both: the token must hold the scope of the endpoint, and the current role of the connecting person in that organization must back it. The role is read at request time, so a change applies to the next call: promote the person and the call works, demote or remove a permission and the call is refused, with the same token.

ScopeRole permissions the person needs
companies:read, clients:read, leads:read, activities:read, tasks:readview permission of the entity (for example leads.view)
tasks:read on GET /projectsadditionally projects.view (a role with tasks.view only gets 403 ROLE_PERMISSION_MISSING there)
companies:write, clients:write, leads:write, activities:write, tasks:writeedit and create permission of the entity (for example leads.edit and leads.create)
invoices:read / invoices:writebudget.view / budget.edit and budget.create
bookings:read / bookings:writebooking.view / booking.create and booking.edit
orders:read / orders:writeorders.view / orders.edit
users:readusers.view
webhooks:managenone, the scope is enough

Each permission must be granted for all records. The API cannot filter a list down to the person’s own records, so a role that sees only assigned leads is refused instead of seeing colleagues’ leads. Owners of the organization pass the role check, but still only for scopes that are on the connection. A scope that is not listed above is never usable with an OAuth token.

Two errors tell the client why a call was refused. Both are 403, and the message is Slovak when you send Accept-Language: sk or cs:

{
"error": {
"code": "ROLE_PERMISSION_MISSING",
"message": "Your role in Dakoty does not allow this (missing permission: leads.view).",
"detail": null
}
}
{
"error": {
"code": "ROLE_SCOPE_LIMITED",
"message": "Your role can see only your own records; this connection needs access to all records.",
"detail": null
}
}

ROLE_PERMISSION_MISSING means the role lacks a permission and an administrator can add it. ROLE_SCOPE_LIMITED means the permission exists but only for assigned records. A call whose scope is missing from the connection still returns 403 FORBIDDEN, as for API keys. Reconnecting does not help in either role case: the person (or an administrator) has to change the role.

The same rule covers REST hooks: subscribing to an event the role cannot read answers 403, and deliveries stop while the role lacks the permission; see REST hooks. The connection itself is not an API key: it does not appear in the list of API keys and does not count towards the organization’s key limit.

The consent screen checks each requested scope against the person’s role in the selected organization (the same check that runs on every call). A scope the role does not back is not removed from the connection. It is granted, but calls that need it return 403 until an administrator gives the role the permission. The check is repeated when the person picks another organization. A scope can need more than one permission (see the table above), so a scope shown as allowed may still be refused on a single endpoint.

Upcoming: the greyed-out display of such scopes with the reason, and the re-check on organization switch in the consent screen UI, ship with the next Dakoty web release.

Approved connections appear under Connected applications in the person’s profile; administrators with the integrations permission also see the connections of the whole organization there. Each Zapier connection lists its scopes and the number of REST hooks (the Zaps that listen for Dakoty events). Disconnecting it revokes the connection together with its access and refresh tokens and deletes all of its REST hooks, so Zaps stop receiving events at once. Removing the person from the organization has the same effect. The approval is recorded in the audit log with the scopes and the connection kind; the disconnect with the connection kind and the number of deleted hooks.

Disconnecting is done in the Dakoty app; it is not part of the developer API.

GET /me needs no scope. For an OAuth token it also names the person who connected the application, and token.id is the id of the connection record that owns the REST hooks of the application:

{
"data": {
"organization": { "id": "…", "name": "Acme s.r.o.", "slug": "acme" },
"token": { "id": "…", "name": "Zapier – Jana Nováková", "scopes": ["leads:read", "leads:write", "webhooks:manage"], "expires_at": null },
"auth": "oauth",
"user": { "id": "…", "name": "Jana Nováková", "email": "jana@example.com" },
"api_version": "2026-10-01",
"limits": { "requests_per_minute": 120, "org_requests_per_minute": 600 }
}
}

For an API key the response has "auth": "api_key" and no user field. Rate limits are counted per connection and per organization, like for keys. A lapsed organization subscription returns 402 PAYMENT_REQUIRED for both kinds of credentials.