Skip to content

Managing API Access

Authentication covers minting a token, business scoping, scopes, IP allowlists, rate limits and the client-credentials request itself. This guide covers everything around the credential's life: asking for API access, listing and revoking tokens, registering and operating partner API clients, disconnecting connected apps (MCP OAuth grants), and what each revocation does to live sessions. The Settings screen for all of this is described in the Developers article of the help center; the routes below are what that screen calls.

All paths are relative to https://api.ondayzero.com. None of these routes take x-business-id: credentials belong to a person, not a business. Successful responses use the usual {"success": true, "data": ...} envelope (examples below show data) and errors follow Error Handling.

What exists, so you do not go looking for the rest:

Area Routes Notes
API access GET /api/v1/tokens/access, POST /api/v1/tokens/access-request Ask DayZero for access, check the answer
API tokens GET /api/v1/tokens, POST /api/v1/tokens/{token_id}/revoke, DELETE /api/v1/tokens/{token_id}, POST /api/v1/tokens/{token_id}/rotate, GET /api/v1/tokens/client-ip Create is in Authentication. There is no GET /tokens/{id}; read a token's record from the list
API clients /api/v1/api-clients and its sub-routes There is no delete; a client is disabled (and can be re-enabled)
Connected apps GET /api/v1/oauth/grants, DELETE /api/v1/oauth/grants/{grant_id}, POST /api/v1/oauth/grants/{grant_id}/revoke MCP clients such as Claude or Cursor
OAuth token services POST /api/v1/oauth2/partner/token, /token, /exchange, /refresh See OAuth token endpoints

There is no public audit endpoint for credentials. API client changes are written to the audit trail (visible in the app's Audit Log), but the audit routes are internal and not part of the public API. For an activity signal on a token, read last_used_at from the list; unusual use (bursts over budget, blocked IPs, use after revocation, a first sighting from a new source network, meaning a new /24 for IPv4 or /48 for IPv6) also alerts the token's owner.

Who may manage whose credentials

Only the owner. Every token, API client and connected-app route is scoped to the signed-in user. Another user's token id, client id or grant id answers 404, exactly like an id that does not exist. A firm owner or admin cannot list, rotate or revoke a staff member's tokens through the API.

What a firm owner or admin can do is switch API access off for a staff member (PATCH /api/v1/advisory-firms/{firm_id}/users/{user_id}/api-access, see API access per person). The switch can only restrict: approval always comes from DayZero.

Route API token may call it Needs a signed-in web session Two-factor step-up Needs approved API access
GET /tokens/access, POST /tokens/access-request, GET /tokens/client-ip Yes No No No
GET /tokens Yes No No Yes
POST /tokens/{id}/revoke, DELETE /tokens/{id} Yes No No No
POST /tokens, POST /tokens/{id}/rotate No (403 AUTH_057) Yes Yes Yes
GET/POST /api-clients, GET/PATCH /api-clients/{id} No (403 AUTH_057) Yes Create, and PATCH of anything but name/description Yes
PUT /api-clients/{id}/jwks, POST .../signing-keys, POST .../enable No Yes Yes Yes
POST .../signing-keys/{kid}/retire, POST .../disable No Yes No Yes
/oauth/grants routes Yes No No No

Notes on the table:

  • "Two-factor step-up" is the same check as minting a token: the account needs a second factor enrolled and the session must have verified both factors recently (403 AUTH_050 if the session has none, AUTH_056 if the check is stale). Revoking never asks for it, so an incident responder can cut access off in one call.
  • An API token still obeys its own scope. A read_only token can list but cannot revoke or delete (403 AUTH_048), and a credit* token is limited to the credit API, so it cannot reach any route in this guide. A leaked full-access key can therefore revoke tokens but never mint, rotate or widen them.
  • MCP OAuth tokens never work on these routes (or anywhere else on the REST API).
  • Listing tokens needs approved API access; revoking and deleting do not, so you can always clean up after access is taken away.

Request API access

API access is off for every account until a DayZero operator approves a request.

bash
curl -X POST "https://api.ondayzero.com/api/v1/tokens/access-request" \
  -H "Authorization: Bearer SESSION_OR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "use_case_kind": "reporting_sync",
    "use_case": "Nightly sync of paid invoices into our data warehouse, read-only, from one server.",
    "company_name": "Northwind Advisory"
  }'
