Hosted account and dashboard API
The session-based API used by the dashboard and CherrySub CLI.
Download this chapterOn this page
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.
For normal automation, use CherrySub 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:
{ "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:
{
"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 · Limits.
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 |
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:
{
"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.
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 |
| POST | /auth/login | 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 |
| POST | /auth/password-reset/request | passwordResetRequest |
| POST | /auth/password-reset/confirm | resetPasswordRequest |
| POST | /auth/password/change | 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 |
| 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 |
| GET | /dashboard/projects | See operation notes / no named JSON body |
| POST | /dashboard/projects | projectRequest |
| PATCH | /dashboard/projects/{project_id} | 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 |
| 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 |
| 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 |
| PATCH | /dashboard/customers/{customer_id}/notes/{note_id} | 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 |
| GET | /dashboard/apps/{app_id}/product-access-mappings | See operation notes / no named JSON body |
| POST | /dashboard/apps/{app_id}/product-access-mappings | 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 |
| 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 |
| GET | /dashboard/apps/{app_id}/store-connections | See operation notes / no named JSON body |
| PUT | /dashboard/apps/{app_id}/store-connections/{platform} | appleConnectionRequest, 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 |
| 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 |
| PATCH | /dashboard/apps/{app_id}/product-sets/{product_set_id} | 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 |
| 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 |
| PATCH | /dashboard/apps/{app_id}/webhooks/{endpoint_id} | 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 |
| PATCH | /dashboard/saved-views/{view_id} | 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 |
| PATCH | /dashboard/saved-charts/{chart_id} | 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 |
| DELETE | /dashboard/apps/{app_id}/catalog-connection/apple | See operation notes / no named JSON body |
| POST | /dashboard/apps/{app_id}/catalog-import/preview | CatalogImportInput |
| POST | /dashboard/apps/{app_id}/catalog-import/apply | 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 |