STACK
MENU
DOCS / API REFERENCE / PARTNER API

Partner API

Vetted platform partners can connect a customer by email. STACK creates a user-owned account or opens a consent flow for an existing account. The partner receives an opaque sk_grant_* token limited to the approved routes, providers, and resources.

The /v1/partner/* endpoints require a partner operator API key. STACK staff mark your account as a partner and set your allowed provider list. Non-partner keys get 403. The grant token itself is documented in the "Using the Grant Token" section below.

Connect a Customer

POST /v1/partner/connect

Connect a customer to STACK by their real email. Two branches, decided by whether the email already has a STACK account:

  • New email - STACK creates a user-owned operator (real email, no parent, unclaimed until first login). The grant is immediately active. The customer receives a GDPR Art. 14 notice email naming your platform, with a claim link and a one-click opt-out that deletes the account while unclaimed. Notice delivery is a hard gate: if the email fails to send, account and grant roll back and you get a retriable 503.
  • Existing email - the grant is created as pending_consent. The customer receives a consent email leading to the dashboard approve/decline page. Consent tokens are single-use, sha256-stored, and expire after 7 days. A pending_consent grant token authenticates nothing until approval.
bash
curl -X POST https://api.getstack.run/v1/partner/connect \
  -H "Authorization: Bearer sk_live_partner_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "customer@example.com",
    "external_customer_id": "cus_your_id",
    "providers": ["agentmail"]
  }'

ConnectInput Fields

  • email (string, required) - the customer's real email. The account is theirs from day one. Placeholder domains (*.stack-tenant.local) are rejected.
  • external_customer_id (string, optional) - your stable customer identifier. Acts as the idempotency key: repeat calls return the existing connection with created: false and no token.
  • scopes (string[], optional) - defaults to the full grant vocabulary; must stay within it.
  • providers (string[], optional) - provider slugs to bind the grant to; defaults to your staff-configured provider list and must stay within it.

Returns 201 when the grant was created, 200 on an idempotent repeat:

json
{
  "grant_id": "pgr_abc123",
  "status": "active",
  "grant_token": "sk_grant_...",
  "user_operator_id": "op_xyz",
  "external_customer_id": "cus_your_id",
  "scopes": ["agents:read", "agents:write", "passports:read", "passports:write",
             "missions:read", "missions:write", "proxy:write", "services:read", "services:connect",
             "inbound-webhooks:read", "inbound-webhooks:write"],
  "allowed_providers": ["agentmail"],
  "created": true
}

grant_token is returned exactly once, on the call that created the grant (created: true). Idempotent repeats return created: false without it. There is no retrieval path - if the token is lost, rotate it.

On the existing-email branch the response carries status: "pending_consent"; poll the connection (or wait for the customer) until it transitions to active, declined, or expired. Rate limit: 100 connect calls per hour per partner; a breach fires a partner_connect_burst security event and returns 403 with code TIER_LIMIT_EXCEEDED, not 429.

List Connections

GET /v1/partner/connections

List every grant your partner operator holds, in all statuses (active, pending_consent,revoked, declined,expired). Tokens are never returned.

bash
curl https://api.getstack.run/v1/partner/connections \
  -H "Authorization: Bearer sk_live_partner_key"

Get a Connection

GET /v1/partner/connections/:id

One grant by id. Includes status, scopes, allowed providers, and lifecycle timestamps (consented_at, revoked_at,last_used_at). Grants belonging to another partner return 404.

bash
curl https://api.getstack.run/v1/partner/connections/pgr_abc123 \
  -H "Authorization: Bearer sk_live_partner_key"

Revoke a Connection

DELETE /v1/partner/connections/:id

Revoke the grant. The token fails on its next request. Passports of grant-created agents revoke and fail on their next STACK-verified call, grant-created webhooks stop resolving, and grant-created service connections disconnect.

bash
curl -X DELETE https://api.getstack.run/v1/partner/connections/pgr_abc123 \
  -H "Authorization: Bearer sk_live_partner_key"
json
{
  "revoked": true,
  "agents_passports_revoked": 3,
  "webhooks_revoked": 2,
  "connections_disconnected": 1
}

Rotate the Grant Token

POST /v1/partner/connections/:id/rotate-token

Rotate a lost or leaked token. The old sk_grant_* stops working. The response returns the new plaintext once with the grant view.

bash
curl -X POST https://api.getstack.run/v1/partner/connections/pgr_abc123/rotate-token \
  -H "Authorization: Bearer sk_live_partner_key"

Using the Grant Token

The grant token is an opaque credential resolved server-side on every request (peppered hash at rest - not a JWT, nothing to decode). Use it exactly like an API key:

bash
curl -X POST https://api.getstack.run/v1/agents/register \
  -H "Authorization: Bearer sk_grant_..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "inbox-agent", "accountability_mode": "logged" }'

A grant has no ambient authority. Three fail-closed layers bound every request:

  • Route allowlist - each available route requires a scope carried by the grant. Every other route returns 403 before the handler runs, including governance, identity, billing, and account-management routes.
  • Provider binding - service connects, inbound webhooks, and proxy targets must name a provider inside the grant's allowed_providers (itself a subset of your staff-set partner provider list). Anything else returns 403.
  • Resource provenance - everything a grant creates is stamped created_via_grant_id. A grant only sees and touches what it created; foreign resources (including other partners on the same customer) return 404, so nothing on the account is enumerable.

Routes available to a grant

text
POST   /v1/agents/register                    agents:write
PATCH  /v1/agents/:id                         agents:write
GET    /v1/agents                             agents:read
GET    /v1/agents/:id                         agents:read
GET    /v1/agents/:id/stats                   agents:read
POST   /v1/passports/issue                    passports:write
POST   /v1/passports/verify                   passports:read
POST   /v1/passports/revoke                   passports:write
GET    /v1/passports/active                   passports:read
POST   /v1/missions                           missions:write
POST   /v1/missions/start                     missions:write + passports:write
POST   /v1/missions/:id/complete              missions:write
POST   /v1/missions/:id/revoke                missions:write
GET    /v1/missions                           missions:read
GET    /v1/missions/:id                       missions:read
POST   /v1/proxy                              proxy:write
GET    /v1/services                           services:read
GET    /v1/services/connected                 services:read
GET    /v1/services/templates                 services:read
POST   /v1/services/connect                   services:connect
POST   /v1/services/grant                     services:connect
POST   /v1/operator/inbound-webhooks          inbound-webhooks:write
GET    /v1/operator/inbound-webhooks          inbound-webhooks:read
GET    /v1/operator/inbound-webhooks/:id      inbound-webhooks:read
DELETE /v1/operator/inbound-webhooks/:id      inbound-webhooks:write

The scope vocabulary is deliberately a subset: no admin, no billing:*, no identity:*, no credentials:*. A grant cannot read raw credentials, change account-wide settings, or reach governance routes. Revoke and reconnect when the customer approves a different scope.

Every Passport is issued for exactly one Mission, so a grant opens a Mission before it issues. POST /v1/missions/start with agent_id opens the Mission and issues its Passport in one call. It needs both missions:write and passports:write. A grant must pass agent_id on Mission calls, naming an agent it created.

bash
curl -X POST https://api.getstack.run/v1/missions/start \
  -H "Authorization: Bearer sk_grant_..." \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_created_by_grant",
    "mission": { "intent_summary": "Triage inbound email", "intent_services": ["agentmail"] }
  }'

The response carries mission_id and passport: { token, jti, expires_at }. To issue separately, open the Mission with POST /v1/missions and pass mission_ids: ["ms_..."] to POST /v1/passports/issue. See Missions API.

Agents default to accountability_mode: "enforced", which requires an intent declaration at passport issue - without one the issue call returns 400 ACCOUNTABILITY_REQUIRED. Partners either register agents with accountability_mode: "standard" or"logged", or supply intent at issue time.

User Side: Partner Grants on Your Account

GET /v1/operator/partner-grants

As the account owner, list every partner holding a grant on your account - partner named, scopes and providers visible. This drives the Partners tab under Services in the dashboard.

bash
curl https://api.getstack.run/v1/operator/partner-grants \
  -H "Authorization: Bearer sk_live_your_key"

DELETE /v1/operator/partner-grants/:id

Revoke a partner's grant on your account. Same cascade as the partner-side revoke; returns the same cascade counts. Grant tokens can never reach these routes - a partner cannot manage its own grant from the user side.

bash
curl -X DELETE https://api.getstack.run/v1/operator/partner-grants/pgr_abc123 \
  -H "Authorization: Bearer sk_live_your_key"

White-label Tenants

White-label tenants are a supported alternative to Connect for platforms that cannot supply customer emails, or that must not expose a direct STACK account per customer. Each tenant is an isolated child operator with its own audit chain, signing key, webhooks, and passports. It has no human user; you hold its API key server-side. You see tenant metadata and usage counts, never tenant audit content, intent bodies, or security-event metadata.

Tenants have no plan of their own. Every metered tenant call (actions, including proxy calls and inbound-webhook receives; email; SMS; L3 classifications) counts against your partner account's single allowance and wallet. STACK staff set your deal's limits: monthly actionsPerMonth, emailPerMonth, smsPerMonth, and l3CallsPerMonth for the whole pool, maxTenants, and an overage mode. wallet (default) charges usage past the allowance to your prepaid wallet; block refuses it with 402 ALLOWANCE_EXHAUSTED.

POST /v1/partner/tenants

Create a tenant. The body takes an optional external_customer_id, unique per partner (a duplicate returns 409). It takes no tier; sending one returns 400 TENANT_TIER_NOT_ALLOWED. Creating a tenant past the deal's maxTenants returns 409 TENANT_LIMIT_REACHED. The tenant API key is returned once.

bash
curl -X POST https://api.getstack.run/v1/partner/tenants \
  -H "Authorization: Bearer sk_live_partner_key" \
  -H "Content-Type: application/json" \
  -d '{ "external_customer_id": "cus_your_id" }'
json
{
  "operator_id": "op_...",
  "api_key": "sk_live_...",
  "external_customer_id": "cus_your_id",
  "tier": "partner",
  "billing": "partner",
  "created_at": "2026-09-29T12:00:00.000Z"
}

GET /v1/partner/tenants, GET /v1/partner/tenants/:id

List your tenants or read one. Each view reports tier: "partner", billing: "partner", and usage: { period, actions }: the tenant's metered actions this UTC month (period is YYYY-MM), which is its share of your pool.

DELETE /v1/partner/tenants/:id

Soft-revoke a tenant. Its key stops working, and its inbound webhooks and passports are revoked. The cascade runs on every call, so a retried DELETE finishes a cascade that failed part-way. transitioned is false when the tenant was already revoked.

json
{ "transitioned": true, "webhooks_revoked": 2, "passports_revoked": 5 }

POST /v1/partner/tenants/:tenant_id/passports/issue

Issue a Passport under a tenant. The body is the POST /v1/passports/issue body, so mission_ids must hold exactly one live Mission of that tenant. Open it with the tenant key, then issue with your partner key.

bash
# 1. Open the Mission under the tenant (tenant key)
curl -X POST https://api.getstack.run/v1/missions \
  -H "Authorization: Bearer sk_live_tenant_key" \
  -H "Content-Type: application/json" \
  -d '{ "agent_id": "agt_tenant_agent", "intent_summary": "Triage inbound email", "intent_services": ["agentmail"] }'

# 2. Issue its Passport (partner key)
curl -X POST https://api.getstack.run/v1/partner/tenants/op_tenant/passports/issue \
  -H "Authorization: Bearer sk_live_partner_key" \
  -H "Content-Type: application/json" \
  -d '{ "agent_id": "agt_tenant_agent", "mission_ids": ["ms_..."] }'

A tenant key can also call POST /v1/missions/start to open the Mission and issue its Passport in one call.

Tenant billing and usage

  • Tenant keys cannot subscribe, top up, or open the billing portal: 403 TENANT_BILLED_BY_PARTNER.
  • Tenant keys cannot read decrypted credentials. GET /v1/credentials/* returns 403; tenants call services through POST /v1/proxy, which injects the credential without exposing it.
  • If your partner account is suspended or loses its partner flag, every tenant call returns 403 BILLING_ACCOUNT_INACTIVE.
  • GET /v1/usage with your partner key or a tenant key reports the pool: consumption.actions.used is the pool total, consumption.actions.own_used the caller's share, consumption.actions.pooled is true, and consumption.actions.tenants counts the tenants. A tenant's response never names the partner.

Tenant approvals

Guarded changes made with a tenant key, such as activating a fleet policy, return 403 GOVERNANCE_APPROVAL_REQUIRED with an approval id. Tenants have no human user, so your account owner or an admin member approves them in the STACK dashboard, at Governance approvals, where each one is labelled with the tenant. Then retry the same call with the tenant key and the X-Governance-Approval header. API keys, yours or the tenant's, cannot approve. GET /v1/governance/approvals with your partner key lists your tenants' approvals with tenant: { operator_id, name }, where name is the tenant's external_customer_id or its id. Each decision is recorded on the tenant's audit chain and on yours.

Tenant Webhook Signing

PUT /v1/partner/tenants/:tenant_id/inbound-webhooks/:wh_id/provider-signing-secret

For partner-owned tenants, a partner administrator configures webhook signing through a direct authenticated request. Send signing_secret in the JSON body. AgentMail uses its provider signing key; generic webhooks use the shared HMAC key configured at the sender. AgentMail forwarding can also set downstream_signing_secret for the receiving application. The response contains only {"configured":true}. OAuth, grant tokens, and agent sessions cannot configure these keys. Keep keys in the administrator's local configuration; do not paste them into an assistant conversation.

SDK methods: partner.tenants.setInboundWebhookSigningSecret in TypeScript and partner_tenants.set_inbound_webhook_signing_secret in both Python clients. Both take the tenant ID, webhook ID, signing key, and an optional downstream key.

Endpoint Summary

text
POST   /v1/partner/connect                        - connect a customer by email
GET    /v1/partner/connections                    - list your grants
GET    /v1/partner/connections/:id                - get one grant
DELETE /v1/partner/connections/:id                - revoke a grant (cascades)
POST   /v1/partner/connections/:id/rotate-token   - rotate the grant token
GET    /v1/operator/partner-grants                - (user) list partner grants on your account
DELETE /v1/operator/partner-grants/:id            - (user) revoke a partner grant
POST   /v1/partner/tenants                        - create a white-label tenant
GET    /v1/partner/tenants                        - list tenants
GET    /v1/partner/tenants/:id                    - get one tenant
DELETE /v1/partner/tenants/:id                    - revoke a tenant (cascades)
POST   /v1/partner/tenants/:tenant_id/inbound-webhooks         - provision a tenant webhook
GET    /v1/partner/tenants/:tenant_id/inbound-webhooks         - list tenant webhooks
DELETE /v1/partner/tenants/:tenant_id/inbound-webhooks/:wh_id  - revoke a tenant webhook
POST   /v1/partner/tenants/:tenant_id/passports/issue          - issue a passport under a tenant
stack | Docs