Field Notes
use_case_kind One of internal_automation, reporting_sync, firm_client_workflows, product_integration, ai_agent, migration, evaluating, other
use_case Your own words, up to 2000 characters. Optional with a listed kind other than other; otherwise at least 20 characters (400 if too short)
company_name Optional, up to 255 characters

Send either a kind or a use_case of 20+ characters. The response is the access state (below) with status pending. DayZero emails the reviewers, and emails you when the request is approved or denied.

Constraints: one open request at a time (409 CONFLICT_095), nothing to request once approved (409 CONFLICT_096), and three requests per account per day (429). A denied or revoked account may ask again.

bash
curl "https://api.ondayzero.com/api/v1/tokens/access" \
  -H "Authorization: Bearer SESSION_OR_API_TOKEN"
json
{
  "status": "pending",
  "can_request": false,
  "request": {
    "id": "01912345-abcd-7000-8000-0000000000a1",
    "status": "pending",
    "use_case_kind": "reporting_sync",
    "use_case_kind_label": "Syncing data to a warehouse or BI tool",
    "use_case": "Nightly sync of paid invoices into our data warehouse, read-only, from one server.",
    "company_name": "Northwind Advisory",
    "requested_at": "2026-10-11T14:02:11Z",
    "decided_at": null,
    "decision_note": null
  }
}
status Meaning
not_requested Never asked; request is null
pending Waiting for DayZero
approved You may create and use tokens
denied Turned down; decision_note may say why. can_request is true
revoked Taken away after approval. can_request is true
firm_disabled Approved, but your firm switched API access off for you

can_request is true only for not_requested, denied and revoked.

What removing access does. The check runs on every API-token request, not only when you manage tokens. When your firm switches access off, every token you hold is refused with 403 AUTH_047 immediately; the tokens stay in your list and work again if the switch is turned back on. When DayZero revokes an approval, all your active tokens are revoked (reason "API access revoked by DayZero") and stay revoked if access is approved again, so you mint new ones. Until access is approved, creating, rotating and listing tokens answer 403 AUTH_069.

API tokens

GET /api/v1/tokens/client-ip returns {"ip": "203.0.113.10"}, the address your request arrives from, handy for filling in allowed_ips when minting.

List tokens

bash
curl "https://api.ondayzero.com/api/v1/tokens?limit=20&include_revoked=true" \
  -H "Authorization: Bearer dz_your_token_here"

The list is cursor-paginated. By default it omits revoked tokens; include_revoked=true adds them. Tokens that a partner API client obtained through the client-credentials grant are never listed here (they are minted by the hundred and live minutes); see API clients.

json
{
  "items": [
    {
      "id": "01912345-abcd-7000-8000-0000000000b2",
      "jti": "Zk3p9Qm1xV7c0TnB5uYw2hLd",
      "name": "Month-end close agent",
      "description": "Read-only token for the reporting assistant",
      "token_prefix": "dz_Zk3p9Qm1",
      "token_type": "api",
      "scopes": "read_only",
      "allowed_ips": ["203.0.113.10"],
      "rate_limit_per_minute": 600,
      "business_ids": ["01912345-abcd-7000-8000-000000000001"],
      "status": "active",
      "rotated_from_id": null,
      "rotated_to_id": null,
      "grace_until": null,
      "created_at": "2026-10-01T09:30:00",
      "last_used_at": "2026-10-11T13:58:02",
      "expires_at": "2026-12-30T09:30:00",
      "revoked_at": null,
      "revocation_reason": null
    }
  ],
  "next_cursor": null,
  "prev_cursor": null,
  "has_next": false,
  "has_prev": false,
  "limit": 20,
  "total": null
}

id is what the revoke, delete and rotate routes take (not jti). scopes is a space-separated string; a token with no explicit scope shows api. allowed_ips, business_ids and rate_limit_per_minute are null when unrestricted or on the platform default. The plaintext token is never returned. status is active, rotating, revoked or expired.

Revoke a token

bash
curl -X POST "https://api.ondayzero.com/api/v1/tokens/01912345-abcd-7000-8000-0000000000b2/revoke" \
  -H "Authorization: Bearer dz_your_admin_token_here" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Laptop lost"}'

