Partner API
The "Connect with STACK" surface for vetted platform partners. One call connects a customer by email: STACK creates a user-owned account (or opens a consent flow if the account exists) and returns an opaque sk_grant_* token the partner uses as a normal Bearer key - scoped, provider-bound, provenance-isolated, and revocable by the customer from their own dashboard.
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.
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:
{
"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.
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.
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 your side of the edge. The token dies on its next request and everything the grant created cascades: passports of grant-created agents revoke (within 60 seconds via the revocation channel), grant-created webhooks stop resolving, grant-created service connections disconnect.
curl -X DELETE https://api.getstack.run/v1/partner/connections/pgr_abc123 \
-H "Authorization: Bearer sk_live_partner_key"{
"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_* dies immediately; the new plaintext is returned exactly once alongside the grant view.
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:
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 - a default-deny map of exactly 19 (method, route) pairs, each requiring a scope the grant carries. Any route not in the map returns 403 before the handler runs, including every governance route (reviews, detector config, security events, notifications, identity, billing, account mutations).
- 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.
The 19 reachable routes
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:writeThe scope vocabulary is deliberately a subset: no admin, no billing:*, no identity:*, no credentials:*. A grant can never read raw credentials or reach governance surfaces. Deliberate v1 exclusions (fail closed):POST /v1/passports/refresh (re-issue instead),POST /v1/passports/revoke-all (operator-wide blast radius),GET /v1/operators/me (data minimization), and scope expansion on a live grant (revoke and reconnect).
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.
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.
curl -X DELETE https://api.getstack.run/v1/operator/partner-grants/pgr_abc123 \
-H "Authorization: Bearer sk_live_your_key"Legacy: Tenant API (deprecated)
The Phase 5.6 tenant API (partner-owned child operators on placeholder emails) is superseded by connect + grants and is supported only through its deprecation window. New integrations must use POST /v1/partner/connect. Existing tenants are not auto-migrated; connect rejects placeholder-domain emails.
POST /v1/partner/tenants - create tenant
GET /v1/partner/tenants - list tenants
GET /v1/partner/tenants/:id - get tenant
DELETE /v1/partner/tenants/:id - soft-revoke tenant (cascades)
POST /v1/partner/tenants/:tenant_id/inbound-webhooks - provision webhook under tenant
GET /v1/partner/tenants/:tenant_id/inbound-webhooks - list tenant webhooks
DELETE /v1/partner/tenants/:tenant_id/inbound-webhooks/:wh_id - revoke tenant webhook
POST /v1/partner/tenants/:tenant_id/passports/issue - issue passport under tenantEndpoint Summary
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