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 (is_partner = true, set by STACK staff along with 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",
             "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 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/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.

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"

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
stack | Docs