The body and reason (free text) are optional. The record is kept and marked revoked.

json
{
  "id": "01912345-abcd-7000-8000-0000000000b2",
  "jti": "Zk3p9Qm1xV7c0TnB5uYw2hLd",
  "name": "Month-end close agent",
  "revoked_at": "2026-10-11T14:10:44",
  "revoked_by": "5f0c2b1e-0000-4000-8000-000000000000",
  "revocation_reason": "Laptop lost"
}

revoked_by is the revoking user's id (the schema describes it as an email, but the service stores the id; treat it as an opaque value). Revoking is permanent: there is no un-revoke. Revoking a token that is already revoked is 400 VALIDATION_107. An unknown id, or one that belongs to someone else, is 404 NOT_FOUND_049.

You can revoke the token you are calling with; the response is returned, and the next request with it fails.

Delete a token

DELETE /api/v1/tokens/{token_id} blacklists the token and then removes its record. It returns the standard message envelope. Prefer revoke: a deleted token leaves no row to show when it was last used or why it was withdrawn.

Rotate a token

Rotation mints a replacement that inherits the name, description, scopes, IP allowlist, rate limit and business limit, and keeps the old token working for an overlap window. It is a web-app call (session plus two-factor step-up); an API token gets 403 AUTH_057. The request body is optional:

json
{ "overlap_hours": 24, "expires_in_days": 90 }

overlap_hours is 0 to 168 (default 24; 0 revokes the old token at once). expires_in_days is 1 to 365 and defaults to the old token's original lifetime. The response:

json
{
  "token": "dz_the_new_token_shown_once",
  "token_info": { "id": "01912345-abcd-7000-8000-0000000000c3", "status": "active", "rotated_from_id": "01912345-abcd-7000-8000-0000000000b2" },
  "replaced_token": { "id": "01912345-abcd-7000-8000-0000000000b2", "status": "rotating", "grace_until": "2026-10-12T14:15:00", "rotated_to_id": "01912345-abcd-7000-8000-0000000000c3" }
}

(token_info and replaced_token carry the full record shown in the list.) Rotation rules:

  • Until grace_until both tokens work. After it the old token is refused with 401 AUTH_059, and use of a rotated-out token is reported to the owner as unusual activity.
  • A token can be rotated once (400 VALIDATION_473); rotate the replacement next time. A revoked or expired token cannot be rotated (400 VALIDATION_474).
  • Rotation counts against the same budget as minting: 10 creates or rotations per user per hour (429), but a token that has been rotated (still inside its overlap window) does not count toward the 10-active-token limit, so rotating at the limit works.
  • You can revoke the old token early, before grace_until, once your integration has switched over.

API clients

An API client is the hardened alternative to a long-lived token, for a partner's servers. You register the public half of a signing key; the partner signs a short-lived assertion and exchanges it at POST /api/v1/oauth2/partner/token for an access token that lives 1 to 60 minutes. No long-lived secret leaves the partner. The request format for that exchange is in Authentication; the assertion's claims are below.

Clients are provisioned for approved partners and are managed by the owner, the DayZero user the client acts as. Tokens a client obtains act as that user, with the client's scopes, IPs, rate limit and (when the token request asks) business limit. All routes need a signed-in web session with approved API access. Each user may own up to 10 active clients (400 VALIDATION_481).

Create a client

POST /api/v1/api-clients (two-factor step-up; 10 creations per user per hour).

