# Hosted account and dashboard API

The hosted API serves the dashboard and CherrySub CLI. It uses the opaque `JSESSIONID` cookie, verified account identity, selected project, app ownership, and role checks. It is separate from the publishable/secret-key runtime API and from root `/admin/v1` administration.

This chapter covers the reviewed request shapes and the complete route inventory. Its JSON field names are preserved exactly: hosted responses commonly use camelCase, many write bodies use snake_case, and saved-chart bodies use camelCase. Do not apply a global casing conversion.

## Authentication and session context

`POST /auth/login` accepts `email` and `password`, sets `JSESSIONID`, and returns email-verification state. `POST /auth/register` also takes `name`, `surname`, and an optional safe local `callbackUrl`. Registering/signing in is not equivalent to verified dashboard access.

Verification confirmation and password reset use their bounded-lifetime account tokens. The verification-request flow can include a local `callbackUrl`. Logout revokes the current session; logout-all revokes the account's sessions. Account/session management follows the [account guide](/docs/accounts-and-projects).

For normal automation, use [CherrySub CLI](/docs/cli) so it manages private session storage and project selection. If you integrate the HTTP interface directly, keep cookies private, refuse credential-bearing redirects, and serialize selected-project changes for a shared session.

## Authorization rules

Dashboard operations require a verified session and membership in the selected project. Resource IDs are checked against project/app ownership, not trusted merely because a caller supplies a UUID.

App/catalog/key/store/webhook configuration and support-note writes require owner/admin authority. Team changes have additional owner/admin target restrictions. Invitation-management reads also require manager authority. Saved views/charts and exports bind to the requesting account and project membership rather than being shared configuration writes.

`POST /dashboard/projects/{project_id}/members` intentionally rejects direct member addition. Use invitation creation and acceptance instead. A viewer cannot bypass a disabled dashboard button by calling the write route directly.

## Project and app setup example

The project-creation body is:

```json
{ "name": "My project" }
```

After creation or `GET /dashboard/projects`, select the intended project with `POST /dashboard/projects/{project_id}/select`. It changes session context and returns no content.

To create an app in that hosted project, use `POST /dashboard/apps`:

```json
{
  "name": "My app",
  "apple_bundle_id": "com.example.app",
  "apple_app_apple_id": 1234567890,
  "google_package_name": "com.example.app"
}
```

Use only the identities your app supports. The hosted response contains `app`, `secretKey`, and `publishableKey`; the keys are one-time plaintext. This differs from the root-admin app response. Store and manage those values through the correct server/client boundaries.

## Managed store credential bodies

`PUT /dashboard/apps/{app_id}/store-connections/apple` accepts `issuer_id`, `key_id`, and `private_key_pem`. Google accepts `service_account_json` as an embedded JSON object, not a string containing escaped JSON. The platform must be `apple` or `google`.

The list/PUT response exposes safe metadata: platform, source, account hint, and timestamps. DELETE disconnects the app's server credential use. `managed` and `environment` are credential sources; neither is proof that a live purchase passed.

The hosted JSON body limit is 32 KiB, except the two catalog-import endpoints, which allow a 1 MiB JSON envelope containing a source file up to 512 KiB. The Google credential validator's larger 64 KiB allowance is therefore not the effective maximum HTTP upload size; escaping/envelope overhead also counts. [Store setup](/docs/google-play) · [Limits](/docs/limits-and-release-status).

## Dataset and chart queries

Customer, transaction, and event list requests support their resource-specific search/filter/sort fields through the hosted data-input parser. Common fields include `query`, `platform`, `environment`, `status`, `type`, `page`, and `page_size`; time/product/store filters depend on the resource. Default page size is 50, maximum 100. Unsupported filters or invalid pagination are rejected rather than applied to an unrelated dataset.

`GET /dashboard/charts` requires an `app_id` and a valid metric/range configuration:

