Documentation

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:

terminalbash
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.

responsesjson
# 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:

errorjson
{
  "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.

GET /api/v1/mebash
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.
terminalbash
curl "https://gettrazo.app/api/v1/organizations?page=1&limit=50" \
  -H "Authorization: Bearer $TRAZO_KEY"
terminalbash
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.
terminalbash
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…"
}
terminalbash
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 }.
terminalbash
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 }
}
terminalbash
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.
terminalbash
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]
  }'
terminalbash
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.