json
{
  "name": "Tie production",
  "description": "Tie's payment job",
  "scopes": ["credit:payments"],
  "allowed_ips": ["203.0.113.10"],
  "rate_limit_per_minute": 600,
  "token_ttl_seconds": 900,
  "jwks": {
    "keys": [
      { "kty": "EC", "crv": "P-256", "kid": "tie-2026-10", "use": "sig", "alg": "ES256", "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0" }
    ]
  },
  "require_mtls": false,
  "require_signed_requests": true
}
Field Notes
name Required, 1 to 255 characters
description Optional, up to 2000 characters
scopes At least one of api (full access), read_only, credit, credit:read, credit:payments; default ["credit:read"]. Same rules as tokens: a credit scope cannot be combined with api or read_only
allowed_ips Up to 32 IPv4/IPv6 addresses or CIDR ranges. Applies to token requests and to using the issued tokens. Unlike tokens, cloud presets are not accepted. Omit for any IP
rate_limit_per_minute 10 to 6,000 per issued token; omit for the default
token_ttl_seconds Lifetime of each issued token, 60 to 3600; default 900
jwks Required. A JWK set of public keys: RS256 (RSA, 2048 bits or more), ES256 (P-256) or EdDSA (Ed25519). Up to 10 keys; with more than one key every key needs a unique kid. Private members are refused (400 VALIDATION_475)
require_mtls Bind each token to the client certificate presented at the token endpoint. Refused (400 AUTH_075) where the load balancer does not forward certificates; check mtls_available in the list response
mtls_cert_thumbprints Optional pin list of up to 20 SHA-256 certificate thumbprints (hex or base64url)
require_signed_requests Payment writes must carry a request signature; see Signed payment requests

The response is the client record. Put its client_id (dzc_...) in both iss and sub of every assertion:

json
{
  "id": "01912345-abcd-7000-8000-0000000000d4",
  "client_id": "dzc_3f9a1c07b2e84d5a6c1e90ab",
  "name": "Tie production",
  "status": "active",
  "scopes": ["credit:payments"],
  "allowed_ips": ["203.0.113.10"],
  "rate_limit_per_minute": 600,
  "token_ttl_seconds": 900,
  "jwks": { "keys": ["..."] },
  "key_ids": ["tie-2026-10"],
  "require_mtls": false,
  "mtls_cert_thumbprints": null,
  "require_signed_requests": true,
  "signing_keys": [],
  "active_token_count": 0,
  "last_token_issued_at": null,
  "disabled_at": null,
  "created_at": "2026-10-11T14:20:00Z",
  "updated_at": null,
  "mtls_available": false
}

There is no client secret to rotate or lose: authentication is the assertion signature. Secrets only exist for signing keys (below), and they are shown once.

List, get and update

  • GET /api/v1/api-clients returns {"items": [...], "mtls_available", "token_endpoint", "assertion_audiences", "max_clients"}. It is not paginated and returns no secrets. token_endpoint and assertion_audiences are exactly what the partner needs to build assertions.
  • GET /api/v1/api-clients/{client_pk} returns one client (404 NOT_FOUND_109 for an unknown or foreign id). client_pk is the id, not the client_id.
  • PATCH /api/v1/api-clients/{client_pk} changes only the fields you send: name, description, scopes, allowed_ips, rate_limit_per_minute, token_ttl_seconds, require_mtls, mtls_cert_thumbprints, require_signed_requests. Send allowed_ips: [] to allow any IP. A rename or description change needs only a session; changing anything else needs the two-factor step-up. Changes apply to tokens issued from then on; tokens already issued keep their claims until they expire (at most token_ttl_seconds).

Rotate the signing key

PUT /api/v1/api-clients/{client_pk}/jwks with {"jwks": {...}} replaces the whole key set (step-up). To rotate with no gap: upload old and new keys together, switch the partner's signer to the new key, then upload the new key alone. Tokens already issued stay valid until they expire.

Request-signing keys

Clients that sign payment writes register a signing key per environment (up to 5 live at once; 400 VALIDATION_477 beyond that or for a duplicate kid).

POST /api/v1/api-clients/{client_pk}/signing-keys (step-up):

Field Notes
alg hmac-sha256 or ed25519
kid Optional, A-Za-z0-9._-, 1 to 64 characters; generated (sk_...) when omitted
public_key Ed25519 only: the raw 32-byte public key, base64 or base64url. Sending it with hmac-sha256 is a 400

For hmac-sha256 DayZero generates the shared secret and returns it once in secret; it is stored encrypted and never shown again. The response is {"client": {...}, "signing_key": {"kid", "alg", "public_key", "created_at", "retired_at", "status"}, "secret": "..."} with secret null for Ed25519. A key's status is active, retiring (retirement scheduled) or retired.

