The Management API is the API the SashiDo dashboard and the MCP server use. With a personal access token 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.
export SASHIDO_TOKEN=sashido_pat_...
2. Check it. GET /tokens/current says what the token may do:
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:
curl https://api.sashido.io/users/info \
-H "Authorization: Bearer $SASHIDO_TOKEN"
{
"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:
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. Not the app's Application ID. |
{buildId} |
objectId of the build in GET /apps/{appObjectId}/builds |
a build's or deployment's name |
name in the builds or deployments list |
imageTag |
GET /products/parse-server-versions |
an engine's name and billingPolicy |
GET /products/parse-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, 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:
{ "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 refused or couldn't complete the request | See the message |
400 |
310 | An App Settings or 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:
{ "body.name": { "message": "'name' is required" } }
Refused changes answer 400 with the reason as plain text (Content-Type: text/html), e.g.:
only app owner can change engines
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:
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
{
"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
{
"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"
}
]
}
}
}
appIdis the app's Application ID, used by the Parse SDKs. The API's paths useobjectId.deploymentis the app's latest deployment;replicasare its instances.settings.engine_scaling_configholds the limits for scaling.
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
curl "https://api.sashido.io/activities?page=1&pageSize=20" \
-H "Authorization: Bearer $SASHIDO_TOKEN"
Example response
{
"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) oraccess_token.via: the client, and through MCP alsomcpClient(e.g. Claude Code) andmcpTool.result.status:ok,refusedorfailed, withcodeandmessagewhen it didn't succeed.data.paramsholds 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. Set it: the default may be an older version. |
engine |
object | no | { "name": "Production 1X", "billingPolicy": "hourly" } from the engines |
Example request
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:
{ "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
{
"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 plan_parse_server_upgrade lists it.
| Body | Type | Required | |
|---|---|---|---|
imageTag |
string | yes | From the versions |
Example response
{ "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 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
{
"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 /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
{
"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
{
"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
{
"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) orbuild.changesnames what changed:added,removedorchanged. 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. Withoutenv:read, a token sees that variables changed, not their names.startedBy.client:dashboard,api,mcp/<version>(with the AI client and its tool) orgithub(a push to the app's repository, with the commit's author).startedBy.ipis 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
causeorstartedBy.
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.
{
"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
{
"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). Optional ?region=. Answers 204.
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
{
"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.
| 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 withPARSE_SERVER_,PARSEGROUND_orSASHIDO_: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)
{
"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.
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.
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).
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.
GET /apps/{appObjectId}/domain
Scope: settings:read.
{
"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, 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
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'
{
"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
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
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).
curl -G "https://api.sashido.io/apps/$APP/parse/scriptlog" \
-H "Authorization: Bearer $SASHIDO_TOKEN" --data-urlencode 'level=error' --data-urlencode 'n=20'
[
{
"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:
{
"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,loginorusers/me, answers403with{ "code": 305, "message": "GET /sessions is not allowed." }. The_Sessionclass is never reachable. - No secrets in responses.
sessionToken,authDataand password hashes are removed from every object, at any depth. - Bounded. 30 seconds per request; responses up to 10 MB, above that
502withThe response is too large. Use limit and keys to narrow it. - Unreachable app.
502withThe 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
# .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:
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
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.