| Query field | Purpose |
| --- | --- |
| `app_id` | App UUID belonging to the selected project |
| `metric` | One metric ID from the [metric dictionary](/docs/metrics) |
| `days` | Supported relative range when custom dates are absent |
| `from`, `to` | Inclusive custom UTC dates |
| `resolution` | day, week, or month within range limits |
| `offset_days` | Supported comparison offset |
| `platform`, `environment`, `product_id` | Applicable data filters |
| `retention_period` | Weekly/monthly/yearly duration selection for retention |
| `conversion_timeframe` | Customer-conversion observation window |
| `customer_lifetime` | Realized LTV window |
| `cancellation_window` | Trial opt-out window |
| `refund_request_reason` | Refund review reason filter |
| `cohort_definition`, `cohort_measure` | Cohort matrix anchor/measure |
| `currency` | Monetary cohort selection where applicable |

Do not send unrelated metric-specific settings as if every combination were supported. Charts are bounded interactive aggregates; they are not a full export API. Product detail requests may need `app_id` to disambiguate a product reused by multiple apps.

## Export and saved-analysis examples

Create an export with `POST /dashboard/exports`:

```json
{
  "app_id": "11111111-1111-4111-8111-111111111111",
  "resource": "transactions",
  "from_date": "2026-08-01",
  "to_date": "2026-08-31",
  "platform": "apple",
  "environment": "sandbox",
  "product_id": "pro.monthly"
}
```

The service returns 202 and a job record. List your jobs with `GET /dashboard/exports?app_id=...`. The Location header identifies the job; the current router does not expose a general GET-by-job-ID endpoint there. Use the list/status and emailed download flow.

Saved views take `name`, `resource` (customers, transactions, or events), and a JSON `filters` object. Saved charts take the complete camelCase chart configuration shown in their body contract below. Both are requester-scoped; the API does not make another person's saved analysis writable merely because you know its ID.

## Errors and HTTP behavior

Hosted errors use `message`; account-link failures may also have `code`, such as `invalid_or_expired_token`, `email_verification_required`, or `invitation_email_mismatch`. Common statuses are 400 invalid input, 401 invalid session/credentials, 403 verification/role failure, 404 scoped resource missing, 409 lifecycle conflict, 429 throttling, and 503 delivery/dependency unavailable.

The generic invalid-input message can say “Invalid account details” even for a dashboard write. Interpret it with the operation's body contract, not as proof that your account email is wrong.

Send `Content-Type: application/json` for JSON bodies. Strict decoding rejects unsupported fields/trailing JSON. The cookie API uses its own auth/account throttles; runtime receipt-key throttling is a different boundary. The control plane has a process-local `/healthz`; the public staging proxy's health path can route to the core instead.

## Catalog import and store product reads

Verified owner/admin accounts can call the app-scoped catalog routes. `POST /dashboard/apps/{app_id}/catalog-import/preview` accepts `format` (`csv` or `json`), string `content`, and optional `activate_current`. It returns a read-only plan with normalized catalog, counts, changes, issues, notes, `canApply`, and `digest`. Apply sends the same fields to `/catalog-import/apply` plus `expected_digest` from the preview. A changed source, scope, or target returns HTTP 409; refresh the preview before retrying. Application is atomic and repeated successful requests return their saved result. `GET /dashboard/apps/{app_id}/catalog-imports` returns recent applied summaries.

`POST /dashboard/apps/{app_id}/store-products/{platform}` accepts an optional opaque `cursor` and returns `products`, optional `nextCursor`, and `notes`. Read every page before applying a selected catalog. Platforms are `apple` and `google`. These requests do not create products in the store or write the destination catalog.

The separate Apple catalog connection lives at `/dashboard/apps/{app_id}/catalog-connection/apple`: GET returns safe metadata, PUT takes `issuer_id`, `key_id`, and `private_key_pem`, and DELETE removes it. The .p8 is an App Store Connect API key, not the IAP key. Both store listing and credential routes require manager authority and destination project/app ownership. Secret material is encrypted and never returned. Google catalog listing uses its existing managed service account. [Migration flow](/docs/migration).

## Complete route index

This index is extracted from the hosted router at the reviewed backend revision. Body contracts are listed in the following section. Read the authorization notes above: not every write is a manager operation, and not every read exposes manager-only metadata to a viewer.