POST /api/v1/api-clients/{client_pk}/signing-keys/{kid}/retire takes an optional {"overlap_hours": 0-168} (default 0, retire now) so the partner can roll out a replacement first. A retirement can be brought forward but never pushed back. Unknown kid is 404 NOT_FOUND_111. No step-up is needed. Which headers carry the signature is described in the credit guide linked above.

Disable and re-enable (revoke a client)

POST /api/v1/api-clients/{client_pk}/disable (no step-up) stops the client and revokes every live access token issued to it, effective on the next request:

json
{ "client": { "id": "01912345-abcd-7000-8000-0000000000d4", "status": "disabled", "disabled_at": "2026-10-11T15:00:00Z" }, "revoked_token_count": 3 }

The partner's token requests then fail with 401 invalid_client and AUTH_067. Disabling is repeatable.

POST /api/v1/api-clients/{client_pk}/enable (step-up) re-activates it. Tokens revoked at disable time stay revoked; the partner simply requests new ones. Enabling re-checks the 10-active-clients limit and, for require_mtls, that mTLS is still available.

Partner token requests: assertion rules

Authentication gives the form fields of POST /api/v1/oauth2/partner/token. The assertion is a JWT you sign with a key in the client's JWK set:

Claim Requirement
header alg RS256, ES256 or EdDSA, and it must match the registered key's type. none and HMAC are refused
header kid Required when the client has more than one key
iss, sub Both equal the client's client_id
aud A value from assertion_audiences in the client list (the token endpoint URL is the standard choice)
iat, exp Required; exp minus iat at most 300 seconds; 30 seconds of clock skew is tolerated
jti Required and never reused: a repeat is refused (401 AUTH_076). Use a fresh random value per request

The optional scope can only narrow the client's scopes (credit covers credit:read and credit:payments; api covers read_only); anything else is 400 invalid_scope. business_id accepts one or more space- or comma-separated UUIDs of businesses the owner can reach and limits the token to them. The token expires after the client's token_ttl_seconds and carries the client's IP allowlist, rate limit and (with mTLS) its certificate binding.

error Status Typical cause
invalid_client 401 Bad or replayed assertion, unknown or disabled client (AUTH_067), IP not allowed (AUTH_028), missing or wrong certificate
invalid_grant 400 The owner is inactive (AUTH_078) or lost API access (AUTH_069, AUTH_047)
invalid_scope 400 scope outside what the client holds
unsupported_grant_type 400 grant_type is not client_credentials (AUTH_077)
invalid_request 400 A business_id that is not a UUID or not reachable
rate_limited 429 60 requests per minute per source IP, or 30 issued tokens per minute per client; wait for Retry-After

If Redis-backed replay protection is briefly unavailable the endpoint answers 503 rather than issue a token it cannot protect; retry shortly. Responses are never cached.

Connected apps (OAuth grants)

When you approve an MCP client such as Claude, ChatGPT or Cursor, DayZero records a grant (see MCP Server for the sign-in flow). Grants are about MCP access only; they do not touch REST API tokens.

bash
curl "https://api.ondayzero.com/api/v1/oauth/grants" \
  -H "Authorization: Bearer dz_your_token_here"
json
{
  "items": [
    {
      "id": "01912345-abcd-7000-8000-0000000000e5",
      "client_id": "mcp_client_abc",
      "client_name": "Claude",
      "scopes": ["mcp:read", "mcp:write", "offline_access"],
      "granted_at": "2026-09-20T10:00:00",
      "last_used_at": "2026-10-11T12:30:00",
      "business_id": null,
      "business_name": null
    }
  ]
}

Only active grants are listed, in a single unpaginated array. business_id and business_name are set when the person limited that connection to one business on the consent screen (every token of the grant then reaches only that business) and null when it reaches every business they can. One app can hold several business-bound connections next to one unbound one, and each is its own grant. To disconnect one:

bash
curl -X DELETE "https://api.ondayzero.com/api/v1/oauth/grants/01912345-abcd-7000-8000-0000000000e5" \
  -H "Authorization: Bearer dz_your_token_here"

POST /api/v1/oauth/grants/{grant_id}/revoke is an alias that also accepts an optional {"reason": "..."}. The response is {"id", "client_id", "client_name", "revoked_at"}. grant_id must be a UUID (400 otherwise); an unknown or foreign grant is 404 NOT_FOUND_094; revoking twice is 400 VALIDATION_107.

