Developer Docs
Quickstart to production in under 5 minutes.
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
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 | forbidden | Lacks permission |
| 404 | not_found | Consent request ID not found |
| 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 |