Management API
Nearly everything the dashboard can do, over JSON. Authenticate with a personal API key (trz_pat_…) and manage organizations, sites, submissions, and users with exactly the permissions your account has in the UI. The API lives under https://gettrazo.app/api/v1 and its contract is English-only.
Authentication & API keys
Any user can create personal API keys from your account page under API keys. Creating or revoking a key sits behind a password confirmation, and each key gets a name so you can tell them apart. You can hold up to 10 active keys; revoke one at any time and it stops working immediately.
The key (trz_pat_…) is shown once, right after you create it, so copy it then; Trazo stores only a hash. Send it on every request in the Authorization header:
curl https://gettrazo.app/api/v1/me \
-H "Authorization: Bearer trz_pat_your_key_here"
A key acts as you: same role (admin, seller, or owner) and same scope as the dashboard. Requests with a missing, invalid, or revoked key, or a key whose account is suspended, get a generic 401 unauthorized. Keys cannot create or revoke other keys; that only happens on the account page.
Requests & responses
Every response is JSON. A single resource comes wrapped in its name ({ "site": { … } }); collections come as an array plus a pagination object. Site create and key rotation additionally return the plaintext api_key once. Write requests take JSON bodies with the same top-level wrapper.
# Single resource: 200 (201 on create)
{ "site": { "id": 42, "name": "Acme Co", … } }
# Collection: 200
{
"sites": [ { "id": 42, "name": "Acme Co", … } ],
"pagination": { "page": 1, "pages": 3, "count": 120, "limit": 50, "prev": null, "next": 2 }
}
# Destroy: 200
{ "deleted": true, "id": 42 }
Collections paginate with ?page= and ?limit= (default 50, clamped to 1–100). The pagination object carries page, pages, count, limit, and prev/next page numbers (null at the edges).
Errors always use one shape: a code you can branch on, a human-readable message, and, for validation failures only, a details object mapping attributes to messages:
{
"error": {
"code": "validation_failed",
"message": "Validation failed",
"details": { "domain": ["can't be blank"] }
}
}
| Code | HTTP | When |
|---|---|---|
| bad_request | 400 | A required parameter is missing or malformed (for example an unknown label value). |
| unauthorized | 401 | Missing, invalid, or revoked API key, or a suspended account. |
| forbidden | 403 | Your role can't perform this action on this record. |
| not_found | 404 | No such record in your scope. |
| validation_failed | 422 | The record didn't pass validation; details maps each attribute to its messages. |
| rate_limited | 429 | Too many requests. Slow down and retry. |
| internal_error | 500 | Something broke on our side. Retry later. |
Records outside your scope return 404 not_found, indistinguishable from records that do not exist.
Rate limits
Authenticated requests are limited to 120 per minute per API key. Requests without an Authorization header are limited to 60 per minute per IP. Over the limit you get 429 with the standard rate_limited error envelope. The public form-intake endpoint has its own separate limits.
Roles & permissions
The API enforces exactly the dashboard's rules: admins see everything, sellers see their organizations, owners see their sites. There are no API-only permissions, with one convenience: the users index is scoped for every role rather than admin-only.
| Resource | Admin | Seller | Owner |
|---|---|---|---|
| Organizations | Full CRUD | No access | No access |
| Sites | Full CRUD + all actions | Their orgs' sites: create, update (can't move orgs), delete, rotate keys, activate/deactivate, billing | Their sites: view, update name & domain, rotate key, send test |
| Submissions | Read + review, all sites | Read + review, their orgs' sites | Read + review, their sites |
| Users | Full management | List & view their orgs' users; invite links for their owners | Themselves only |
Me
GET /api/v1/me works for any valid key and returns the calling user (id, email, role, status, and their organizations for sellers or sites for owners) plus the API key being used. Handy as a first call to verify a key and discover your scope.
curl https://gettrazo.app/api/v1/me \
-H "Authorization: Bearer $TRAZO_KEY"
# 200
{
"user": {
"id": 7, "email": "you@example.com", "role": "seller", "status": "active",
"organizations": [{ "id": 3, "name": "Acme Agency" }], "sites": []
},
"api_key": { "id": 1, "name": "CI deploys", "token_prefix": "trz_pat_abc12345", … }
}
Organizations
Admin-only, mirroring the dashboard. Writable attributes, wrapped in an organization object: name, active, expires_at, max_sites, monthly_submission_limit, notes. Responses include computed billing state: free_tier, subscribed, submissions_used, submission_pool_limit, sites_count.
| Method | Path | Who | Notes |
|---|---|---|---|
| GET | /api/v1/organizations | Admin | Paginated list. |
| GET | /api/v1/organizations/:id | Admin | One organization. |
| POST | /api/v1/organizations | Admin | 201 with the new organization. |
| PATCH | /api/v1/organizations/:id | Admin | Returns the updated organization. |
| DELETE | /api/v1/organizations/:id | Admin | Deletes the organization and everything under it. |
curl "https://gettrazo.app/api/v1/organizations?page=1&limit=50" \
-H "Authorization: Bearer $TRAZO_KEY"
curl -X POST https://gettrazo.app/api/v1/organizations \
-H "Authorization: Bearer $TRAZO_KEY" \
-H "Content-Type: application/json" \
-d '{ "organization": { "name": "Acme Agency", "max_sites": 3 } }'
Sites
Site payloads are wrapped in a site object: name, domain, notification_email, organization_id. Owners can change only name and domain (other attributes are silently ignored for them), and sellers cannot move a site between organizations. Admins and sellers can attach an owner on create or update with top-level owner_user_id (an existing owner) or owner_email (invites a new one). Site keys (sk_live_…) are hashed at rest; the plaintext appears exactly once, in the create and rotate responses.
| Method | Path | Who | Notes |
|---|---|---|---|
| GET | /api/v1/sites | All roles (scoped) | Optional ?organization_id= filter; ordered by name. |
| GET | /api/v1/sites/:id | All roles (scoped) | One site. |
| POST | /api/v1/sites | Admin, seller | 201; response includes the plaintext api_key once. |
| PATCH | /api/v1/sites/:id | All roles (scoped) | Params sliced by role (see above). |
| DELETE | /api/v1/sites/:id | Admin, seller | Deletes the site; an owner left with no sites is removed too. |
| POST | /api/v1/sites/:id/rotate_key | All roles (scoped) | New key; the old one stops working immediately. Plaintext returned once. |
| PATCH | /api/v1/sites/:id/toggle_active | Admin, seller | Flips active; suspends or reactivates an owner with no other active site. |
| POST | /api/v1/sites/:id/send_test | All roles (scoped) | Creates a test submission and emails the notification address. |
| POST | /api/v1/sites/:id/cancel_billing | Admin, seller | Stops billing; submissions accepted until end of month. 422 if already canceled. |
| POST | /api/v1/sites/:id/uncancel_billing | Admin, seller | Undoes a pending cancellation. 422 if none pending. |
curl -X POST https://gettrazo.app/api/v1/sites \
-H "Authorization: Bearer $TRAZO_KEY" \
-H "Content-Type: application/json" \
-d '{
"site": {
"name": "Acme Co",
"domain": "acme.example",
"notification_email": "leads@acme.example",
"organization_id": 7
},
"owner_email": "owner@acme.example"
}'
# 201: the plaintext key appears once, on create and rotate only
{
"site": { "id": 42, "name": "Acme Co", "api_key_prefix": "sk_live_abc1", … },
"api_key": "sk_live_abc123…"
}
curl -X POST https://gettrazo.app/api/v1/sites/42/rotate_key \
-H "Authorization: Bearer $TRAZO_KEY"
# 200
{ "site": { "id": 42, … }, "api_key": "sk_live_new456…" }
Submissions
Read and review only: submissions are created by the public form-intake endpoint, never through the management API. The index takes the dashboard's filters: site_id, organization_id, from, to, email_sent, free-text q, and tab (all, not_spam, likely_spam, spam), and includes per-tab counts. Labeling a withheld submission not_spam delivers it.
| Method | Path | Who | Notes |
|---|---|---|---|
| GET | /api/v1/submissions | All roles (scoped) | Filters + tab; payload includes counts and pagination. |
| GET | /api/v1/submissions/:id | All roles (scoped) | One submission with its classification verdict. |
| POST | /api/v1/submissions/:id/label | All roles (scoped) | label = "not_spam" or "spam". The authoritative correction; moves tabs. |
| POST | /api/v1/submissions/:id/rate | All roles (scoped) | rating = "up" or "down". Model feedback only. |
| POST | /api/v1/submissions/:id/resend_email | All roles (scoped) | Re-enqueues the notification email. |
| POST | /api/v1/submissions/retry_failed_emails | All roles (scoped) | Re-enqueues failed emails in your scope; returns { "retried": n }. |
curl "https://gettrazo.app/api/v1/submissions?site_id=42&tab=not_spam&from=2026-08-01&q=quote" \
-H "Authorization: Bearer $TRAZO_KEY"
# 200
{
"submissions": [ { "id": 1234, "name": "Jane Doe", "email": "jane@example.com", … } ],
"counts": { "all": 128, "not_spam": 110, "likely_spam": 6, "spam": 12 },
"pagination": { "page": 1, "pages": 3, "count": 110, "limit": 50, "prev": null, "next": 2 }
}
curl -X POST https://gettrazo.app/api/v1/submissions/1234/label \
-H "Authorization: Bearer $TRAZO_KEY" \
-H "Content-Type: application/json" \
-d '{ "label": "not_spam" }'
Users
The index is scoped for every role: admins list everyone, sellers list their organizations' users, owners see only themselves. Creating a user (admin only) sends an invitation email; user takes email and role, plus top-level organization_ids / site_ids to assign memberships. The admin role can never be assigned through the API.
| Method | Path | Who | Notes |
|---|---|---|---|
| GET | /api/v1/users | All roles (scoped) | Scoped list for every role. |
| GET | /api/v1/users/:id | Admin, self, managing seller | One user with organizations and sites. |
| POST | /api/v1/users | Admin | 201; sends the invitation email. admin role not assignable. |
| PATCH | /api/v1/users/:id | Admin | email, role (never admin) + membership reassignment. |
| DELETE | /api/v1/users/:id | Admin | Deletes the user. |
| POST | /api/v1/users/:id/resend_invitation | Admin, managing seller | Re-sends the invite email with a fresh token. |
| POST | /api/v1/users/:id/invite_link | Admin, managing seller | Returns { "invite_url": … }. 422 if already accepted. |
| POST | /api/v1/users/:id/reset_password | Admin, managing seller | Emails a password reset link. |
| POST | /api/v1/users/:id/reset_2fa | Admin, managing seller | Emails a two-factor reset link. |
| POST | /api/v1/users/:id/suspend | Admin | Blocks sign-in and API use immediately. |
| POST | /api/v1/users/:id/reactivate | Admin | Lifts a suspension. |
curl -X POST https://gettrazo.app/api/v1/users \
-H "Authorization: Bearer $TRAZO_KEY" \
-H "Content-Type: application/json" \
-d '{
"user": { "email": "seller@example.com", "role": "seller" },
"organization_ids": [7]
}'
curl -X POST https://gettrazo.app/api/v1/users/9/invite_link \
-H "Authorization: Bearer $TRAZO_KEY"
# 200
{ "invite_url": "https://gettrazo.app/login/invite/…" }
Deliberately not in the API
A few things stay in the dashboard on purpose: creating or revoking personal API keys (a stolen key must not be able to mint more keys), creating admin users (CLI only), creating or editing submissions (the intake endpoint is the only way in), billing checkout and portal, and white-label branding.