Authentication
Your people call with a session from your sign-in application. Your systems call with a machine token an admin creates in Studio. Every call names your organization in the path, and every route checks the caller against it.
- Header
- Authorization: Bearer
- Scope
- One organization per call
- Environments
- DEV · UAT · PROD
01Three credentials
| Credential | Who holds it | Where it works today | Status |
|---|---|---|---|
| User sessionClerk session token | A member signed in to Studio or to your app on your instance's sign-in application. | Every REST route on the four services, within the member's role. | Shipped |
| Machine tokenOne per environment | Your system. An admin creates it on Studio's Machine access page. | The webhook trigger of a secure automation in that environment. | Shipped |
| MCP keyPer MCP config | An outside AI assistant. | The MCP server of its config. Studio creates, lists, and deactivates keys. | Preview |
Authentication · ChecksWhat every request passes before it changes anything, drawn on a sample call.
02User sessions
Every request carries the session token, as Authorization: Bearer or the __session cookie. The token names the member's active organization, their role, and the permissions of that role.
- Short-lived. A session token lasts about a minute. The SDK in your app refreshes it, so ask for a token before each call instead of storing one.
- Role based. Admin holds every permission. Builder creates, edits, and runs everyday objects outside PROD. Member reads everything except secrets, machine access, members, and billing.
- Recorded. Runs and activity record the member as
user:{user_id}with their name as the label.
// In a browser app that uses Clerk on your instance's sign-in application
const token = await window.Clerk.session.getToken();
await fetch(`${base}/core/api/v1/organizations/${orgId}/me`, {
headers: { Authorization: `Bearer ${token}` },
});
03Machine tokens
A machine token is how your system calls in, one per environment. Studio's Settings, Machine access page creates it, shows it once, lists it masked afterwards, and revokes it.
- Create. An admin picks the environment and creates the token. An environment that already has an active token gets that one back, never a second.
- Store. Copy the token into your system's secret store when Studio shows it. It is never shown again.
- Attach. In the automation's webhook settings, mark the webhook secure and choose the token for each environment it runs in.
- Revoke. Revoking takes effect at once, with one caveat: a verification is cached for up to 60 seconds, so a revoked token can still start a run inside that window.
# Studio makes this call for you. It needs an admin session.
curl -s -X POST "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/security/m2m" \
-H "Authorization: Bearer $NSTACK_SESSION" \
-H "Content-Type: application/json" \
-d '{"env_id":"{env_id}"}'
{
"key_ref": "…",
"token": "{machine_token}",
"token_id": "{token_id}",
"env_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"created": true,
"created_at": "2026-10-02T14:30:00.000Z",
"expires_at": null
}
The token works until you revoke it. Keep it in a secret store, never in source control or a client app, and revoke it in Studio the moment you suspect a leak.
04Secure webhooks
A secure webhook starts an automation in one environment. NSTACK verifies the token, checks that it belongs to your organization and is attached to this webhook for this environment, then queues the run. The run records the token as its actor, machine:{token_id}.
curl -s -X POST "$NSTACK_BASE/workflow/api/v1/organizations/$NSTACK_ORG/workflows/$WORKFLOW_ID?env=$ENV_ID" \
-H "Authorization: Bearer $NSTACK_MACHINE_TOKEN" \
-H "Content-Type: application/json" \
-d @payload.json
const url = `${process.env.NSTACK_BASE}/workflow/api/v1/organizations/${process.env.NSTACK_ORG}` +
`/workflows/${workflowId}?env=${envId}`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NSTACK_MACHINE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`webhook refused: ${res.status}`);
import os
import requests
url = (f"{os.environ['NSTACK_BASE']}/workflow/api/v1/organizations/{os.environ['NSTACK_ORG']}"
f"/workflows/{workflow_id}")
res = requests.post(url, params={"env": env_id}, json=payload,
headers={"Authorization": f"Bearer {os.environ['NSTACK_MACHINE_TOKEN']}"})
res.raise_for_status()
The body is any JSON object, passed to the run as its input. The call answers {"status": "accepted", "correlation_id", "message"} and the automation runs after the response. Read what it did on the run record.
05MCP keys Preview
An outside AI assistant sends its key as Authorization: Bearer to the MCP server of one config. Studio creates, lists, and disables the keys, and every call shows on the run record with the key as its actor. The MCP page covers the calls.
- Today. A key belongs to one MCP config and one organization. Studio shows its value once, at creation.
- Planned. Scopes per key, OAuth 2.1 for clients that support it, and a run record per tool call.
06Organization scoping
Every route carries the organization in its path, /v1/organizations/{org_id}/…, and the path must name the session's active organization. A member of two organizations switches the active one before calling the other. Nobody gets an exception.
{ "error": { "code": "org_mismatch", "message": "The organization in the path is not the session's active organization" } }
A request for another organization's records, runs, or activity is refused the same way.
07Environments and PROD
An organization has its environments, usually DEV, UAT, and PROD. Each has an env_id, an alias, and an is_production flag. Connection settings, knowledge indexes, agent pins, automations, and machine tokens are all set per environment.
- The service enforces it. A change that touches a production environment needs the permission to manage production on top of the route's own. When NSTACK cannot confirm that an environment is not production, it refuses the change.
- Studio confirms it. Before it sends a PROD change, Studio asks the member to type the environment's alias.
- Your code does both. Read
is_productionfrom the environment and put your own confirmation in front of any change you send to it.
08Errors you will see
| Status | Code | Means |
|---|---|---|
| 401 | unauthorized | No token, or a token that is expired, revoked, or unverifiable. Fetch a fresh session token, or check the machine token in Studio. |
| 403 | org_mismatch | The path names an organization other than the session's active one. |
| 403 | forbidden | The role lacks the route's permission, or the change touches PROD without the permission to manage production. |
| 403 | machine_token_not_accepted | The webhook is not marked secure, so it takes a member session instead. |
| 403 | machine_token_not_attached | The token is valid but not attached to this webhook for this environment. |
| 409 | conflict | The instance_id you passed has already run. |
| 503 | production_check_unavailable | The service could not confirm with core whether the environment is production, so it refused the change. Retry. |
| 503 | machine_token_unverified | The workflow service could not reach Clerk to verify the token. Retry with backoff. |
Error bodies take the form {"error": {"code", "message"}}.
09In design
- In design Machine access to REST. Your system reads datasets and runs with a machine credential, scoped by permission and environment, with no member session.
- In design Scoped MCP keys. Scopes per key, and OAuth 2.1 for assistants that support it.