---
title: "Notifications & workspace endpoints — API reference | Data Tactical Group"
route: "/docs/api/reference/notifications-and-workspace"
updated: "2026-10-23"
---

# Notifications & workspace

6 endpoints, generated from the API's own OpenAPI document. The in-app notifications and the app's saved workspace.

## GET `/v2/account/workspace`

**The autosaved workspace**

Credentials: API key or App session. Operation id `acctGetWorkspace`.

Either credential (an API key or a session); resolves the caller's active organization (`X-DTG-Org`). Any role. D53: the person's own saved state for the active organization (switching organization switches workspace; nobody reads anyone else's). Not paused by a past-due balance.

### Parameters

**`X-DTG-Org` header · string (uuid)**

The organization this request acts in (a uuid). Omitted: the caller's own organization, else their earliest active membership. An API key is bound to its organization and may only name that one.

### Responses

#### `200` success

The stored object, or null when none was ever saved.

As `application/json`.

#### `401` error

Unauthorized. Codes: [`key_revoked`](/docs/api/errors#code-key-revoked) (the API key was revoked; `revoked_at`, `reason`, and a sentence saying when and why); [`unauthenticated`](/docs/api/errors#code-unauthenticated) (no API key or session; the key is unknown, or its account, membership or organization is no longer active; the session has expired, passed its 30-day maximum age, or belongs to an unverified email address).

As `application/problem+json`.

#### `403` error

Forbidden. Codes: [`no_membership`](/docs/api/errors#code-no-membership) (the account has no active membership in an active organization), [`account_suspended`](/docs/api/errors#code-account-suspended) (O56: the signed-in person's own account is suspended by DTG; `reason` where recorded), [`org_suspended`](/docs/api/errors#code-org-suspended) (the selected organization is suspended by DTG; the body carries `organization {id, name}` and `reason`), [`org_closed`](/docs/api/errors#code-org-closed) (the selected organization is closed; the body carries `organization {id, name}`), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (a key of the mode this process does not serve: a sandbox key on the live API, or a live key on the sandbox; the body carries `expected` and `base_url`, the address that serves it).

As `application/problem+json`.

#### `404` error

Not Found. Codes: [`not_found`](/docs/api/errors#code-not-found) (the `X-DTG-Org` header is not a uuid, names an organization the caller is not an active member of, or, on an API key, names an organization other than the key's own).

As `application/problem+json`.

#### `429` error

Only the per-minute rate limit: [`rate_limited`](/docs/api/errors#code-rate-limited) (120 requests a minute per credential, 300 per organization; lower ceilings for a sandbox key), with `Retry-After` and `X-RateLimit-*`.

As `application/problem+json`.

**`Retry-After` header**

Seconds to wait before retrying.

**`X-RateLimit-Limit` header**

The per-minute request ceiling that applies.

**`X-RateLimit-Remaining` header**

Requests left in the current window.

**`X-RateLimit-Reset` header**

Seconds until the window refills: on every answer, the time until the bucket is full again; on a refusal, until a request is allowed (as `Retry-After`).

Problem codes: [`key_revoked`](/docs/api/errors#code-key-revoked) (401), [`unauthenticated`](/docs/api/errors#code-unauthenticated) (401), [`no_membership`](/docs/api/errors#code-no-membership) (403), [`account_suspended`](/docs/api/errors#code-account-suspended) (403), [`org_suspended`](/docs/api/errors#code-org-suspended) (403), [`org_closed`](/docs/api/errors#code-org-closed) (403), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (403), [`not_found`](/docs/api/errors#code-not-found) (404), [`rate_limited`](/docs/api/errors#code-rate-limited) (429). Every problem is a [problem document](/docs/api/errors).

## PUT `/v2/account/workspace`

**Save the workspace**

Credentials: API key or App session. Operation id `acctPutWorkspace`.

Either credential (an API key or a session); resolves the caller's active organization (`X-DTG-Org`). Any role. Stores the object as given (last write wins) and echoes it back.

### Parameters

**`X-DTG-Org` header · string (uuid)**

The organization this request acts in (a uuid). Omitted: the caller's own organization, else their earliest active membership. An API key is bound to its organization and may only name that one.

### Request body

Required; `application/json`.

### Responses

#### `200` success

The stored object.

As `application/json`.

#### `400` error

Bad Request. Codes: [`workspace_invalid`](/docs/api/errors#code-workspace-invalid) (the body is not a JSON object), [`workspace_too_large`](/docs/api/errors#code-workspace-too-large) (over 32 KB serialized; the body carries `max_bytes`), [`malformed_json`](/docs/api/errors#code-malformed-json) (the body is not valid JSON, or uses an unsupported content encoding).

As `application/problem+json`.

#### `401` error

Unauthorized. Codes: [`key_revoked`](/docs/api/errors#code-key-revoked) (the API key was revoked; `revoked_at`, `reason`, and a sentence saying when and why); [`unauthenticated`](/docs/api/errors#code-unauthenticated) (no API key or session; the key is unknown, or its account, membership or organization is no longer active; the session has expired, passed its 30-day maximum age, or belongs to an unverified email address).

As `application/problem+json`.

#### `403` error

Forbidden. Codes: [`no_membership`](/docs/api/errors#code-no-membership) (the account has no active membership in an active organization), [`account_suspended`](/docs/api/errors#code-account-suspended) (O56: the signed-in person's own account is suspended by DTG; `reason` where recorded), [`org_suspended`](/docs/api/errors#code-org-suspended) (the selected organization is suspended by DTG; the body carries `organization {id, name}` and `reason`), [`org_closed`](/docs/api/errors#code-org-closed) (the selected organization is closed; the body carries `organization {id, name}`), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (a key of the mode this process does not serve: a sandbox key on the live API, or a live key on the sandbox; the body carries `expected` and `base_url`, the address that serves it). A sandbox key: [`sandbox_read_only`](/docs/api/errors#code-sandbox-read-only) (it may write only clusters).

As `application/problem+json`.

#### `404` error

Not Found. Codes: [`not_found`](/docs/api/errors#code-not-found) (the `X-DTG-Org` header is not a uuid, names an organization the caller is not an active member of, or, on an API key, names an organization other than the key's own).

As `application/problem+json`.

#### `413` error

Payload Too Large. Codes: [`body_too_large`](/docs/api/errors#code-body-too-large) (the body is over this endpoint's 64 KB limit; the body carries `limit_bytes`).

As `application/problem+json`.

#### `429` error

Only the per-minute rate limit: [`rate_limited`](/docs/api/errors#code-rate-limited) (120 requests a minute per credential, 300 per organization; lower ceilings for a sandbox key), with `Retry-After` and `X-RateLimit-*`.

As `application/problem+json`.

**`Retry-After` header**

Seconds to wait before retrying.

**`X-RateLimit-Limit` header**

The per-minute request ceiling that applies.

**`X-RateLimit-Remaining` header**

Requests left in the current window.

**`X-RateLimit-Reset` header**

Seconds until the window refills: on every answer, the time until the bucket is full again; on a refusal, until a request is allowed (as `Retry-After`).

Problem codes: [`workspace_invalid`](/docs/api/errors#code-workspace-invalid) (400), [`workspace_too_large`](/docs/api/errors#code-workspace-too-large) (400), [`malformed_json`](/docs/api/errors#code-malformed-json) (400), [`key_revoked`](/docs/api/errors#code-key-revoked) (401), [`unauthenticated`](/docs/api/errors#code-unauthenticated) (401), [`no_membership`](/docs/api/errors#code-no-membership) (403), [`account_suspended`](/docs/api/errors#code-account-suspended) (403), [`org_suspended`](/docs/api/errors#code-org-suspended) (403), [`org_closed`](/docs/api/errors#code-org-closed) (403), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (403), [`sandbox_read_only`](/docs/api/errors#code-sandbox-read-only) (403), [`not_found`](/docs/api/errors#code-not-found) (404), [`body_too_large`](/docs/api/errors#code-body-too-large) (413), [`rate_limited`](/docs/api/errors#code-rate-limited) (429). Every problem is a [problem document](/docs/api/errors).

## GET `/v2/account/notifications`

**The bell: this person's notifications in the active organization**

Credentials: API key or App session. Operation id `acctListNotifications`.

Either credential (an API key or a session); resolves the caller's active organization (`X-DTG-Org`). Any role. D54: one bell per person per organization, newest first, at most 200, with the unread badge. Each row carries its facts (`kind`, `payload`) and the words rendered from them now. Poll it; there is no push.

### Parameters

**`X-DTG-Org` header · string (uuid)**

The organization this request acts in (a uuid). Omitted: the caller's own organization, else their earliest active membership. An API key is bound to its organization and may only name that one.

### Responses

#### `200` success

The bell.

As `application/json`.

**`data` array of AcctNotification always present**

Undismissed notifications, newest first, at most 200 (90-day retention).

**`data[].id` string (uuid) always present**

The notification id.

**`data[].kind` string always present**

What happened. Known kinds: `vintage_release`, [`vintage_withdrawn`](/docs/api/errors#code-vintage-withdrawn), `county_window_opening`, `county_batch_applied`, `api_limit_hit`, `payment_failed`, `payment_recovered`, `plan_changed`, `plan_reset_at_switch`, `downgrade_applied`, `seat_granted`, `seat_removed`, `member_left`, `member_departed`, `join_request`, `join_decision`, `ownership_claim`, `trial_started`, `trial_expiring`, `announcement`, `new_device`, `password_changed`, [`org_suspended`](/docs/api/errors#code-org-suspended), [`account_suspended`](/docs/api/errors#code-account-suspended), `account_unsuspended`, `composite_shed`, `keys_revoked`, `support_reply`. A kind not in this list may appear; it still renders a generic title and body.

**`data[].group` string always present**

The bell's group for it: `Data`, `Counties`, `Keys`, `Billing`, `Members`, `Account` or `Announcements`.

**`data[].title` string always present**

One-line title.

**`data[].body` string always present**

The sentence.

**`data[].action` object**

The one action that resolves it, when there is one (absent otherwise).

**`data[].action.label` string always present**

Button label.

**`data[].action.to` string always present**

A route in DTG's web app (for example `/account/billing`). A client that does not use the app reads `payload` instead.

**`data[].emailed` boolean always present**

True when the same fact was also sent by email.

**`data[].created_at` string (date-time) always present**

When it was written.

**`data[].read_at` string (date-time) or null always present**

When it was marked read; null while unread.

**`data[].payload` AcctNotificationPayloadVintageRelease or AcctNotificationPayloadVintageWithdrawn or AcctNotificationPayloadCountyWindowOpening or AcctNotificationPayloadCountyBatchApplied or AcctNotificationPayloadApiLimitHit or AcctNotificationPayloadPaymentFailed or AcctNotificationPayloadPaymentRecovered or AcctNotificationPayloadPlanChanged or AcctNotificationPayloadDowngradeApplied or AcctNotificationPayloadSeatGranted or AcctNotificationPayloadSeatRemoved or AcctNotificationPayloadMemberLeft or AcctNotificationPayloadMemberDeparted or AcctNotificationPayloadJoinRequest or AcctNotificationPayloadJoinDecision or AcctNotificationPayloadOwnershipClaim or AcctNotificationPayloadTrialStarted or AcctNotificationPayloadTrialExpiring or AcctNotificationPayloadAnnouncement or AcctNotificationPayloadNewDevice or AcctNotificationPayloadPasswordChanged or AcctNotificationPayloadOrgSuspended or AcctNotificationPayloadAccountSuspended or AcctNotificationPayloadAccountUnsuspended or AcctNotificationPayloadCompositeShed or AcctNotificationPayloadKeysRevoked or AcctNotificationPayloadSupportReply always present**

The structured facts, by `kind` (one schema per kind below). Never a secret.

**`data[].payload.vintage` string**

The new current vintage id.

**`data[].payload.label` string**

Its display label.

**`data[].payload.superseded` object**

Present only when this release corrects a build the organization exported from.

**`data[].payload.superseded.vintage` string always present**

The corrected (withdrawn) vintage id.

**`data[].payload.superseded.label` string always present**

Its display label.

**`data[].payload.superseded.exports` integer always present**

How many exports (API, CSV, XLSX) the organization made from it.

**`data[].payload.current` string**

The vintage now current.

**`data[].payload.current_label` string**

Its display label.

**`data[].payload.withdrawn_at` string**

When the withdrawal was recorded (ISO 8601).

**`data[].payload.was_current` boolean**

True when the withdrawn build was the current vintage.

**`data[].payload.exported` object**

Present only for an organization that exported from the withdrawn build.

**`data[].payload.exported.exports` integer always present**

How many exports (API, CSV, XLSX) it made from it.

**`data[].payload.anchor_day` integer**

The organization's anchor day of the month.

**`data[].payload.effective_on` string (date)**

The next anchor date (YYYY-MM-DD), when changes made now take effect.

**`data[].payload.removed` integer**

Counties removed.

**`data[].payload.change_id` string (uuid)**

The scheduled change's id.

**`data[].payload.limit` "points" | "rate" | "csv" | "api"**

Which limit: `points` (the monthly points allowance) or `rate` (the per-minute ceiling). `csv` and `api` are reserved for re-armed monthly quotas.

**`data[].payload.api_key_id` string (uuid)**

The refused key's id; absent when the request was not made with a key.

**`data[].payload.key_name` string**

The key's name (T108: written whenever the refusal was a key's).

**`data[].payload.org` string**

The organization's name.

**`data[].payload.recovered_at` string (date-time)**

When the recovery was recorded.

**`data[].payload.changes` array of AcctPlanChangeItem**

What changed now, in the Billing screen's order. Empty when the request only scheduled a change.

**`data[].payload.changes[].item` "plan" | "seats" | "counties" | "api" always present**

**`data[].payload.changes[].before` always present**

The value before the change.

**`data[].payload.changes[].after` always present**

The value after it.

**`data[].payload.effective_at` string (date-time) or null**

When the changes in `changes` took effect; null when there are none.

**`data[].payload.scheduled` object or null**

What the request scheduled for the end of the billing period; null when nothing.

**`data[].payload.scheduled.changes` array of AcctPlanChangeItem always present**

The changes scheduled.

**`data[].payload.scheduled.changes[].item` "plan" | "seats" | "counties" | "api" always present**

**`data[].payload.scheduled.changes[].before` always present**

The value before the change.

**`data[].payload.scheduled.changes[].after` always present**

The value after it.

**`data[].payload.scheduled.effective_at` string (date-time) or null always present**

The end of the current billing period, when they take effect.

**`data[].payload.plan` string**

The new plan's display name.

**`data[].payload.detail` string**

A sentence: how many counties were read-locked and members became free members.

**`data[].payload.reason` string**

Why the composite ran (an internal label).

**`data[].payload.email` string**

The leaver's email address.

**`data[].payload.role` "owner" | "admin" | "member"**

The role they held.

**`data[].payload.request_id` string (uuid)**

The join request's id (approve or deny it under `/v2/account/join-requests`).

**`data[].payload.approved` boolean**

True when approved (the only value written today; a denial writes no notification).

**`data[].payload.claimant` string**

The claimant's email address.

**`data[].payload.completes_at` string (date)**

The date the claim completes unless an owner signs in first (YYYY-MM-DD).

**`data[].payload.county` string**

The trial county's name.

**`data[].payload.fips` string**

Its 5-digit FIPS code.

**`data[].payload.expires_at` string (date)**

The trial's last date (YYYY-MM-DD).

**`data[].payload.title` string**

The announcement's title.

**`data[].payload.body` string**

The announcement's text.

**`data[].payload.announcement_id` string (uuid)**

The announcement's id.

**`data[].payload.keys` array of AcctNotificationKeyListed**

The person's active API keys when the password changed, every organization (empty when none; absent on rows written before T110).

**`data[].payload.keys[].name` string always present**

The key's name.

**`data[].payload.keys[].key_prefix` string always present**

The key's first characters.

**`data[].payload.keys[].key_last4` string or null always present**

The key's last four characters; null for a key minted before T110.

**`data[].payload.keys[].mode` string always present**

`live` or `sandbox`.

**`data[].payload.keys[].organization` string**

The organization the key belongs to (password\_changed only).

**`data[].payload.keys[].created_at` string (date-time)**

When it was minted (password\_changed only).

**`data[].payload.keys[].last_used_at` string (date-time) or null**

When it last authenticated a request; null if never (password\_changed only).

**`data[].payload.keys[].holder` string or null**

The holder's address (keys\_revoked only).

**`data[].payload.shed` integer**

Counties released (read-locked).

**`data[].payload.count` integer**

How many keys.

**`data[].payload.ticket_id` string (uuid)**

The support request's id.

**`data[].payload.ticket_ref` string**

Its first eight characters, as the mails name it.

**`unread` integer always present**

The badge: unread, undismissed notifications in total (may exceed what `data` holds).

#### `401` error

Unauthorized. Codes: [`key_revoked`](/docs/api/errors#code-key-revoked) (the API key was revoked; `revoked_at`, `reason`, and a sentence saying when and why); [`unauthenticated`](/docs/api/errors#code-unauthenticated) (no API key or session; the key is unknown, or its account, membership or organization is no longer active; the session has expired, passed its 30-day maximum age, or belongs to an unverified email address).

As `application/problem+json`.

#### `403` error

Forbidden. Codes: [`no_membership`](/docs/api/errors#code-no-membership) (the account has no active membership in an active organization), [`account_suspended`](/docs/api/errors#code-account-suspended) (O56: the signed-in person's own account is suspended by DTG; `reason` where recorded), [`org_suspended`](/docs/api/errors#code-org-suspended) (the selected organization is suspended by DTG; the body carries `organization {id, name}` and `reason`), [`org_closed`](/docs/api/errors#code-org-closed) (the selected organization is closed; the body carries `organization {id, name}`), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (a key of the mode this process does not serve: a sandbox key on the live API, or a live key on the sandbox; the body carries `expected` and `base_url`, the address that serves it).

As `application/problem+json`.

#### `404` error

Not Found. Codes: [`not_found`](/docs/api/errors#code-not-found) (the `X-DTG-Org` header is not a uuid, names an organization the caller is not an active member of, or, on an API key, names an organization other than the key's own).

As `application/problem+json`.

#### `429` error

Only the per-minute rate limit: [`rate_limited`](/docs/api/errors#code-rate-limited) (120 requests a minute per credential, 300 per organization; lower ceilings for a sandbox key), with `Retry-After` and `X-RateLimit-*`.

As `application/problem+json`.

**`Retry-After` header**

Seconds to wait before retrying.

**`X-RateLimit-Limit` header**

The per-minute request ceiling that applies.

**`X-RateLimit-Remaining` header**

Requests left in the current window.

**`X-RateLimit-Reset` header**

Seconds until the window refills: on every answer, the time until the bucket is full again; on a refusal, until a request is allowed (as `Retry-After`).

Problem codes: [`key_revoked`](/docs/api/errors#code-key-revoked) (401), [`unauthenticated`](/docs/api/errors#code-unauthenticated) (401), [`no_membership`](/docs/api/errors#code-no-membership) (403), [`account_suspended`](/docs/api/errors#code-account-suspended) (403), [`org_suspended`](/docs/api/errors#code-org-suspended) (403), [`org_closed`](/docs/api/errors#code-org-closed) (403), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (403), [`not_found`](/docs/api/errors#code-not-found) (404), [`rate_limited`](/docs/api/errors#code-rate-limited) (429). Every problem is a [problem document](/docs/api/errors).

## POST `/v2/account/notifications/read-all`

**Mark every notification read**

Credentials: API key or App session. Operation id `acctReadAllNotifications`.

Either credential (an API key or a session); resolves the caller's active organization (`X-DTG-Org`). Any role. Takes no body. Marks every unread, undismissed notification of this person in the active organization read.

### Parameters

**`X-DTG-Org` header · string (uuid)**

The organization this request acts in (a uuid). Omitted: the caller's own organization, else their earliest active membership. An API key is bound to its organization and may only name that one.

### Responses

#### `200` success

How many were marked.

As `application/json`.

**`read` integer always present**

Notifications newly marked read.

#### `400` error

Bad Request. Codes: [`malformed_json`](/docs/api/errors#code-malformed-json) (the body is not valid JSON, or uses an unsupported content encoding).

As `application/problem+json`.

#### `401` error

Unauthorized. Codes: [`key_revoked`](/docs/api/errors#code-key-revoked) (the API key was revoked; `revoked_at`, `reason`, and a sentence saying when and why); [`unauthenticated`](/docs/api/errors#code-unauthenticated) (no API key or session; the key is unknown, or its account, membership or organization is no longer active; the session has expired, passed its 30-day maximum age, or belongs to an unverified email address).

As `application/problem+json`.

#### `403` error

Forbidden. Codes: [`no_membership`](/docs/api/errors#code-no-membership) (the account has no active membership in an active organization), [`account_suspended`](/docs/api/errors#code-account-suspended) (O56: the signed-in person's own account is suspended by DTG; `reason` where recorded), [`org_suspended`](/docs/api/errors#code-org-suspended) (the selected organization is suspended by DTG; the body carries `organization {id, name}` and `reason`), [`org_closed`](/docs/api/errors#code-org-closed) (the selected organization is closed; the body carries `organization {id, name}`), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (a key of the mode this process does not serve: a sandbox key on the live API, or a live key on the sandbox; the body carries `expected` and `base_url`, the address that serves it). A sandbox key: [`sandbox_read_only`](/docs/api/errors#code-sandbox-read-only) (it may write only clusters).

As `application/problem+json`.

#### `404` error

Not Found. Codes: [`not_found`](/docs/api/errors#code-not-found) (the `X-DTG-Org` header is not a uuid, names an organization the caller is not an active member of, or, on an API key, names an organization other than the key's own).

As `application/problem+json`.

#### `413` error

Payload Too Large. Codes: [`body_too_large`](/docs/api/errors#code-body-too-large) (the body is over this endpoint's 8 KB limit; the body carries `limit_bytes`).

As `application/problem+json`.

#### `429` error

Only the per-minute rate limit: [`rate_limited`](/docs/api/errors#code-rate-limited) (120 requests a minute per credential, 300 per organization; lower ceilings for a sandbox key), with `Retry-After` and `X-RateLimit-*`.

As `application/problem+json`.

**`Retry-After` header**

Seconds to wait before retrying.

**`X-RateLimit-Limit` header**

The per-minute request ceiling that applies.

**`X-RateLimit-Remaining` header**

Requests left in the current window.

**`X-RateLimit-Reset` header**

Seconds until the window refills: on every answer, the time until the bucket is full again; on a refusal, until a request is allowed (as `Retry-After`).

Problem codes: [`malformed_json`](/docs/api/errors#code-malformed-json) (400), [`key_revoked`](/docs/api/errors#code-key-revoked) (401), [`unauthenticated`](/docs/api/errors#code-unauthenticated) (401), [`no_membership`](/docs/api/errors#code-no-membership) (403), [`account_suspended`](/docs/api/errors#code-account-suspended) (403), [`org_suspended`](/docs/api/errors#code-org-suspended) (403), [`org_closed`](/docs/api/errors#code-org-closed) (403), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (403), [`sandbox_read_only`](/docs/api/errors#code-sandbox-read-only) (403), [`not_found`](/docs/api/errors#code-not-found) (404), [`body_too_large`](/docs/api/errors#code-body-too-large) (413), [`rate_limited`](/docs/api/errors#code-rate-limited) (429). Every problem is a [problem document](/docs/api/errors).

## POST `/v2/account/notifications/{id}/read`

**Mark one notification read**

Credentials: API key or App session. Operation id `acctReadNotification`.

Either credential (an API key or a session); resolves the caller's active organization (`X-DTG-Org`). Any role. Takes no body. Idempotent: an already-read notification keeps its first `read_at`. Another person's notification is indistinguishable from one that does not exist.

### Parameters

**`X-DTG-Org` header · string (uuid)**

The organization this request acts in (a uuid). Omitted: the caller's own organization, else their earliest active membership. An API key is bound to its organization and may only name that one.

**`id` path · string (uuid) required**

The notification id.

### Responses

#### `200` success

The notification's id and read time.

As `application/json`.

**`id` string (uuid) always present**

The notification id.

**`read_at` string (date-time) always present**

When it was (first) marked read.

#### `400` error

Bad Request. Codes: [`malformed_json`](/docs/api/errors#code-malformed-json) (the body is not valid JSON, or uses an unsupported content encoding).

As `application/problem+json`.

#### `401` error

Unauthorized. Codes: [`key_revoked`](/docs/api/errors#code-key-revoked) (the API key was revoked; `revoked_at`, `reason`, and a sentence saying when and why); [`unauthenticated`](/docs/api/errors#code-unauthenticated) (no API key or session; the key is unknown, or its account, membership or organization is no longer active; the session has expired, passed its 30-day maximum age, or belongs to an unverified email address).

As `application/problem+json`.

#### `403` error

Forbidden. Codes: [`no_membership`](/docs/api/errors#code-no-membership) (the account has no active membership in an active organization), [`account_suspended`](/docs/api/errors#code-account-suspended) (O56: the signed-in person's own account is suspended by DTG; `reason` where recorded), [`org_suspended`](/docs/api/errors#code-org-suspended) (the selected organization is suspended by DTG; the body carries `organization {id, name}` and `reason`), [`org_closed`](/docs/api/errors#code-org-closed) (the selected organization is closed; the body carries `organization {id, name}`), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (a key of the mode this process does not serve: a sandbox key on the live API, or a live key on the sandbox; the body carries `expected` and `base_url`, the address that serves it). A sandbox key: [`sandbox_read_only`](/docs/api/errors#code-sandbox-read-only) (it may write only clusters).

As `application/problem+json`.

#### `404` error

Not Found. Codes: [`not_found`](/docs/api/errors#code-not-found) (the `X-DTG-Org` header is not a uuid, names an organization the caller is not an active member of, or, on an API key, names an organization other than the key's own), [`notification_not_found`](/docs/api/errors#code-notification-not-found) (no such notification for this person in this organization; also a non-uuid id answers [`not_found`](/docs/api/errors#code-not-found) ).

As `application/problem+json`.

#### `413` error

Payload Too Large. Codes: [`body_too_large`](/docs/api/errors#code-body-too-large) (the body is over this endpoint's 8 KB limit; the body carries `limit_bytes`).

As `application/problem+json`.

#### `429` error

Only the per-minute rate limit: [`rate_limited`](/docs/api/errors#code-rate-limited) (120 requests a minute per credential, 300 per organization; lower ceilings for a sandbox key), with `Retry-After` and `X-RateLimit-*`.

As `application/problem+json`.

**`Retry-After` header**

Seconds to wait before retrying.

**`X-RateLimit-Limit` header**

The per-minute request ceiling that applies.

**`X-RateLimit-Remaining` header**

Requests left in the current window.

**`X-RateLimit-Reset` header**

Seconds until the window refills: on every answer, the time until the bucket is full again; on a refusal, until a request is allowed (as `Retry-After`).

Problem codes: [`malformed_json`](/docs/api/errors#code-malformed-json) (400), [`key_revoked`](/docs/api/errors#code-key-revoked) (401), [`unauthenticated`](/docs/api/errors#code-unauthenticated) (401), [`no_membership`](/docs/api/errors#code-no-membership) (403), [`account_suspended`](/docs/api/errors#code-account-suspended) (403), [`org_suspended`](/docs/api/errors#code-org-suspended) (403), [`org_closed`](/docs/api/errors#code-org-closed) (403), [`wrong_instance`](/docs/api/errors#code-wrong-instance) (403), [`sandbox_read_only`](/docs/api/errors#code-sandbox-read-only) (403), [`not_found`](/docs/api/errors#code-not-found) (404), [`notification_not_found`](/docs/api/errors#code-notification-not-found) (404), [`body_too_large`](/docs/api/errors#code-body-too-large) (413), [`rate_limited`](/docs/api/errors#code-rate-limited) (429). Every problem is a [problem document](/docs/api/errors).

## POST `/v2/events`

**Record screens opened in the app**

Credentials: App session. Operation id `postProductEvents`.

T166: the app's event client posts the screens a person opens, in batches. A signed-in session only: an API key is refused ( [`session_required`](/docs/api/errors#code-session-required) ). Each event is `{name: screen_opened, route}`, where `route` is the app's route family (`/explore/table`, `/clusters/:id`), never a full path; the time, organization, account and plan are the server's. Every other product event (tables, exports, API calls, the sign-up funnel, the assistant) Taigan records where it happens. While `PRODUCT_EVENTS` is off (`rules.product_events`), a batch is accepted and nothing is written (`recorded` 0). Writes no audit row.

### Parameters

**`X-DTG-Org` header · string (uuid)**

The organization this request acts in (a uuid). Omitted: the caller's own organization, else their earliest active membership.

### Request body

Required; `application/json`.

**`events` array of object required**

1 to 50 events.

**`events[].name` "screen\_opened" required**

The only event a client sends.

**`events[].route` string required**

The route family. (pattern ^/\[a-z0-9/:\_-\]{0,63}$)

### Responses

#### `202` success

Accepted.

As `application/json`.

**`recorded` integer**

Events written (0 while `PRODUCT_EVENTS` is off).

**`product_events` "on" | "off"**

The switch.

#### `400` error

Bad Request. Codes: [`events_invalid`](/docs/api/errors#code-events-invalid) (not 1 to 50 events of `name` `screen_opened` and a route family; `index` names the first bad entry), [`malformed_json`](/docs/api/errors#code-malformed-json).

As `application/problem+json`.

#### `401` error

Unauthorized. Codes: [`unauthenticated`](/docs/api/errors#code-unauthenticated) (no session, or the session is not live).

As `application/problem+json`.

#### `403` error

Forbidden. Codes: [`session_required`](/docs/api/errors#code-session-required) (an API key), [`no_membership`](/docs/api/errors#code-no-membership), [`account_suspended`](/docs/api/errors#code-account-suspended), [`org_suspended`](/docs/api/errors#code-org-suspended), [`org_closed`](/docs/api/errors#code-org-closed), [`terms_acceptance_required`](/docs/api/errors#code-terms-acceptance-required).

As `application/problem+json`.

#### `429` error

Too Many Requests. Only [`rate_limited`](/docs/api/errors#code-rate-limited) here, from this route's own ceiling (20 batches a minute per account, with `Retry-After`); a batch never spends the per-credential or per-organization ceilings of the other routes.

As `application/problem+json`.

**`Retry-After` header**

Seconds to wait before retrying.

**`X-RateLimit-Limit` header**

The per-minute request ceiling that applies.

**`X-RateLimit-Remaining` header**

Requests left in the current window.

**`X-RateLimit-Reset` header**

Seconds until the window refills: on every answer, the time until the bucket is full again; on a refusal, until a request is allowed (as `Retry-After`).

Problem codes: [`events_invalid`](/docs/api/errors#code-events-invalid) (400), [`malformed_json`](/docs/api/errors#code-malformed-json) (400), [`unauthenticated`](/docs/api/errors#code-unauthenticated) (401), [`session_required`](/docs/api/errors#code-session-required) (403), [`no_membership`](/docs/api/errors#code-no-membership) (403), [`account_suspended`](/docs/api/errors#code-account-suspended) (403), [`org_suspended`](/docs/api/errors#code-org-suspended) (403), [`org_closed`](/docs/api/errors#code-org-closed) (403), [`terms_acceptance_required`](/docs/api/errors#code-terms-acceptance-required) (403), [`rate_limited`](/docs/api/errors#code-rate-limited) (429). Every problem is a [problem document](/docs/api/errors).

Generated from `taigan/openapi.yaml` at catahoula `77925fab712f`(API document version 2.0).
