---
title: SashiDo Management API
group: Automate and AI
nav: Management API
updated: 2026-11-14
description: Reference for the SashiDo Management API. Authenticate with a personal access token, then manage apps, builds, deployments, engines, scaling and environment variables, read the audit log, and query your app's data through the Parse proxy. Parameters, example requests and responses, status codes and errors.
keywords: sashido api, sashido management api, sashido api reference, parse server rest api, parse server deploy api, parse server ci cd, sashido access token api, parse server proxy
---

The Management API is the API the SashiDo dashboard and the MCP server use. With a [personal access token](/en/docs/developers/authenticate) you can call it from your scripts, CI pipelines and tools.

## Quick start

**1. Create a token** in the dashboard: **Account › Access Tokens › Create token**. Copy it. It is shown once.

```bash
export SASHIDO_TOKEN=sashido_pat_...
```

**2. Check it.** `GET /tokens/current` says what the token may do:

```bash
curl https://api.sashido.io/tokens/current \
  -H "Authorization: Bearer $SASHIDO_TOKEN"
```

**3. Find your app's `objectId`.** Every app endpoint takes it in the path. `GET /users/info` lists your apps:

```bash
curl https://api.sashido.io/users/info \
  -H "Authorization: Bearer $SASHIDO_TOKEN"
```

```json
{
  "data": {
    "user": { "objectId": "Us3rIdAbCd", "username": "you@example.com", "createdAt": "2021-03-04T10:00:00.000Z" },
    "products": {
      "apps": [
        { "objectId": "AbC123xYz0", "name": "Shop", "region": "1-eu", "parseServerVersion": "release-6.5.11-ubi8-node18", "…": "…" }
      ]
    }
  }
}
```

**4. Call an app endpoint**, e.g. its recent deployments:

```bash
export APP=AbC123xYz0
curl "https://api.sashido.io/apps/$APP/deployments?limit=3" \
  -H "Authorization: Bearer $SASHIDO_TOKEN"
```

## Basics

| | |
|---|---|
| Base URL | `https://api.sashido.io` (no version prefix) |
| Authentication | `Authorization: Bearer sashido_pat_...` on every request |
| Request bodies | JSON with `Content-Type: application/json`, up to 300 KB |
| Rate limit | 600 requests per minute per token; above it `429` with `Retry-After` |

**Responses.** Endpoints that return data answer `200` with the result in `data`: `{ "data": … }`. Endpoints that only act (deploy, scale, rename…) answer `204 No Content` with an empty body. The exceptions are noted at each endpoint.

**Where the IDs come from:**

