Skip to content

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"
  }'
const res = await fetch("https://api.paygent.to/api/v1/products", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "MyAgent",
    contact_name: "Jane Developer",
    contact_email: "jane@example.com",
  }),
});
const data = await res.json(); // includes `api_key` — store it now, shown once

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_..."
const res = await fetch("https://api.paygent.to/api/v1/products", {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const data = await res.json();

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
const res = await fetch("https://api.paygent.to/api/v1/config/plans", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    soft_gate_at: 0.8,
    hard_gate_at: 1.0,
    plans: [
      {
        name: "pro",
        max_spend_per_period: 49.0,
        max_spend_per_session: 5.0,
        model_limits: { "gpt-4o": { max_tokens_per_period: 50000 } },
        cost_rates: { "gpt-4o": { input: 0.0025, output: 0.01 } },
      },
    ],
  }),
});

Request 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
    }
  ]
}

Top-level fields:

Field Type Default Description
soft_gate_at float (0–1) 0.80 Percentage of limit at which the soft gate fires. Applied to all plans unless overridden per plan.
hard_gate_at float (0–1) 1.00 Percentage of limit at which the hard gate fires and blocks the call.
plans array required List of plan objects (see below).

Plan fields (each item in plans):

Field Type Required Default Description
name string yes Unique plan name (e.g. free, pro). Used as the identifier when assigning users.
max_spend_per_period float no inf Maximum dollars a user can spend in a billing period. null or omitted = unlimited.
max_spend_per_session float no inf Maximum dollars per session window.
session_timeout_minutes float no 30.0 Duration of a session window in minutes. After expiry, session_cost resets and session_id rotates.
soft_gate_at float no top-level value Override the top-level soft gate threshold for this plan.
hard_gate_at float no top-level value Override the top-level hard gate threshold for this plan.
model_limits object no {} Per-model token limits. Keys are model names, values are {"max_tokens_per_period": int}. Models not listed are unlimited.
cost_rates object no {} Per-model cost rates. Keys are model names, values are {"input": float, "output": float} — cost per 1K tokens.
default_cost_rate object | null no null Fallback cost rate for models not in cost_rates. Format: {"input": float, "output": float}.
tool_costs object no {} Per-tool flat costs. Keys are tool names, values are cost per call.
default_tool_cost float no 0.02 Fallback cost for tools not in tool_costs.
pre_call_estimate bool no false If true, the SDK estimates cost before the call and reserves it against the limit (concurrency-safe).
pre_call_buffer_tokens int no 4096 Token buffer used for pre-call estimation when pre_call_estimate is true.

See Configure your first plan for design guidance and common patterns.

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_..."
const res = await fetch("https://api.paygent.to/api/v1/config/plans", {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const data = await res.json();

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}'
const res = await fetch(
  `https://api.paygent.to/api/v1/config/plans/${planId}`,
  {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      max_spend_per_period: 79.0,
      model_limits: { "gpt-4o": { max_tokens_per_period: 80000 } },
    }),
  },
);

Request body: any subset of the plan fields listed under POST /config/plans. Only the fields you include are updated; everything else stays unchanged.

Response (200): the full updated plan object (same shape as items in the POST /config/plans response).

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"}'
const res = await fetch("https://api.paygent.to/api/v1/users", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ external_user_id: "user_123", name: "Alice" }),
});

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"
  }'
const now = new Date();
const periodEnd = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
const res = await fetch(
  "https://api.paygent.to/api/v1/users/user_123/subscription",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      plan_id: "pro-plan-uuid",
      stripe_subscription_id: "sub_1OabCDeFghIJ",
      period_start: now.toISOString(),
      period_end: periodEnd.toISOString(),
    }),
  },
);

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 window
  • 404 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_..."