| Method | Route | Body contract |
| --- | --- | --- |
| GET | `/healthz` | See operation notes / no named JSON body |
| POST | `/auth/register` | [registerRequest](#registerrequest) |
| POST | `/auth/login` | [loginRequest](#loginrequest) |
| POST | `/auth/logout` | See operation notes / no named JSON body |
| POST | `/auth/logout-all` | See operation notes / no named JSON body |
| GET | `/auth/account` | See operation notes / no named JSON body |
| POST | `/auth/verify-email/request` | See operation notes / no named JSON body |
| POST | `/auth/verify-email/confirm` | [accountTokenRequest](#accounttokenrequest) |
| POST | `/auth/password-reset/request` | [passwordResetRequest](#passwordresetrequest) |
| POST | `/auth/password-reset/confirm` | [resetPasswordRequest](#resetpasswordrequest) |
| POST | `/auth/password/change` | [changePasswordRequest](#changepasswordrequest) |
| GET | `/auth/sessions` | See operation notes / no named JSON body |
| DELETE | `/auth/sessions/{session_id}` | See operation notes / no named JSON body |
| GET | `/auth/project-invitations/preview` | See operation notes / no named JSON body |
| POST | `/auth/project-invitations/accept` | [accountTokenRequest](#accounttokenrequest) |
| GET | `/dashboard` | See operation notes / no named JSON body |
| GET | `/dashboard/charts` | See operation notes / no named JSON body |
| GET | `/dashboard/exports` | See operation notes / no named JSON body |
| POST | `/dashboard/exports` | [exportJobRequest](#exportjobrequest) |
| GET | `/dashboard/projects` | See operation notes / no named JSON body |
| POST | `/dashboard/projects` | [projectRequest](#projectrequest) |
| PATCH | `/dashboard/projects/{project_id}` | [projectRequest](#projectrequest) |
| POST | `/dashboard/projects/{project_id}/select` | See operation notes / no named JSON body |
| GET | `/dashboard/projects/{project_id}/members` | See operation notes / no named JSON body |
| POST | `/dashboard/projects/{project_id}/members` | See operation notes / no named JSON body |
| PATCH | `/dashboard/projects/{project_id}/members/{user_id}` | [projectMemberRequest](#projectmemberrequest) |
| DELETE | `/dashboard/projects/{project_id}/members/{user_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/projects/{project_id}/invitations` | See operation notes / no named JSON body |
| POST | `/dashboard/projects/{project_id}/invitations` | [projectMemberRequest](#projectmemberrequest) |
| DELETE | `/dashboard/projects/{project_id}/invitations/{invitation_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/projects/{project_id}/activity` | See operation notes / no named JSON body |
| GET | `/dashboard/customers` | See operation notes / no named JSON body |
| GET | `/dashboard/customers/{customer_id}` | See operation notes / no named JSON body |
| POST | `/dashboard/customers/{customer_id}/notes` | [customerSupportNoteRequest](#customersupportnoterequest) |
| PATCH | `/dashboard/customers/{customer_id}/notes/{note_id}` | [customerSupportNoteRequest](#customersupportnoterequest) |
| DELETE | `/dashboard/customers/{customer_id}/notes/{note_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/events` | See operation notes / no named JSON body |
| GET | `/dashboard/transactions` | See operation notes / no named JSON body |
| GET | `/dashboard/transactions/{event_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/products/{platform}/{product_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/access-levels/{access_level_id}` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/access-levels` | [accessLevelRequest](#accesslevelrequest) |
| GET | `/dashboard/apps/{app_id}/product-access-mappings` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/product-access-mappings` | [productAccessMappingRequest](#productaccessmappingrequest) |
| DELETE | `/dashboard/apps/{app_id}/product-access-mappings/{mapping_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/integrations/{kind}` | See operation notes / no named JSON body |
| GET | `/dashboard/apps` | See operation notes / no named JSON body |
| POST | `/dashboard/apps` | [createAppRequest](#createapprequest) |
| GET | `/dashboard/apps/artwork` | See operation notes / no named JSON body |
| GET | `/dashboard/apps/{app_id}` | See operation notes / no named JSON body |
| PATCH | `/dashboard/apps/{app_id}` | [createAppRequest](#createapprequest) |
| GET | `/dashboard/apps/{app_id}/store-connections` | See operation notes / no named JSON body |
| PUT | `/dashboard/apps/{app_id}/store-connections/{platform}` | [appleConnectionRequest](#appleconnectionrequest), [googleConnectionRequest](#googleconnectionrequest) |
| DELETE | `/dashboard/apps/{app_id}/store-connections/{platform}` | See operation notes / no named JSON body |
| GET | `/dashboard/apps/{app_id}/api-keys` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/api-keys` | [apiKeyRequest](#apikeyrequest) |
| DELETE | `/dashboard/apps/{app_id}/api-keys/{key_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/apps/{app_id}/product-sets` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/product-sets` | [productSetRequest](#productsetrequest) |
| PATCH | `/dashboard/apps/{app_id}/product-sets/{product_set_id}` | [updateProductSetRequest](#updateproductsetrequest) |
| DELETE | `/dashboard/apps/{app_id}/product-sets/{product_set_id}` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/product-sets/{product_set_id}/packages` | [productSetPackageRequest](#productsetpackagerequest) |
| DELETE | `/dashboard/apps/{app_id}/product-sets/{product_set_id}/packages/{package_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/apps/{app_id}/webhooks` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/webhooks` | [webhookEndpointRequest](#webhookendpointrequest) |
| PATCH | `/dashboard/apps/{app_id}/webhooks/{endpoint_id}` | [updateWebhookEndpointRequest](#updatewebhookendpointrequest) |
| DELETE | `/dashboard/apps/{app_id}/webhooks/{endpoint_id}` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/webhooks/{endpoint_id}/rotate-secret` | See operation notes / no named JSON body |
| GET | `/dashboard/apps/{app_id}/webhook-deliveries` | See operation notes / no named JSON body |
| GET | `/dashboard/apps/{app_id}/webhook-deliveries/{message_id}` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/webhook-deliveries/{message_id}/retry` | See operation notes / no named JSON body |
| GET | `/dashboard/saved-views` | See operation notes / no named JSON body |
| POST | `/dashboard/saved-views` | [createSavedViewRequest](#createsavedviewrequest) |
| PATCH | `/dashboard/saved-views/{view_id}` | [createSavedViewRequest](#createsavedviewrequest) |
| DELETE | `/dashboard/saved-views/{view_id}` | See operation notes / no named JSON body |
| GET | `/dashboard/saved-views/{view_id}/results` | See operation notes / no named JSON body |
| GET | `/dashboard/saved-charts` | See operation notes / no named JSON body |
| POST | `/dashboard/saved-charts` | [savedChartRequest](#savedchartrequest) |
| PATCH | `/dashboard/saved-charts/{chart_id}` | [savedChartRequest](#savedchartrequest) |
| DELETE | `/dashboard/saved-charts/{chart_id}` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/store-products/{platform}` | See operation notes / no named JSON body |
| GET | `/dashboard/apps/{app_id}/catalog-connection/apple` | See operation notes / no named JSON body |
| PUT | `/dashboard/apps/{app_id}/catalog-connection/apple` | [appleConnectionRequest](#appleconnectionrequest) |
| DELETE | `/dashboard/apps/{app_id}/catalog-connection/apple` | See operation notes / no named JSON body |
| POST | `/dashboard/apps/{app_id}/catalog-import/preview` | [CatalogImportInput](#catalogimportinput) |
| POST | `/dashboard/apps/{app_id}/catalog-import/apply` | [CatalogImportInput](#catalogimportinput) |
| GET | `/dashboard/apps/{app_id}/catalog-imports` | See operation notes / no named JSON body |

## Hosted request body fields

These field names and transport types are extracted from the handler DTOs. Required/allowed combinations and lifecycle permissions are validated by the service; a zero-value Go field is not a declaration that the field is optional. This table deliberately does not invent a formal OpenAPI contract for the hosted interface. `JSON value` means an embedded object/value, not a stringified JSON blob.

### registerRequest

| JSON field | Transport type |
| --- | --- |
| `callbackUrl` | string |
| `name` | string |
| `surname` | string |
| `email` | string |
| `password` | string |

### loginRequest

| JSON field | Transport type |
| --- | --- |
| `email` | string |
| `password` | string |

### accountTokenRequest

| JSON field | Transport type |
| --- | --- |
| `token` | string |

### passwordResetRequest

| JSON field | Transport type |
| --- | --- |
| `email` | string |

### resetPasswordRequest

| JSON field | Transport type |
| --- | --- |
| `token` | string |
| `newPassword` | string |

### changePasswordRequest

| JSON field | Transport type |
| --- | --- |
| `currentPassword` | string |
| `newPassword` | string |

### exportJobRequest

| JSON field | Transport type |
| --- | --- |
| `app_id` | UUID string |
| `resource` | string |
| `from_date` | string |
| `to_date` | string |
| `platform` | string |
| `environment` | string |
| `product_id` | string |

### customerSupportNoteRequest

| JSON field | Transport type |
| --- | --- |
| `body` | string |

### accessLevelRequest

| JSON field | Transport type |
| --- | --- |
| `lookup_key` | string |
| `display_name` | string |

### productAccessMappingRequest

| JSON field | Transport type |
| --- | --- |
| `platform` | string |
| `product_id` | string |
| `access_level_id` | UUID string |
| `grant_mode` | string |

### projectRequest

| JSON field | Transport type |
| --- | --- |
| `name` | string |

### projectMemberRequest

| JSON field | Transport type |
| --- | --- |
| `email` | string |
| `role` | string |

### createAppRequest

| JSON field | Transport type |
| --- | --- |
| `name` | string |
| `apple_bundle_id` | string; pointer field supports omission |
| `apple_app_apple_id` | integer (int64); pointer field supports omission |
| `google_package_name` | string; pointer field supports omission |

### appleConnectionRequest

| JSON field | Transport type |
| --- | --- |
| `issuer_id` | string |
| `key_id` | string |
| `private_key_pem` | string |

### googleConnectionRequest

| JSON field | Transport type |
| --- | --- |
| `service_account_json` | JSON value |

### apiKeyRequest

| JSON field | Transport type |
| --- | --- |
| `kind` | string |
| `label` | string |

### productSetRequest

| JSON field | Transport type |
| --- | --- |
| `identifier` | string |
| `display_name` | string |
| `is_current` | boolean |

### updateProductSetRequest

| JSON field | Transport type |
| --- | --- |
| `is_current` | boolean |

### productSetPackageRequest

| JSON field | Transport type |
| --- | --- |
| `identifier` | string |
| `package_type` | string |
| `platform` | string |
| `product_id` | string |

### webhookEndpointRequest

| JSON field | Transport type |
| --- | --- |
| `url` | string |
| `description` | string |
| `event_types` | array of strings |

### updateWebhookEndpointRequest

| JSON field | Transport type |
| --- | --- |
| `url` | string; pointer field supports omission |
| `description` | string; pointer field supports omission |
| `event_types` | array of strings; pointer field supports omission |
| `enabled` | boolean; pointer field supports omission |

### createSavedViewRequest

| JSON field | Transport type |
| --- | --- |
| `name` | string |
| `resource` | string |
| `filters` | JSON value |

### savedChartRequest

| JSON field | Transport type |
| --- | --- |
| `appId` | UUID string |
| `name` | string |
| `metric` | string |
| `days` | integer |
| `fromDate` | string |
| `toDate` | string |
| `resolution` | string |
| `compareMode` | string |
| `platform` | string |
| `environment` | string |
| `productId` | string |
| `chartType` | string |
| `segment` | string |
| `currency` | string |
| `retentionPeriod` | string |
| `retentionMode` | string |
| `conversionTimeframe` | string |
| `customerLifetime` | string |
| `cancellationWindow` | string |
| `refundRequestReason` | string |
| `cohortDefinition` | string |
| `cohortMeasure` | string |

### CatalogImportInput

| JSON field | Transport type |
| --- | --- |
| `format` | string |
| `content` | string |
| `activate_current` | bool |
| `expected_digest` | string |

