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 to STACK by their real email. Two branches, decided by whether the email already has a STACK account:
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"]
}'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 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"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 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.
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 a lost or leaked token. The old sk_grant_* stops working. The response returns the new plaintext once with the grant view.
curl -X POST https://api.getstack.run/v1/partner/connections/pgr_abc123/rotate-token \
-H "Authorization: Bearer sk_live_partner_key"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:
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 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.
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"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"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