Developer Docs
Integrate in 5 minutes. Verify your account once to go live.
Quickstart
Install
pip install permitly
npm install permitly
Minimal working example (Python)
from permitly import PermitlyClient, ScopeTemplates
client = PermitlyClient("sk_live_...")
# Step 1 — request consent, get a URL to redirect your user to
result = client.consent.request({
"end_user_ref": "user_123",
"scopes": [ScopeTemplates.send_email()],
"redirect_url": "https://yourapp.com/done"
})
return redirect(result.consent_url)
# Step 2 — after user approves, verify the token your agent receives
payload = client.consent.verify(token)
# payload.scopes contains the approved scopes
# Step 3 — receive webhook events (Flask example)
from flask import request
from permitly import WebhookVerifier
verifier = WebhookVerifier("whsec_...")
@app.route("/webhook", methods=["POST"])
def webhook():
event = verifier.verify(request.data, request.headers["X-Permitly-Signature"])
if event["type"] == "consent.approved":
# grant the agent access
pass
return "", 200
Account Verification
You can build and test against the full API immediately after signing up — no verification required to start. Verification is only needed before your agents interact with real end users.
You can build and test against the API immediately after signing up. Before any agent under your account can send a consent request to a real end user, your account needs two things on file — once, not per agent:
- →Domain verification. Claim a domain in the dashboard, then publish a DNS TXT record we give you. DNS changes can take a few hours to propagate — that's normal, just check again later.
- →Payment method on file. Add a card via Stripe Checkout. No charge is made — this is purely an identity/accountability check, required even on the Free plan.
Until both are complete, POST /v1/consent/request returns a
403 with code
account_not_verified. Everything else — creating agents,
configuring webhooks, generating API keys — works before verification, so you can fully integrate before deciding to go live.
Once verified, your domain is shown on every consent page your agents send, so end users have something concrete to check your agent's identity against. See Trust & Security for why this exists.
Authentication
All API requests require a bearer token. API keys have the prefix sk_live_.
Authorization: Bearer sk_live_abcdef1234567890
Get your key from Settings → API Keys in your dashboard. Keys are shown once — store them securely. We store only the bcrypt hash.
Consent Requests
POST /v1/consent/request
Create a consent request and get a hosted URL to redirect your user to.
{
"agent_id": "ag_calendarbot",
"end_user_ref": "user_123",
"end_user_email": "alice@example.com", // optional
"scopes": [
{ "key": "calendar.book", "label": "Book appointments", "risk": "low" }
],
"redirect_url": "https://yourapp.com/done",
"expires_in": 3600 // seconds, default 1800
}
Response: { "data": { "id": "cr_...", "consent_url": "https://permitly.dev/c/cr_...", "status": "pending" } }
GET /v1/consent/{id}
Get the current status of a consent request. Status values: pending, approved, declined, expired, revoked.
GET /v1/consent
List consent requests. Supports query params: status, agent_id, end_user_ref, per_page, page.
Response includes meta.total, meta.current_page.
DELETE /v1/consent/{id}
Revoke an active consent. Fires consent.revoked webhook immediately. Returns 204 No Content.
Token Verification
After consent is approved, your agent receives a signed JWT. Verify it server-side before granting access.
POST /v1/consent/verify
{ "token": "eyJhbGci..." }
JWT payload fields
| Field | Description |
|---|---|
| sub | End user reference (your end_user_ref value) |
| agt | Agent ID |
| cid | Consent request ID (cr_...) |
| scp | Array of approved scope keys |
| iat | Issued at (Unix timestamp) |
| exp | Expires at (Unix timestamp) |
| jti | JWT ID — used for revocation lookup |
Python example
payload = client.consent.verify(token)
# Raises PermitlyError on invalid/expired/revoked token
if "calendar.book" in payload.scopes:
# grant calendar access
pass
Webhooks
Permitly fires a webhook on every consent lifecycle event. Configure your endpoint in the dashboard under Agents → Webhooks.
Payload shape
{
"id": "evt_01abc...",
"type": "consent.approved", // consent.approved | consent.declined | consent.revoked
"created_at": "2026-05-26T14:00:00Z",
"data": {
"consent_id": "cr_...",
"agent_id": "ag_calendarbot",
"end_user_ref": "user_123",
"scopes": ["calendar.book"],
"token": "eyJhbGci..." // only on consent.approved
}
}
Signature verification
Every request includes X-Permitly-Signature: sha256=<hmac>. Verify it to reject spoofed payloads.
# Python
from permitly import WebhookVerifier
verifier = WebhookVerifier("whsec_...")
event = verifier.verify(request.body, request.headers["X-Permitly-Signature"])
// Node.js
import { WebhookVerifier } from 'permitly'
const verifier = new WebhookVerifier('whsec_...')
const event = verifier.verify(req.rawBody, req.headers['x-permitly-signature'])
Scope Templates
Built-in templates produce a ready-to-use scope dict with sensible defaults. Usage: ScopeTemplates.send_email()
| Key | Label | Risk | Reversible |
|---|---|---|---|
| send_email | Send email on your behalf | HIGH | yes |
| read_email | Read email messages | HIGH | yes |
| calendar_read | Read calendar events | MEDIUM | yes |
| calendar_write | Create/edit calendar events | MEDIUM | yes |
| calendar_book | Book appointments | LOW | yes |
| files_read | Read files and documents | MEDIUM | yes |
| files_write | Create and edit files | HIGH | yes |
| contacts_read | Read contact list | MEDIUM | yes |
| contacts_write | Add or update contacts | MEDIUM | yes |
| profile_read | Read basic profile info | LOW | yes |
Error Reference
All errors use the same envelope:
{ "error": { "code": "token_expired", "message": "The consent token has expired." } }
| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthenticated | Missing or invalid API key |
| 403 | account_not_verified | Builder account has not completed domain + payment verification |
| 403 | forbidden | Lacks permission |
| 404 | not_found | The requested resource does not exist |
| 422 | validation_error | Request body failed validation |
| 422 | token_expired | JWT exp claim has passed |
| 422 | token_revoked | Consent was revoked after token issued |
| 422 | token_invalid | JWT signature verification failed |
| 429 | rate_limit_exceeded | Too many requests — back off and retry |
| 500 | server_error | Unexpected server error — retry with backoff |
| 503 | service_unavailable | Permitly is temporarily unavailable — check status page |