const res = await fetch(
  "https://api.paygent.to/api/v1/users/user_123/session",
  { headers: { Authorization: `Bearer ${apiKey}` } },
);
const data = await res.json();

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_..."
const params = new URLSearchParams({ period: "current_period", breakdown: "model" });
const res = await fetch(
  `https://api.paygent.to/api/v1/users/user_123/usage?${params}`,
  { headers: { Authorization: `Bearer ${apiKey}` } },
);
const data = await res.json();

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", ...}]'
const events = [
  {
    id: "abc-1234",
    user_id: "user_123",
    session_id: "sess-9876",
    timestamp: new Date().toISOString(),
    model: "gpt-4o-mini",
    input_tokens: 100,
    output_tokens: 50,
    total_tokens: 150,
    cost_tokens: 0.000045,
    cost_total: 0.000045,
  },
];
const res = await fetch("https://api.paygent.to/api/v1/events/batch", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(events),
});
const data = await res.json(); // { accepted, duplicates }

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", ...}]'
const gateEvents = [
  {
    id: "abc-1234",
    user_id: "user_123",
    timestamp: new Date().toISOString(),
    status: "hard_gate",
    gate_reason: "total_spend",
    usage_pct: 1.02,
    current_value: 49.91,
    limit_value: 49.0,
    blocked: true,
    message: "Spend limit reached",
  },
];
await fetch("https://api.paygent.to/api/v1/gate-events/batch", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(gateEvents),
});

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_..."
const params = new URLSearchParams({
  status: "hard_gate",
  blocked_only: "true",
  limit: "100",
});
const res = await fetch(
  `https://api.paygent.to/api/v1/users/user_123/gate-events?${params}`,
  { headers: { Authorization: `Bearer ${apiKey}` } },
);
const data = await res.json();

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.


Webhooks

POST /webhooks/endpoints

Create a webhook endpoint. Returns the signing secret — shown only once.

curl -X POST https://api.paygent.to/api/v1/webhooks/endpoints \
  -H "Authorization: Bearer pg_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/paygent",
    "subscribed_events": ["gate_event.hard_gate", "usage.threshold.80"],
    "description": "Production alerting"
  }'
r = httpx.post(
    "https://api.paygent.to/api/v1/webhooks/endpoints",
    headers={"Authorization": f"Bearer {api_key}"},
    json={
        "url": "https://your-app.com/webhooks/paygent",
        "subscribed_events": ["gate_event.hard_gate", "usage.threshold.80"],
        "description": "Production alerting",
    },
)
secret = r.json()["secret"]  # store this — shown only once
const res = await fetch("https://api.paygent.to/api/v1/webhooks/endpoints", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://your-app.com/webhooks/paygent",
    subscribed_events: ["gate_event.hard_gate", "usage.threshold.80"],
    description: "Production alerting",
  }),
});
const data = await res.json(); // includes `secret` — store it now

Request body:

Field Type Required Description
url string (URL) yes Your HTTPS endpoint.
subscribed_events array of strings yes Event types to receive. See Webhooks → Event types.
description string | null no Human-readable label.

Response (201):

{
  "id": "550e8400-...",
  "url": "https://your-app.com/webhooks/paygent",
  "secret": "whsec_a1b2c3d4...",
  "subscribed_events": ["gate_event.hard_gate", "usage.threshold.80"],
  "description": "Production alerting",
  "enabled": true,
  "created_at": "2026-06-22T14:00:00Z"
}

secret is returned only on creation. Store it before discarding the response.


GET /webhooks/endpoints

List all webhook endpoints. Secrets are masked.

curl https://api.paygent.to/api/v1/webhooks/endpoints \
  -H "Authorization: Bearer pg_live_..."
r = httpx.get(
    "https://api.paygent.to/api/v1/webhooks/endpoints",
    headers={"Authorization": f"Bearer {api_key}"},
)

Response (200): array of endpoint objects (same shape as creation response, without secret).


GET /webhooks/endpoints/{id}/secret

Reveal the signing secret for an endpoint.

curl https://api.paygent.to/api/v1/webhooks/endpoints/$ENDPOINT_ID/secret \
  -H "Authorization: Bearer pg_live_..."