Revoking blacklists the grant's refresh token and every active MCP access and refresh token issued under it, including ones from earlier refreshes, so the app loses access on its next call rather than when its access token would have expired. Revoking one business-bound connection leaves the person's other connections for the same app alone. The app can connect again by going through the consent screen, which reactivates the same grant record.

What revoking does

Action Effect on live use Effect on everything else
Revoke or delete an API token That token is refused on its next request (401) Other tokens, your web session, MCP grants and API clients are untouched
Rotate with an overlap Both tokens work until grace_until, then the old one gets 401 AUTH_059 Replacement inherits all limits
Rotate with overlap_hours 0 Old token refused at once
Disable an API client All its live access tokens are revoked at once; new token requests fail Re-enabling does not restore them. The owner's own tokens are untouched
Revoke a connected app Its MCP access and refresh tokens are blacklisted at once REST API tokens are untouched; the user's session is untouched
Firm switches API access off All of the person's tokens and clients are refused (AUTH_047) Nothing is revoked; turning it back on restores them
DayZero revokes API access All the person's active tokens are revoked Approval later does not revive them
Owner's second factor removed Their tokens are revoked and their API clients disabled, and they are emailed Turning two-factor back on does not revive them; mint new ones

OAuth token endpoints

Beyond the partner endpoint, /api/v1/oauth2 serves three more routes. They are provisioned for DayZero's own apps and partner backends; for your own integrations use an API token.

  • POST /api/v1/oauth2/token (form-encoded) issues an access and refresh token for the user named by user_email, authenticated by a shared-secret signature: send client_id, timestamp (Unix seconds, within 5 minutes of now), a single-use nonce, and signature, the hex HMAC-SHA256 of client_id|timestamp|nonce|user_email. It answers 404 unless enabled for the deployment, may be limited to an IP allowlist (403 AUTH_028), is limited to 30 requests per minute per IP, and never issues tokens for DayZero staff (403 AUTH_093) or inactive accounts. Signature problems are 401 (AUTH_030 bad signature, AUTH_031 nonce reused, AUTH_032 stale timestamp).
  • POST /api/v1/oauth2/refresh (form-encoded refresh_token, and client_id when the token is bound to one) returns a new access token and rotates the refresh token: use the new one next time. Replaying an already-rotated refresh token after a short grace window (about 5 minutes) is treated as theft and revokes the whole token family. 60 requests per minute per IP.
  • POST /api/v1/oauth2/exchange is called by the DayZero web app. It trades a single-use authorization code (10-minute lifetime) from the sign-in redirect, or a refresh token, for tokens. It needs no client credentials and is limited to 30 requests per minute per IP. Integrations should not use it.

All three return {"access_token", "token_type": "Bearer", "expires_in", "refresh_token", "scope"} outside the usual envelope. The MCP authorization server (/oauth2/register, /oauth2/authorize, /oauth2/mcp/token and the .well-known metadata) is served separately and documented in the MCP Server guide.

Errors at a glance

Status code Meaning
400 VALIDATION_107 Token or grant already revoked
400 VALIDATION_166 10 active tokens already
400 VALIDATION_473 / VALIDATION_474 Token already rotated / cannot be rotated (revoked or expired)
400 VALIDATION_475 / 477 / 481 Invalid JWK set / invalid signing key / 10 active clients
400 AUTH_075 require_mtls is not available in this environment
403 AUTH_050 / AUTH_056 / AUTH_057 No second factor on the session / stale check / an API token cannot do this
403 AUTH_069 / AUTH_047 API access not approved / switched off by your firm
403 AUTH_048 The calling token's scope does not allow it
404 NOT_FOUND_049 / NOT_FOUND_109 / NOT_FOUND_111 / NOT_FOUND_094 Token / API client / signing key / grant not found (or not yours)
409 CONFLICT_095 / CONFLICT_096 Access request already open / access already approved
429 GEN_005 Creation, rotation or access-request limit hit; wait and retry

Not covered

Operator-side approval of access requests and admin token management are internal and not part of the public API. Webhook endpoints for partner credit are covered in the Credit and Webhooks guides, and the interactive MCP consent flow in MCP Server.