Backend API Reference
The Paygent backend exposes a small REST API. The SDK uses it. You also call it directly to manage plans, users, and subscriptions.
Base URL: https://api.paygent.to/api/v1
Authentication: Bearer token in Authorization: Bearer pg_live_... header. The product creation endpoint (POST /products) is the only exception — it's how you mint the first key.
Content type: application/json for all request and response bodies.
Products
POST /products
Create a new product (tenant). Returns the API key — exactly once. No authentication required.
import httpx
r = httpx.post(
"https://api.paygent.to/api/v1/products",
json={
"name": "MyAgent",
"contact_name": "Jane Developer",
"contact_email": "jane@example.com",
},
)
r.raise_for_status()
print(r.json())
curl -X POST https://api.paygent.to/api/v1/products \
-H "Content-Type: application/json" \
-d '{
"name": "MyAgent",
"contact_name": "Jane Developer",
"contact_email": "jane@example.com"
}'
// Coming soon
Request body:
{
"name": "MyAgent",
"contact_name": "Jane Developer",
"contact_email": "jane@example.com"
}
| Field | Type | Required |
|---|---|---|
name |
string (1–255) | yes |
contact_name |
string | null | no |
contact_email |
string | null | no |
Response (201):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "MyAgent",
"slug": "myagent",
"contact_name": "Jane Developer",
"contact_email": "jane@example.com",
"created_at": "2026-05-06T12:00:00Z",
"api_key": "pg_live_AbCd1234..."
}
api_key is returned only on creation. Store it before discarding the response.
GET /products
List products for the authenticated key.
r = httpx.get(
"https://api.paygent.to/api/v1/products",
headers={"Authorization": f"Bearer {api_key}"},
)
print(r.json())
curl https://api.paygent.to/api/v1/products \
-H "Authorization: Bearer pg_live_..."
// Coming soon
Response (200):
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "MyAgent",
"slug": "myagent",
"contact_name": "Jane Developer",
"contact_email": "jane@example.com",
"created_at": "2026-05-06T12:00:00Z"
}
]
API keys are not included.
Config
POST /config/plans
Create plan configurations.
body = {
"soft_gate_at": 0.80,
"hard_gate_at": 1.00,
"plans": [
{
"name": "pro",
"max_spend_per_period": 49.00,
"max_spend_per_session": 5.00,
"session_timeout_minutes": 30.0,
"model_limits": {
"gpt-4o": {"max_tokens_per_period": 50000},
},
"cost_rates": {
"gpt-4o": {"input": 0.0025, "output": 0.010},
},
"default_cost_rate": {"input": 0.001, "output": 0.003},
"tool_costs": {"search": 0.05},
"default_tool_cost": 0.02,
"pre_call_estimate": False,
"pre_call_buffer_tokens": 4096,
},
],
}
r = httpx.post(
"https://api.paygent.to/api/v1/config/plans",
headers={"Authorization": f"Bearer {api_key}"},
json=body,
)
r.raise_for_status()
curl -X POST https://api.paygent.to/api/v1/config/plans \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d @plans.json
// Coming soon
Request body: see Configure your first plan for the field-by-field walkthrough.
Response (201):
{
"plans": [
{
"id": "abc-uuid",
"name": "pro",
"max_spend_per_period": 49.00,
"max_spend_per_session": 5.00,
"soft_gate_at": 0.80,
"hard_gate_at": 1.00,
"cost_rates": {...},
"default_cost_rate": {...},
"tool_costs": {...},
"default_tool_cost": 0.02,
"model_limits": {...},
"session_timeout_minutes": 30.0,
"pre_call_estimate": false,
"pre_call_buffer_tokens": 4096,
"created_at": "...",
"updated_at": "..."
}
],
"duplicates": []
}
duplicates lists names that already exist for this product (skipped, not updated). Use PATCH to modify existing plans.
Errors: 409 Conflict if every plan in the request was a duplicate.
GET /config/plans
List all plans for this product.
r = httpx.get(
"https://api.paygent.to/api/v1/config/plans",
headers={"Authorization": f"Bearer {api_key}"},
)
print(r.json())
curl https://api.paygent.to/api/v1/config/plans \
-H "Authorization: Bearer pg_live_..."
// Coming soon
Response (200):
{
"plans": [
{ "id": "...", "name": "free", ... },
{ "id": "...", "name": "pro", ... }
]
}
Same PlanResponse shape as POST /config/plans.
PATCH /config/plans/{plan_id}
Partial update of a single plan. Only fields in the body are changed.
r = httpx.patch(
f"https://api.paygent.to/api/v1/config/plans/{plan_id}",
headers={"Authorization": f"Bearer {api_key}"},
json={
"max_spend_per_period": 79.00,
"model_limits": {
"gpt-4o": {"max_tokens_per_period": 80000},
},
},
)
r.raise_for_status()
curl -X PATCH https://api.paygent.to/api/v1/config/plans/$PLAN_ID \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d '{"max_spend_per_period": 79.00}'
// Coming soon
Request body: any subset of PlanSchema fields.
Response (200): the updated PlanResponse.
Errors: 404 Not Found if no plan with that id exists for this product.
Users
POST /users
Create a user.
r = httpx.post(
"https://api.paygent.to/api/v1/users",
headers={"Authorization": f"Bearer {api_key}"},
json={
"external_user_id": "user_123",
"name": "Alice",
},
)
r.raise_for_status()
curl -X POST https://api.paygent.to/api/v1/users \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d '{"external_user_id": "user_123", "name": "Alice"}'
// Coming soon
Request body:
| Field | Type | Required |
|---|---|---|
external_user_id |
string (1–255) | yes |
name |
string | null | no |
Response (201):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"external_user_id": "user_123",
"name": "Alice",
"plan_name": null,
"stripe_subscription_id": null,
"created_at": "2026-05-06T12:00:00Z",
"updated_at": "2026-05-06T12:00:00Z"
}
Errors: 409 Conflict if external_user_id already exists for this product.
POST /users/{user_id}/subscription
Update a user's plan. Call this after your own checkout flow succeeds. user_id here is the external_user_id.
from datetime import datetime, timedelta, timezone
r = httpx.post(
f"https://api.paygent.to/api/v1/users/user_123/subscription",
headers={"Authorization": f"Bearer {api_key}"},
json={
"plan_id": "pro-plan-uuid",
"stripe_subscription_id": "sub_1OabCDeFghIJ",
"period_start": datetime.now(timezone.utc).isoformat(),
"period_end": (datetime.now(timezone.utc) + timedelta(days=30)).isoformat(),
},
)
r.raise_for_status()
curl -X POST https://api.paygent.to/api/v1/users/user_123/subscription \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d '{
"plan_id": "pro-plan-uuid",
"period_start": "2026-05-01T00:00:00Z",
"period_end": "2026-06-01T00:00:00Z"
}'
// Coming soon
Request body:
| Field | Type | Required |
|---|---|---|
plan_id |
UUID | yes |
stripe_subscription_id |
string | null | no |
period_start |
ISO datetime | null | conditional |
period_end |
ISO datetime | null | conditional |
period_start and period_end are required when the assigned plan has a finite max_spend_per_period.
Response (200): updated UserResponse with plan_name populated.
Errors:
400 Bad Request— period dates missing or invalid window404 Not Found— user or plan doesn't exist
GET /users/{user_id}/session
SDK's session-bootstrap endpoint. Returns plan config, current usage, and billing period for the user. Most apps don't call this directly — the SDK does.
r = httpx.get(
f"https://api.paygent.to/api/v1/users/user_123/session",
headers={"Authorization": f"Bearer {api_key}"},
)
print(r.json())
curl https://api.paygent.to/api/v1/users/user_123/session \
-H "Authorization: Bearer pg_live_..."
// Coming soon
Response (200):
{
"user_id": "user_123",
"paygent_user_id": "550e8400-e29b-41d4-a716-446655440000",
"plan": "pro",
"sdk_enabled": true,
"plan_config": {
"max_spend_per_period": 49.00,
"max_spend_per_session": 5.00,
"soft_gate_at": 0.80,
"hard_gate_at": 1.00,
"model_limits": {...},
"cost_rates": {...}
},
"current_usage": {
"period_cost": 23.47,
"period_tokens_total": 51720,
"period_tokens_by_model": {...},
"period_cost_by_model": {...}
},
"billing_period": {
"start": "2026-05-01T00:00:00Z",
"end": "2026-06-01T00:00:00Z"
}
}
paygent_user_id is the backend's UUID for the user. The SDK stores it on disk and uses it to detect "user deleted and re-created" scenarios.
Errors: 404 Not Found if user doesn't exist.
GET /users/{user_id}/usage
Detailed usage data for a user.
r = httpx.get(
f"https://api.paygent.to/api/v1/users/user_123/usage",
headers={"Authorization": f"Bearer {api_key}"},
params={"period": "current_period", "breakdown": "model"},
)
print(r.json())
curl "https://api.paygent.to/api/v1/users/user_123/usage?period=current_period" \
-H "Authorization: Bearer pg_live_..."
// Coming soon
Query parameters:
| Param | Default | Values |
|---|---|---|
period |
current_period |
current_period, current_month, YYYY-MM |
breakdown |
model |
model |
Response (200):
{
"user_id": "user_123",
"period": "current_period",
"total_cost": 23.47,
"total_tokens": 51720,
"tokens_by_model": {
"gpt-4o": 31200,
"gpt-4o-mini": 12100
},
"cost_by_model": {
"gpt-4o": 18.20,
"gpt-4o-mini": 0.95
},
"tool_calls_count": 14
}
Errors: 404 Not Found if user doesn't exist.
Events
POST /events/batch
Ingest a batch of usage events from the SDK. The SDK calls this in the background; you typically don't call it directly. The endpoint is idempotent — duplicate ids are silently counted as duplicates rather than rejected.
events = [
{
"id": "abc-1234",
"user_id": "user_123",
"session_id": "sess-9876",
"timestamp": "2026-05-06T14:32:18Z",
"model": "gpt-4o-mini",
"input_tokens": 100,
"output_tokens": 50,
"total_tokens": 150,
"cost_tokens": 0.000045,
"cost_total": 0.000045,
},
]
r = httpx.post(
"https://api.paygent.to/api/v1/events/batch",
headers={"Authorization": f"Bearer {api_key}"},
json=events,
)
print(r.json())
curl -X POST https://api.paygent.to/api/v1/events/batch \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d '[{"id": "...", "user_id": "user_123", ...}]'
// Coming soon
Request body: JSON array (not wrapped in an object) of UsageEventSchema:
| Field | Type | Required |
|---|---|---|
id |
string (UUID) | yes — idempotency key |
user_id |
string | yes |
session_id |
string | null | no |
timestamp |
ISO datetime | yes |
model |
string | null | no |
input_tokens |
int | default 0 |
output_tokens |
int | default 0 |
total_tokens |
int | default 0 |
tool_calls |
array of strings | default [] |
cost_tokens |
float | default 0.0 |
cost_tools |
float | default 0.0 |
cost_total |
float | default 0.0 |
metadata |
dict | default {} |
Response (200):
{
"accepted": 8,
"duplicates": 2
}
Gate events (audit trail)
POST /gate-events/batch
Ingest soft-/hard-gate decisions from the SDK. Used by the SDK's background sync; rarely called directly. Idempotent.
gate_events = [
{
"id": "abc-1234",
"user_id": "user_123",
"timestamp": "2026-05-06T14:32:18Z",
"status": "hard_gate",
"gate_reason": "total_spend",
"usage_pct": 1.02,
"current_value": 49.91,
"limit_value": 49.00,
"blocked": True,
"message": "Spend limit reached",
},
]
httpx.post(
"https://api.paygent.to/api/v1/gate-events/batch",
headers={"Authorization": f"Bearer {api_key}"},
json=gate_events,
)
curl -X POST https://api.paygent.to/api/v1/gate-events/batch \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d '[{"id": "...", "user_id": "user_123", ...}]'
// Coming soon
Request body: array of GateEventSchema:
| Field | Type | Required |
|---|---|---|
id |
UUID string | yes |
user_id |
string | yes |
paygent_user_id |
string | null | no |
session_id |
string | null | no |
timestamp |
ISO datetime | yes |
status |
soft_gate | hard_gate |
yes |
gate_reason |
string | yes — total_spend, session_spend, or model_limit:<model> |
usage_pct |
float | yes |
current_value |
float | yes |
limit_value |
float | yes |
model |
string | null | no |
blocked |
bool | default false |
message |
string | null | no |
metadata |
dict | default {} |
Response (200):
{ "accepted": 5, "duplicates": 0 }
GET /users/{user_id}/gate-events
Query gate events for a single user. Newest-first, cursor-paginated.
r = httpx.get(
f"https://api.paygent.to/api/v1/users/user_123/gate-events",
headers={"Authorization": f"Bearer {api_key}"},
params={
"status": "hard_gate",
"blocked_only": "true",
"since": "2026-05-01T00:00:00Z",
"limit": 100,
},
)
print(r.json())
curl "https://api.paygent.to/api/v1/users/user_123/gate-events?status=hard_gate&blocked_only=true&limit=100" \
-H "Authorization: Bearer pg_live_..."
// Coming soon
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
status |
string | — | soft_gate or hard_gate. |
gate_reason |
string | — | total_spend, session_spend, model_limit:<model>. |
blocked_only |
bool | false | Only events that actually blocked (blocked=true). |
since |
ISO datetime | — | Inclusive lower bound. |
until |
ISO datetime | — | Exclusive upper bound. |
cursor |
string | — | From previous response's next_cursor. |
limit |
int (1–500) | 100 | Page size. |
Response (200):
{
"user_id": "user_123",
"events": [
{
"id": "abc-1234",
"user_id": "user_123",
"session_id": "sess-9876",
"timestamp": "2026-05-06T14:32:18Z",
"status": "hard_gate",
"gate_reason": "total_spend",
"usage_pct": 1.02,
"current_value": 49.91,
"limit_value": 49.00,
"model": "gpt-4o",
"blocked": true,
"message": "Spend limit reached"
}
],
"next_cursor": "eyJpZCI6ImFiYy0xMjM0In0="
}
next_cursor is non-null when more pages exist. Pass it as cursor on the next call.
Health
GET /health
Liveness check. No authentication required.
r = httpx.get("https://api.paygent.to/api/v1/health")
print(r.json())
curl https://api.paygent.to/api/v1/health
// Coming soon
Response (200):
{
"status": "ok",
"database": "connected"
}
status is degraded if the database isn't reachable.
Error responses
All errors follow FastAPI's default shape:
{
"detail": "User 'user_123' not found."
}
Common status codes:
| Code | When |
|---|---|
400 Bad Request |
Invalid request body or query params |
401 Unauthorized |
Missing or invalid Authorization header |
403 Forbidden |
API key doesn't grant access to this resource |
404 Not Found |
User / plan / product doesn't exist |
409 Conflict |
Resource already exists (e.g. duplicate external_user_id) |
422 Unprocessable Entity |
Body validation failed (Pydantic errors include a list of issues) |
500 Internal Server Error |
Backend bug or transient failure |
Next steps
- SDK Reference — the Python SDK calls these endpoints for you
- Configure your first plan — designing the plan bodies
- Assign users to plans — the user/subscription flow in practice