Response (200):

{ "secret": "whsec_a1b2c3d4..." }

PATCH /webhooks/endpoints/{id}

Update URL, subscribed events, enabled status, or description.

curl -X PATCH https://api.paygent.to/api/v1/webhooks/endpoints/$ENDPOINT_ID \
  -H "Authorization: Bearer pg_live_..." \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'
httpx.patch(
    f"https://api.paygent.to/api/v1/webhooks/endpoints/{endpoint_id}",
    headers={"Authorization": f"Bearer {api_key}"},
    json={"enabled": False},
)

Request body: any subset of url, subscribed_events, description, enabled.

Response (200): the full updated endpoint object.


DELETE /webhooks/endpoints/{id}

Delete an endpoint and cascade its delivery log.

curl -X DELETE https://api.paygent.to/api/v1/webhooks/endpoints/$ENDPOINT_ID \
  -H "Authorization: Bearer pg_live_..."

Response: 204 No Content.


POST /webhooks/endpoints/{id}/rotate-secret

Generate a new signing secret, invalidating the old one.

curl -X POST https://api.paygent.to/api/v1/webhooks/endpoints/$ENDPOINT_ID/rotate-secret \
  -H "Authorization: Bearer pg_live_..."

Response (200):

{ "secret": "whsec_newSecret..." }

POST /webhooks/endpoints/{id}/test

Send a synthetic test event synchronously. Returns the delivery result.

curl -X POST https://api.paygent.to/api/v1/webhooks/endpoints/$ENDPOINT_ID/test \
  -H "Authorization: Bearer pg_live_..."

Response (200):

{
  "success": true,
  "status_code": 200,
  "response_time_ms": 142
}

GET /webhooks/endpoints/{id}/deliveries

Query the delivery log for an endpoint. Cursor-paginated, newest first.

curl "https://api.paygent.to/api/v1/webhooks/endpoints/$ENDPOINT_ID/deliveries?limit=50" \
  -H "Authorization: Bearer pg_live_..."
r = httpx.get(
    f"https://api.paygent.to/api/v1/webhooks/endpoints/{endpoint_id}/deliveries",
    headers={"Authorization": f"Bearer {api_key}"},
    params={"limit": 50},
)

Query parameters:

Param Default Description
limit 50 Page size (1–200).
cursor From previous response's next_cursor.
status success, failed, pending.

Response (200):

{
  "deliveries": [
    {
      "id": "whdel_...",
      "event_type": "gate_event.hard_gate",
      "status": "success",
      "status_code": 200,
      "attempt": 1,
      "created_at": "2026-06-22T14:30:00Z",
      "completed_at": "2026-06-22T14:30:01Z"
    }
  ],
  "next_cursor": "..."
}

Manual Events

POST /events

Record a single manual usage event. For server-side recording without the SDK.

curl -X POST https://api.paygent.to/api/v1/events \
  -H "Authorization: Bearer pg_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "user_123",
    "cost": 0.05,
    "description": "Web search via Tavily",
    "model": "web-search",
    "tool_calls": ["tavily_search"]
  }'
r = httpx.post(
    "https://api.paygent.to/api/v1/events",
    headers={"Authorization": f"Bearer {api_key}"},
    json={
        "user_id": "user_123",
        "cost": 0.05,
        "description": "Web search via Tavily",
        "model": "web-search",
        "tool_calls": ["tavily_search"],
    },
)
const res = await fetch("https://api.paygent.to/api/v1/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    user_id: "user_123",
    cost: 0.05,
    description: "Web search via Tavily",
    model: "web-search",
    tool_calls: ["tavily_search"],
  }),
});

Request body:

Field Type Required Description
user_id string yes End-user identifier (external_user_id).
cost float | null no Explicit dollar cost. If provided, used as-is.
model string | null no Model name. Used for cost calculation if cost is null.
input_tokens int default 0 Input token count.
output_tokens int default 0 Output token count.
description string | null no Human-readable label.
tool_calls array of strings default [] Tool names invoked.
metadata dict default {} Arbitrary key-value data.
session_id string | null no Override session ID.
idempotency_key string | null no Dedupe key. Prevents double-counting on retries.

Response (201):

{
  "id": "evt_550e8400-...",
  "user_id": "user_123",
  "cost": 0.05,
  "model": "web-search",
  "source": "manual",
  "created_at": "2026-06-22T14:30:00Z"
}

Idempotent on idempotency_key — sending the same key twice returns the existing event.


Analytics

Dashboard-backing endpoints. Also available to developers for building custom dashboards or integrations.

GET /analytics/summary

Total cost, tokens, events, and users for a time window.

curl "https://api.paygent.to/api/v1/analytics/summary?days=30" \
  -H "Authorization: Bearer pg_live_..."

Query parameters:

Param Default Description
days 30 Number of days to look back.

Response (200):

{
  "total_cost": 1247.83,
  "total_tokens": 24500000,
  "total_events": 18234,
  "active_users": 142,
  "period_start": "2026-05-23T00:00:00Z",
  "period_end": "2026-06-22T00:00:00Z",
  "previous_period": {
    "total_cost": 980.12,
    "total_tokens": 19200000,
    "total_events": 14800,
    "active_users": 128
  }
}

GET /analytics/timeseries

Daily cost and token timeseries.

curl "https://api.paygent.to/api/v1/analytics/timeseries?days=30" \
  -H "Authorization: Bearer pg_live_..."

Response (200):

{
  "data": [
    {"date": "2026-06-22", "cost": 42.15, "tokens": 820000, "events": 612},
    {"date": "2026-06-21", "cost": 38.90, "tokens": 760000, "events": 580}
  ]
}

GET /analytics/gate-timeseries

Daily gate event counts (soft and hard).

curl "https://api.paygent.to/api/v1/analytics/gate-timeseries?days=30" \
  -H "Authorization: Bearer pg_live_..."

Response (200):

{
  "data": [
    {"date": "2026-06-22", "soft_gates": 12, "hard_gates": 3},
    {"date": "2026-06-21", "soft_gates": 8, "hard_gates": 1}
  ]
}

GET /analytics/breakdown

Cost and tokens broken down by model.

curl "https://api.paygent.to/api/v1/analytics/breakdown?days=30" \
  -H "Authorization: Bearer pg_live_..."

Response (200):

{
  "models": [
    {"model": "gpt-4o", "cost": 820.50, "tokens": 8200000, "pct": 65.7},
    {"model": "gpt-4o-mini", "cost": 127.33, "tokens": 12800000, "pct": 10.2},
    {"model": "claude-sonnet", "cost": 300.00, "tokens": 3500000, "pct": 24.1}
  ]
}

GET /analytics/top-users

Top N users by cost.

curl "https://api.paygent.to/api/v1/analytics/top-users?days=30&limit=10" \
  -H "Authorization: Bearer pg_live_..."

Query parameters:

Param Default Description
days 30 Lookback window.
limit 10 Number of users to return (1–100).

Response (200):

{
  "users": [
    {"user_id": "user_456", "cost": 89.23, "tokens": 1240000, "events": 342},
    {"user_id": "user_123", "cost": 67.80, "tokens": 980000, "events": 289}
  ]
}

GET /analytics/plan-subscribers

User count per plan.

curl "https://api.paygent.to/api/v1/analytics/plan-subscribers" \
  -H "Authorization: Bearer pg_live_..."

Response (200):

{
  "plans": [
    {"plan_name": "free", "subscriber_count": 89},
    {"plan_name": "pro", "subscriber_count": 42},
    {"plan_name": "enterprise", "subscriber_count": 11}
  ]
}

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
const res = await fetch("https://api.paygent.to/api/v1/health");
const data = await res.json(); // { status: "ok" }

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