| Parameter | Where to get it |
|---|---|
| `{appObjectId}` | `objectId` of the app in [`GET /users/info`](#get-usersinfo). Not the app's Application ID. |
| `{buildId}` | `objectId` of the build in [`GET /apps/{appObjectId}/builds`](#get-appsappobjectidbuilds) |
| a build's or deployment's `name` | `name` in the builds or [deployments](#get-appsappobjectiddeployments) list |
| `imageTag` | [`GET /products/parse-server-versions`](#get-productsparse-server-versions) |
| an engine's `name` and `billingPolicy` | [`GET /products/parse-server-engines`](#get-productsparse-server-engines) |
| `region` | the app's `region`: `1-eu` (Europe) or `2-us` (North America) |

**Regions.** Build, deploy, engine and scale endpoints take an optional `?region=`. Without it they act on all the app's regions.

### Tracing your calls

Two optional headers are saved with each change in your [Activity](#get-activities), to tell your scripts apart later. They are labels only and change nothing about access.

| Header | Max length | Example |
|---|---|---|
| `X-SashiDo-Client` | 64 | `github-actions` (default for tokens: `api`) |
| `X-Request-Id` | 64 | an ID from your own logs |

## Errors

Check the status code first; the body depends on where the error comes from.

**Access and SashiDo errors** are JSON with a numeric `code`:

```json
{ "code": 302, "message": "The access token isn't allowed to access this app." }
```

| Status | `code` | When | What to do |
|---|---|---|---|
| `401` | 300 | The token is invalid, expired or revoked, or the header isn't `Bearer` | Create a new token |
| `403` | 301 | The token lacks a scope; the message names it | Use a token with that scope |
| `403` | 302 | The token is limited to other apps | Use a token for this app |
| `403` | 303 | The endpoint isn't available with a token | Use the dashboard |
| `429` | 304 | More than 600 requests in a minute | Wait `Retry-After` seconds |
| `404` | 101 | No app with this `objectId` that you can access | Check the `objectId` |
| `400` | 200 | e.g. `"Build not found."` | Check the ID |
| `400`/`403`/`409`/`502` | 305 | The [Parse proxy](#your-apps-data-the-parse-proxy) refused or couldn't complete the request | See the message |
| `400` | 310 | An [App Settings](#app-settings) or [custom domain](#custom-domain) request is invalid: an unknown setting, a wrong type, a setting the app's Parse Server version lacks | Fix what the message names |
| `400` | 311 | The platform refused the change, e.g. an email sender on a domain you don't own | See the message |
| `403` | 312 | You can see the app but not change its settings | Ask the owner |

`401` and `403` access errors also carry the standard `WWW-Authenticate: Bearer error="invalid_token"` (or `"insufficient_scope"`) header.

**Invalid parameters** (a missing or mistyped field) answer `400` with the fields at fault:

```json
{ "body.name": { "message": "'name' is required" } }
```

**Refused changes** answer `400` with the reason as **plain text** (`Content-Type: text/html`), e.g.:

```text
only app owner can change engines
```

```text
Parse Server version release-9.9.9 isn't available.
```

**Errors from your app's Parse Server**, through the proxy, are Parse's own: `{ "code": 101, "error": "Object not found." }`.

A client that handles all of them:

```javascript
async function sashido(path, options = {}) {
  const res = await fetch(`https://api.sashido.io${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${process.env.SASHIDO_TOKEN}`,
      'Content-Type': 'application/json',
      'X-SashiDo-Client': 'my-script',
      ...options.headers,
    },
  });
  if (res.status === 204) return null;
  const text = await res.text();
  let body;
  try { body = JSON.parse(text); } catch { body = text; }
  if (res.status === 429) {
    const wait = Number(res.headers.get('Retry-After') || 60);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return sashido(path, options);
  }
  if (!res.ok) {
    const message = typeof body === 'string' ? body : body.message || body.error || JSON.stringify(body);
    throw new Error(`${res.status}: ${message}`);
  }
  return body;
}

const { data } = await sashido('/users/info');
```

## Account

### `GET /tokens/current`

The token you call with. Works with any scope.

#### Example response

```json
{
  "data": {
    "id": "TkN1dAbCd0",
    "name": "GitHub Actions deploy",
    "scopes": ["apps:read", "deploy"],
    "appIds": ["AbC123xYz0"],
    "expiresAt": "2027-02-11T09:30:00.000Z",
    "user": { "objectId": "Us3rIdAbCd", "username": "you@example.com" }
  }
}
```

`appIds` is `null` for a token for all apps; `expiresAt` is `null` for one that doesn't expire.

### `GET /users/info`

Scope: `account:read`. You and the apps the token can access, with their runtime. No keys, no billing data.

#### Example response

```json
{
  "data": {
    "user": { "objectId": "Us3rIdAbCd", "username": "you@example.com", "createdAt": "2021-03-04T10:00:00.000Z" },
    "products": {
      "apps": [
        {
          "objectId": "AbC123xYz0",
          "name": "Shop",
          "appId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
          "serviceName": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
          "slug": "shop",
          "region": "1-eu",
          "status": "active",
          "server_status": { "state": "finished" },
          "parseServerVersion": "release-6.5.11-ubi8-node18",
          "settings": {
            "engine": { "1-eu": { "name": "Production 1X", "billingPolicy": "hourly" } },
            "engine_scaling_config": { "min": 1, "max": 20, "buffer": 4 },
            "highly_available": false
          },
          "deployment": {
            "objectId": "DpL0yAbCd0",
            "name": "parse-server-deployment-109",
            "number": 109,
            "phase": "Active",
            "region": "1-eu",
            "duration": 74,
            "replicas": { "auto": false, "min": 1, "max": 1 },
            "createdAt": "2026-11-12T14:02:11.000Z",
            "updatedAt": "2026-11-12T14:03:25.000Z"
          },
          "createdAt": "2021-03-04T10:05:00.000Z",
          "updatedAt": "2026-11-12T14:03:25.000Z"
        }
      ]
    }
  }
}
```

- `appId` is the app's Application ID, used by the Parse SDKs. The API's paths use `objectId`.
- `deployment` is the app's latest deployment; `replicas` are its instances.
- `settings.engine_scaling_config` holds the limits for [scaling](#put-appsappobjectidscale).

### `GET /activities`

Scope: `account:read`. Your account's Activity (audit log), newest first: every change, who made it and how it went. Collaborators see only their own events.

| Query | Type | Default | |
|---|---|---|---|
| `page` | number | 1 | Starts at 1 |
| `pageSize` | number | 50 | Events per page |

`GET /activities/{type}` returns only one type, e.g. `/activities/deploy`.

#### Example request

```bash
curl "https://api.sashido.io/activities?page=1&pageSize=20" \
  -H "Authorization: Bearer $SASHIDO_TOKEN"
```

#### Example response

```json
{
  "data": [
    {
      "objectId": "AcT1vAbCd0",
      "type": "set_env",
      "createdAt": "2026-11-13T08:41:07.000Z",
      "ip": "203.0.113.24",
      "browser": "curl/8.4.0",
      "actor": { "kind": "access_token", "tokenId": "TkN1dAbCd0", "tokenName": "GitHub Actions deploy" },
      "via": { "client": "github-actions", "requestId": "4f0c9a52-6f1e-4a43-9a3b-2a7c1d9e8b10" },
      "result": { "status": "ok" },
      "source": "server",
      "app": { "objectId": "AbC123xYz0", "name": "Shop" },
      "data": {
        "message": "Environment variables changed",
        "params": { "env": [{ "name": "STRIPE_KEY", "value": "(not recorded)" }] }
      }
    }
  ]
}
```

- `actor.kind`: `session` (you in the dashboard) or `access_token`.
- `via`: the client, and through MCP also `mcpClient` (e.g. Claude Code) and `mcpTool`.
- `result.status`: `ok`, `refused` or `failed`, with `code` and `message` when it didn't succeed.
- `data.params` holds what the change was called with. Values under names that look secret (`value`, `password`, `token`, `key`…) are never stored; they show as `"(not recorded)"`.

## Apps

### `POST /apps`

Scope: `apps:write`, with a token for all apps. Creates an app. **Billable.**

| Body | Type | Required | |
|---|---|---|---|
| `name` | string | yes | |
| `region` | string | yes | `1-eu` (Europe) or `2-us` (North America) |
| `parseServerVersion` | string | no | An `imageTag` from [the versions](#get-productsparse-server-versions). Set it: the default may be an older version. |
| `engine` | object | no | `{ "name": "Production 1X", "billingPolicy": "hourly" }` from [the engines](#get-productsparse-server-engines) |

#### Example request

```bash
curl -X POST https://api.sashido.io/apps \
  -H "Authorization: Bearer $SASHIDO_TOKEN" -H "Content-Type: application/json" \
  -d '{"name": "Shop staging", "region": "1-eu", "parseServerVersion": "release-6.5.11-ubi8-node18"}'
```

#### Example response

`200`, the new app **without** a `data` wrapper, with the same fields as in [`GET /users/info`](#get-usersinfo):

```json
{ "objectId": "NeW4pPAbC0", "name": "Shop staging", "region": "1-eu", "parseServerVersion": "release-6.5.11-ubi8-node18", "…": "…" }
```

An unavailable version answers `400` with the text `Parse Server version <imageTag> isn't available.`

### `PUT /apps/{appObjectId}/name`

Scope: `apps:write`. Renames the app. Body: `{ "name": "Shop" }`. Answers `204`.

### `GET /products/parse-server-versions`

Any scope. The Parse Server versions apps can run.

#### Example response

```json
{
  "data": [
    { "imageTag": "release-3.6.0-node18", "version": "3.6.0" },
    { "imageTag": "release-6.5.11-ubi8-node18", "version": "6.5.11" }
  ]
}
```

One version can carry `"latest": true`: the one new apps get when `POST /apps` names none.

### `PUT /apps/{appObjectId}/version`

Scope: `apps:write`. Switches the app to another Parse Server version and **redeploys it**. Check what your cloud code needs first: the [MCP server's](/en/docs/developers/mcp) `plan_parse_server_upgrade` lists it.

| Body | Type | Required | |
|---|---|---|---|
| `imageTag` | string | yes | From [the versions](#get-productsparse-server-versions) |

#### Example response

```json
{ "data": { "imageTag": "release-6.5.11-ubi8-node18", "version": "6.5.11" } }
```

When the app's settings must change first (e.g. a social login that the new version configures differently), it answers `400` with the reasons as text.

## Builds

A build packages the app's cloud code from its GitHub repository into a new image. A [deployment](#deployments) then runs it.

### `GET /apps/{appObjectId}/builds`

Scope: `apps:read`. The app's builds, newest first.

| Query | Type | Default | |
|---|---|---|---|
| `limit` | number | 30 | |
| `populateGithubData` | boolean | `true` | Adds the commit's author and message to `trigger` |

#### Example response

```json
{
  "data": [
    {
      "objectId": "BuILdAbCd0",
      "name": "parse-server-buildconfig-42",
      "number": 42,
      "phase": "Complete",
      "region": "1-eu",
      "duration": 96,
      "trigger": {
        "source": "GitHub",
        "commit": "9fceb02d0ae598e95dc970b74767f19372d61af8",
        "message": "Validate notes in beforeSave",
        "author": { "name": "Ada Lovelace", "email": "ada@example.com" }
      },
      "createdAt": "2026-11-12T13:58:40.000Z",
      "updatedAt": "2026-11-12T14:00:16.000Z"
    }
  ]
}
```

`phase`: `New`, `Pending`, `Running`, `Complete`, `Failed`, `Error` or `Cancelled`.

A build someone started from the dashboard, the API or MCP also has `startedBy`, as in the [deployment history](#get-appsappobjectiddeploymentshistory).

### `GET /apps/{appObjectId}/builds/{buildId}/log`

Scope: `apps:read`. The end of a build's log, where a failure shows.

| Query | Type | Default | |
|---|---|---|---|
| `lines` | number | 300 | The last lines; up to 1000 |

#### Example response

```json
{
  "data": {
    "buildId": "BuILdAbCd0",
    "number": 42,
    "phase": "Failed",
    "finished": true,
    "text": "npm ERR! code ETARGET\nnpm ERR! notarget No matching version found for stripe@^99.0.0.\n",
    "truncated": true
  }
}
```

`truncated` says earlier lines were left out. An unknown `buildId` answers `400`, code 200, `Build not found.`

### `POST /apps/{appObjectId}/build`

Scope: `deploy`. Starts a build of the cloud code in the app's repository. Answers `204`; the build appears in the list.

### `POST /apps/{appObjectId}/build/cancel`

Scope: `deploy`. Body: `{ "name": "parse-server-buildconfig-43" }`, the build's `name`. Answers `204`.

## Deployments

### `GET /apps/{appObjectId}/deployments`

Scope: `apps:read`. The app's deployments, newest first.

| Query | Type | Default | |
|---|---|---|---|
| `limit` | number | 10 | Up to 50 |

#### Example response

```json
{
  "data": [
    {
      "objectId": "DpL0yAbCd1",
      "name": "parse-server-deployment-110",
      "number": 110,
      "phase": "Running",
      "region": "1-eu",
      "duration": 0,
      "replicas": { "auto": false, "min": 1, "max": 1 },
      "createdAt": "2026-11-13T09:12:01.000Z",
      "updatedAt": "2026-11-13T09:12:05.000Z"
    },
    {
      "objectId": "DpL0yAbCd0",
      "name": "parse-server-deployment-109",
      "number": 109,
      "phase": "Active",
      "region": "1-eu",
      "duration": 74,
      "replicas": { "auto": false, "min": 1, "max": 1 },
      "createdAt": "2026-11-12T14:02:11.000Z",
      "updatedAt": "2026-11-12T14:03:25.000Z"
    }
  ]
}
```

`Active` is the deployment serving traffic. `New`, `Pending` and `Running` are rolling out. Earlier ones end as `Complete`, `Failed`, `Cancelled` or `Error`.

### `POST /apps/{appObjectId}/deploy`

Scope: `deploy`. Deploys the app's latest build. Answers `204`; the new deployment appears in the list, first as `Running`, then `Active`.

### `POST /apps/{appObjectId}/deploy/cancel`

Scope: `deploy`. Body: `{ "name": "parse-server-deployment-110" }`, the deployment's `name`. Answers `204`.

### `GET /apps/{appObjectId}/deployments/history`

Scope: `apps:read`. The deployments, newest first, each with what started it (`cause`) and who (`startedBy`). Work SashiDo does on its own infrastructure isn't listed.

| Query | Type | Default | |
|---|---|---|---|
| `limit` | number | 100 | Up to 100 |

#### Example response

```json
{
  "data": [
    {
      "objectId": "DpL0yAbCd2",
      "name": "parse-server-deployment-111",
      "number": 111,
      "phase": "Active",
      "region": "1-eu",
      "createdAt": "2026-11-14T08:40:12.000Z",
      "updatedAt": "2026-11-14T08:41:30.000Z",
      "cause": {
        "kind": "settings",
        "changes": [
          { "name": "PARSE_SERVER_PUSH_ENABLE_REST", "change": "changed", "from": "off", "to": "on" },
          { "name": "PARSE_SERVER_LIVE_QUERY_OPTIONS", "change": "changed", "from": "none", "to": "Message, Todo" }
        ]
      },
      "startedBy": {
        "client": "mcp/0.2.0",
        "user": { "name": "Ada Lovelace", "email": "ada@example.com" },
        "tokenName": "Claude Code",
        "mcpClient": "claude-code",
        "mcpTool": "update_app_settings",
        "ip": "203.0.113.7"
      }
    },
    {
      "objectId": "DpL0yAbCd1",
      "name": "parse-server-deployment-110",
      "number": 110,
      "phase": "Complete",
      "region": "1-eu",
      "createdAt": "2026-11-13T09:12:01.000Z",
      "updatedAt": "2026-11-13T09:13:15.000Z",
      "cause": {
        "kind": "build",
        "build": { "number": 42, "source": "GitHub", "commit": "9fceb02d0ae598e95dc970b74767f19372d61af8", "message": "Validate notes in beforeSave" }
      },
      "startedBy": { "client": "github", "user": { "name": "Ada Lovelace", "email": "ada@example.com" } }
    }
  ]
}
```

- `cause.kind`: `settings`, `env` (environment variables), `domain`, `push_certificate`, `version` (Parse Server version), `engine`, `high_availability`, `manual` (Start Deploy) or `build`.
- `changes` names what changed: `added`, `removed` or `changed`. Before and after (`from`, `to`) are shown only for settings that hold nothing secret, such as on/off options, numbers and the Live Queries classes; a variable's or a key's value never is. Without `env:read`, a token sees that variables changed, not their names.
- `startedBy.client`: `dashboard`, `api`, `mcp/<version>` (with the AI client and its tool) or `github` (a push to the app's repository, with the commit's author).
- `startedBy.ip` is the address the change came from, shown only to the person who made it: a collaborator sees who, not where.
- Deployments from before November 14, 2026 have no `cause` or `startedBy`.

### `GET /apps/rollouts/active`

Scope: `apps:read`. What your apps are building or deploying now, and what finished in the last two minutes: the dashboard's "your app is updating" indicator. With a token limited to some apps, only those.

```json
{
  "data": [
    { "appObjectId": "AbC123xYz", "appName": "my-app", "kind": "deploy", "reason": "settings", "region": "1-eu", "startedAt": "2026-11-14T08:40:12.000Z" }
  ],
  "recent": [
    { "appObjectId": "QrS456uVw", "appName": "staging", "kind": "build", "reason": "git", "region": "1-eu", "startedAt": "2026-11-14T08:37:02.000Z", "state": "done", "endedAt": "2026-11-14T08:39:40.000Z" }
  ]
}
```

`kind` is `build` or `deploy`; `reason` is what started it, as in the history's `cause.kind`, or `git` for a push to the app's repository. `state` is `done` or `failed`.

## Engines, scaling and high availability

These change your bill, and only the app's owner can make them. A collaborator gets `400` with `only app owner can change engines`.

### `GET /products/parse-server-engines`

Scope: `apps:read`. The engines your plan offers.

#### Example response

```json
{
  "data": [
    {
      "name": "Production 1X",
      "billingPolicy": "hourly",
      "default": false,
      "allowedInTrial": false,
      "supportsAutoScaling": true,
      "supportsHighAvailability": true,
      "params": { "cpu": 4, "ram": 1073741824 }
    }
  ]
}
```

`ram` is in bytes; `cpu` is the engine's relative CPU share.

### `PUT /apps/{appObjectId}/engine`

Scope: `apps:write`. **Billable.** Body: `{ "name": "Production 1X", "billingPolicy": "hourly" }`. Optional `?region=`. Answers `204`.

### `PUT /apps/{appObjectId}/scale`

Scope: `apps:write`. **Billable.** Sets the number of instances.

| Body | Type | Required | |
|---|---|---|---|
| `auto` | boolean | yes | Auto-scaling (engines with `supportsAutoScaling`) |
| `min` | number | yes | Instances when `auto` is off; the minimum with it on |
| `max` | number | yes | The maximum with `auto` on; at least `min` |
| `cpuTarget` | number | no | The CPU % at which auto-scaling adds instances |

`min` and `max` are brought within your limits (`settings.engine_scaling_config` in [`/users/info`](#get-usersinfo)). Optional `?region=`. Answers `204`.

```bash
curl -X PUT "https://api.sashido.io/apps/$APP/scale" \
  -H "Authorization: Bearer $SASHIDO_TOKEN" -H "Content-Type: application/json" \
  -d '{"auto": true, "min": 2, "max": 4}'
```

### `PUT /apps/{appObjectId}/highAvailability`

Scope: `apps:write`. **Billable.** Body: `{ "enabled": true }`. Keeps at least three instances with health checks, and resets `min` and `max` to your limits. An engine without `supportsHighAvailability` answers `400`. Answers `204`.

## Environment variables

### `GET /apps/{appObjectId}/env`

Scope: `env:read`.

#### Example response

```json
{
  "data": [
    { "name": "STRIPE_KEY", "value": "sk_test_…", "perm": "rw" },
    { "name": "NODE_ENV", "value": "production", "perm": "ro" }
  ]
}
```

`perm`: `rw` are your variables; `ro` are set by SashiDo and read-only. A token sees your own variables and the app's addresses (`NODE_ENV`, `PARSE_SERVER_URL`, `PARSE_PUBLIC_SERVER_URL`). Credentials SashiDo manages, such as the database connection string, are never returned to a token; the dashboard shows them to you.

### `PUT /apps/{appObjectId}/env`

Scope: `env:write`. **Replaces your variables** (`rw`) with the list you send: your variables left out are removed, so send all of them. Variables SashiDo manages stay as they are; you don't need to send them. Answers `204`. When the variables change, the app is redeployed with them, as when you save them in the dashboard; follow it in [the deployments](#get-appsappobjectiddeployments).

| Body | Type | Required | |
|---|---|---|---|
| `env` | array | yes | `[{ "name": "STRIPE_KEY", "value": "sk_live_…" }]` |

Refused with `400` and the names in the text:

- names that aren't letters, digits and `_`, or start with a digit: `Invalid env variable name 9LIVES`;
- the same name twice: `Cannot update vars more than once in the same request …`;
- read-only or reserved names, such as `NODE_ENV`, `PORT`, `PATH`, the variables SashiDo manages and those starting with `PARSE_SERVER_`, `PARSEGROUND_` or `SASHIDO_`: `Cannot update read-only/reserved env variables …`.

### `POST /apps/{appObjectId}/deploy-env`

Scope: `deploy`. Redeploys the app with its current variables, e.g. when a redeploy after a change didn't go through. `PUT /env` doesn't need it. Optional `?region=`. Answers `204`.

## App Settings

The settings of **App Settings** in the dashboard: users and passwords, social logins, email, custom pages, push, Live Queries and API options.

### `GET /apps/{appObjectId}/settings`

Scope: `settings:read`. Secrets are never returned, only whether they are set (`clientSecretSet`, `appSecretSet`, `consumerSecretSet`).

#### Example response (shortened)

```json
{
  "data": {
    "users": {
      "usernameAndPassword": true,
      "anonymousUsers": false,
      "sessionLengthSeconds": 31536000,
      "passwordPolicy": { "validatorPattern": null, "doNotAllowUsername": true, "maxPasswordAgeDays": null, "maxPasswordHistory": 5, "resetTokenValidityDurationSeconds": null },
      "accountLockout": { "threshold": 5, "durationMinutes": 15 },
      "mfaTotp": false
    },
    "socialLogin": {
      "google": { "enabled": true, "clientIds": ["1234-abc.apps.googleusercontent.com"] },
      "github": { "enabled": true, "clientId": "Iv1.0123456789abcdef", "clientSecretSet": true, "acceptAccessToken": false },
      "facebook": { "enabled": false, "appId": null, "appSecretSet": false }
    },
    "email": { "verifyUserEmails": true, "senderAddress": "hello@example.com", "senderDisplayName": "My App" },
    "push": { "clientPush": false, "restPush": true },
    "liveQueries": { "classes": ["Message", "Todo"] },
    "api": { "allowClientClassCreation": false, "maxLimit": null, "allowHeaders": [] },
    "availability": { "mfaTotp": "8.6.104" }
  }
}
```

`availability` names the Parse Server version a setting needs when the app's version is older (`null` when the app has it). Such a setting can't be turned on until you [upgrade](#put-appsappobjectidversion).

### `PATCH /apps/{appObjectId}/settings`

Scope: `settings:write`. Send only what changes, in the sections `GET` shows; everything else stays as it is, nested settings too. `null` clears a text, number or object. Secrets (`clientSecret`, `appSecret`, `consumerSecret`) can be set, never read.

```bash
curl -X PATCH https://api.sashido.io/apps/AbC123xYz/settings \
  -H "Authorization: Bearer $SASHIDO_TOKEN" -H "Content-Type: application/json" \
  -d '{ "push": { "restPush": true }, "liveQueries": { "classes": ["Message", "Todo"] } }'
```

The answer is `{ "data": { "settings": {…}, "redeploys": true } }`: the settings as saved, and whether the app redeploys to apply them (most changes do; follow it in the [deployments](#get-appsappobjectiddeployments)).

The same checks as the dashboard apply: an unknown setting, a wrong type or a setting the app's version lacks is refused with code 310, and the platform's rules (such as allowed email senders) with 311.

## Custom domain

Serve the app's API, files, email links and website on your own domain. See [Hosting and domains](/en/docs/developers/hosting#a-custom-domain).

### `GET /apps/{appObjectId}/domain`

Scope: `settings:read`.

```json
{
  "data": {
    "hostName": "api.example.com",
    "letsEncrypt": true,
    "dns": {
      "target": "pg-app-abc123.scalabl.cloud",
      "records": [
        "api.example.com CNAME pg-app-abc123.scalabl.cloud",
        "pg-app-abc123.api.example.com CNAME pg-app-abc123.scalabl.cloud"
      ]
    }
  }
}
```

`records` lists the CNAME records the domain needs: either one works.

### `PUT /apps/{appObjectId}/domain`

Scope: `settings:write`. Body: `{ "hostName": "api.example.com", "letsEncrypt": true }`; `letsEncrypt` is optional. The CNAME record must exist first, or the change is refused with the record to create. Answers like `GET`, with `redeploys: true` when the host name changed.

### `DELETE /apps/{appObjectId}/domain`

Scope: `settings:write`. Removes the host name and the Let's Encrypt certificate; the app redeploys on its `scalabl.cloud` address. Clients using your domain stop reaching the app.

## Your app's data: the Parse proxy

`/apps/{appObjectId}/parse/<path>` forwards the request to your app's Parse Server and adds the master key on our side. Send the same method, path, query and body as to the [Parse REST API](https://docs.parseplatform.org/rest/guide/), with only your token: no Application ID, no keys. Responses and errors are Parse Server's own.

| Paths | Scope |
|---|---|
| `GET classes/<Class>`, `GET classes/<Class>/<objectId>`, `GET aggregate/<Class>` | `data:read` |
| `GET schemas`, `GET schemas/<Class>`, `GET config` | `data:read` |
| `GET hooks/functions`, `GET hooks/triggers` | `data:read` |
| `POST classes/<Class>`, `PUT classes/<Class>/<objectId>`, `DELETE classes/<Class>/<objectId>` | `data:write` |
| `POST batch` (1 to 50 writes on `classes`) | `data:write` |
| `POST schemas/<Class>`, `PUT schemas/<Class>`, `DELETE schemas/<Class>`, `PUT config` | `data:write` |
| `POST files/<name>` (up to 10 MB), `POST functions/<name>`, `POST jobs/<name>`, `POST push` | `data:write` |
| `POST`/`PUT`/`DELETE` `hooks/functions/…`, `hooks/triggers/…` | `data:write` |
| `GET scriptlog` | `logs:read` |
| `GET health` | `apps:read` |

Query parameters per path: `classes` takes `where`, `limit`, `skip`, `order`, `keys`, `excludeKeys`, `include`, `count`; `aggregate` takes `pipeline`, `match`, `group`, `project`, `sort`, `limit`, `skip`, `distinct`. Others are refused with `400 Unsupported parameter: <name>`.

### Query objects

```bash
curl -G "https://api.sashido.io/apps/$APP/parse/classes/Order" \
  -H "Authorization: Bearer $SASHIDO_TOKEN" \
  --data-urlencode 'where={"status":"paid","total":{"$gte":100}}' \
  --data-urlencode 'order=-createdAt' \
  --data-urlencode 'limit=20' \
  --data-urlencode 'keys=total,status,createdAt'
```

```json
{
  "results": [
    { "objectId": "0rDeRAbCd0", "total": 129.9, "status": "paid", "createdAt": "2026-11-12T18:20:44.512Z", "updatedAt": "2026-11-12T18:20:44.512Z" }
  ]
}
```

Add `count=1` to get `"count"` with the results.

### Create, update, delete

```bash
curl -X POST "https://api.sashido.io/apps/$APP/parse/classes/Order" \
  -H "Authorization: Bearer $SASHIDO_TOKEN" -H "Content-Type: application/json" \
  -d '{"total": 49.5, "status": "new"}'
# 201 {"objectId":"0rDeRAbCd1","createdAt":"2026-11-13T09:30:00.000Z"}

curl -X PUT "https://api.sashido.io/apps/$APP/parse/classes/Order/0rDeRAbCd1" \
  -H "Authorization: Bearer $SASHIDO_TOKEN" -H "Content-Type: application/json" \
  -d '{"status": "paid"}'
# 200 {"updatedAt":"2026-11-13T09:31:00.000Z"}

curl -X DELETE "https://api.sashido.io/apps/$APP/parse/classes/Order/0rDeRAbCd1" \
  -H "Authorization: Bearer $SASHIDO_TOKEN"
# 200 {}
```

### Call a cloud function

```bash
curl -X POST "https://api.sashido.io/apps/$APP/parse/functions/hello" \
  -H "Authorization: Bearer $SASHIDO_TOKEN" -H "Content-Type: application/json" \
  -d '{"name": "Ada"}'
# 200 {"result":"Hello, Ada!"}
```

The function runs with the master key, so `request.master` is `true`.

### Read the logs

`GET scriptlog`, scope `logs:read`. Query: `level` (`info` or `error`), `n` or `size` (number of entries), `from` and `until` (dates), `order` (`asc` or `desc`).

```bash
curl -G "https://api.sashido.io/apps/$APP/parse/scriptlog" \
  -H "Authorization: Bearer $SASHIDO_TOKEN" --data-urlencode 'level=error' --data-urlencode 'n=20'
```

```json
[
  {
    "timestamp": "2026-11-13T08:02:13.441Z",
    "level": "error",
    "message": "Failed running cloud function signup for user undefined with:\n  Input: {}\n  Error: {\"message\":\"A username is required.\",\"code\":141}",
    "meta": { "error": { "name": "Error", "message": "A username is required.", "code": 141, "stack": "Error: A username is required.\n    at …" } },
    "hostname": "parse-server-deployment-109-x7k2p"
  }
]
```

### Check the app's health

`GET health`, scope `apps:read`:

```json
{
  "status": "ok",
  "database": "ok",
  "version": "6.5.11",
  "nodeVersion": "v18.20.4",
  "region": "1-eu",
  "build": "42",
  "deployment": "109",
  "state": "finished"
}
```

### What the proxy guards

- **Only the paths above.** Anything else, such as `sessions`, `login` or `users/me`, answers `403` with `{ "code": 305, "message": "GET /sessions is not allowed." }`. The `_Session` class is never reachable.
- **No secrets in responses.** `sessionToken`, `authData` and password hashes are removed from every object, at any depth.
- **Bounded.** 30 seconds per request; responses up to 10 MB, above that `502` with `The response is too large. Use limit and keys to narrow it.`
- **Unreachable app.** `502` with `The app's Parse Server is unreachable.` while the app is down or restarting.
- **Audited.** Every write is in your Activity (without its body). Reads are summed up by the hour; reads of your users are recorded one by one.

## Recipes

### Deploy from GitHub Actions and wait until it's live

```yaml
# .github/workflows/deploy.yml
name: Deploy
on:
  workflow_dispatch:
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy and wait
        env:
          SASHIDO_TOKEN: ${{ secrets.SASHIDO_TOKEN }}
          APP: ${{ vars.SASHIDO_APP }}
        run: |
          api() { curl -sf -H "Authorization: Bearer $SASHIDO_TOKEN" -H "X-SashiDo-Client: github-actions" "$@"; }
          latest() { api "https://api.sashido.io/apps/$APP/deployments?limit=1" | jq -r '.data[0] | "\(.number) \(.phase)"'; }
          before=$(latest | cut -d' ' -f1)
          api -X POST "https://api.sashido.io/apps/$APP/deploy"
          for i in $(seq 1 60); do
            read number phase <<< "$(latest)"
            if [ "$number" != "$before" ]; then
              case "$phase" in
                Active) echo "Deployment $number is live"; exit 0 ;;
                Failed|Cancelled) echo "Deployment $number: $phase"; exit 1 ;;
              esac
            fi
            sleep 10
          done
          echo "Timed out"; exit 1
```

Use a token with `apps:read` and `deploy`, limited to this app.

### Change one environment variable

`PUT /env` replaces your variables, so read them, change one and write them back. The app redeploys with the new value:

```javascript
const { data: env } = await sashido(`/apps/${APP}/env`);
const next = env
  .filter((v) => v.perm === 'rw' && v.name !== 'STRIPE_KEY')
  .map(({ name, value }) => ({ name, value }))
  .concat({ name: 'STRIPE_KEY', value: process.env.NEW_STRIPE_KEY });

await sashido(`/apps/${APP}/env`, { method: 'PUT', body: JSON.stringify({ env: next }) });
```

### Export a class page by page

```javascript
let last = null;
for (;;) {
  const where = last ? { createdAt: { $gt: { __type: 'Date', iso: last } } } : {};
  const params = new URLSearchParams({ where: JSON.stringify(where), order: 'createdAt', limit: '1000' });
  const { results } = await sashido(`/apps/${APP}/parse/classes/Order?${params}`);
  if (!results.length) break;
  // write results somewhere
  last = results[results.length - 1].createdAt;
}
```

Paging by `createdAt` stays fast at any depth, unlike `skip`.

## Not available with tokens

These stay in the dashboard: managing tokens, deleting apps, billing, the apps' keys, signing in and two-factor authentication, metrics, and the history of push notifications.
