Docs / API reference
API reference
The public HTTP API: accounts and tokens, usage, public keys, bundle and license downloads, ingest, and the Intel files. JSON in, JSON out, base URL https://protet.io unless stated.
Status. Account, token, usage, public-key and Intel endpoints are live. The ingest and findings endpoints on ingress.protet.io accept requests, but the path from ingested events to a verdict is being fixed (see the Action page). Do not build on a verdict from them yet.
Conventions
- Request bodies are JSON with
Content-Type: application/jsonand at most 64 KiB. - Session auth.
POST /auth/loginreturns{"access_token": "...", "token_type": "bearer"}and also sets anHttpOnlycs_sessioncookie. Send the token asAuthorization: Bearer <access_token>. It lives for 1 hour. Changing your password invalidates other sessions. - Machine auth. The ingest refresh token (from
POST /portal/ingest-token) goes inAuthorization: Bearer ...too. It is accepted by/v1/ingest/refreshand/v1/usage. The pull token goes inx-license-keyand is accepted by/v1/bundle. - Errors are
{"detail": "..."}(validation errors: a list underdetail).401not authenticated or bad credentials,403not allowed for your plan or suspended,404not found,422invalid body,429rate limited,503temporarily unavailable. - Rate limits (per hour): 5 sign-ups per source address; 5 verification re-sends and 5 password-reset requests per source address; 10 logins per email and source address. Past the limit you get
429; wait and retry.
Account
| Request | Body | Response |
|---|---|---|
POST /auth/signup | email, password (8 to 256 characters), name (optional) | {"status": "pending_verification", "customer_id": "..."}. Same response whether or not the address exists. |
POST /auth/verify | token from the email link | {"status": "active", "license": null, "pull_token": "..."} |
POST /auth/resend-verification | email | {"status": "ok"}, always. |
POST /auth/login | email, password | {"access_token": "...", "token_type": "bearer"} |
POST /auth/logout | {"ok": true}; clears the cookie. | |
GET /auth/me | {"user_id": "...", "role": "...", "customer_id": "..."} | |
POST /auth/change-password | current_password, new_password | {"status": "ok"} |
POST /auth/forgot-password | email | {"status": "ok"}, always. A reset link goes to the address if it is registered. |
POST /auth/reset-password | token, new_password | {"status": "ok"} |
Tokens and portal
These need session auth. A cloud-plan account mints an ingest token; an on-prem account mints pull tokens.
| Request | Response |
|---|---|
POST /portal/ingest-token | {"token": "..."}: a refresh token, valid 90 days, shown once. Cloud plans only. |
POST /portal/ingest-token/revoke-all | 204. Every refresh token minted before this call stops working at its next refresh; mint a new one afterwards. Cloud plans. |
GET /portal/entitlements | Your plan: tier with key, name, description, capabilities, deployment_mode and gateway_url. |
GET /portal/license-summary | {"has_license": false} on cloud plans; on-prem: issue and expiry dates, days_remaining, features, limits. |
GET /portal/pull-tokens | List of {"id", "label", "revoked_at"}. For on-prem bundle pulls. |
POST /portal/pull-tokens | Body {"label": "ci"}. Returns the token once, in token. For on-prem bundle pulls. |
POST /portal/pull-tokens/{id}/revoke | The token with revoked_at set. It stops working immediately. |
curl -s -X POST https://protet.io/portal/ingest-token -H "Authorization: Bearer $TOKEN"
Usage
GET /v1/usage with a session token or your ingest refresh token. Cloud plans only (others get 403). If the metering service cannot be reached you get 503.
curl -H "Authorization: Bearer $PROTET_TOKEN" https://protet.io/v1/usage
{
"period": "2026-10",
"resets_at": "2026-11-01T00:00:00Z",
"sessions": { "used": 12, "limit": 500 }
}
Ingest and findings (preview)
| Request | Auth | Description |
|---|---|---|
POST https://protet.io/v1/ingest/refresh | refresh token | Returns {"token": "<access token>", "expires_in": 3600}. Checks your account is active every time; suspended accounts get no new access token. |
POST https://ingress.protet.io/v1/ingest | access token | Body {"events": [...]}. Returns {"received", "accepted", "rejected_quota"}. Event shape and the 422 rule are on Integrations. |
GET https://ingress.protet.io/v1/findings/stream?session=<id> | access token | Server-sent events carrying findings for a session. Send the access token, not the refresh token. At most 10 concurrent streams per account; more get 429. Last-Event-ID resumes. Rejects missing tokens with 401. |
The old https://protet.io/v1/findings/stream no longer exists and returns 404.
Keys, license and bundle
| Request | Auth | Description |
|---|---|---|
GET /v1/public-keys | none | Public Ed25519 keys, base64. bundle and license at the top level, and keys[] with purpose, kid and public_key. |
GET /v1/license | session | Your signed license.json (on-prem). 404 if you have none. |
GET /v1/bundle | pull token in x-license-key | 200 with bundle.tar.gz; 202 while a new bundle is built (retry); 401 bad or revoked token; 403 no plan assigned. |
curl -s https://protet.io/v1/public-keys | python3 -m json.tool
Intel files
Public, unauthenticated, cached for 5 minutes. Full field reference and examples: Intel feed guide.
GET /intel/v1/all/findings.ndjson GET /intel/v1/npm/findings.ndjson GET /intel/v1/all/feed.atom GET /intel/v1/npm/feed.atom GET /intel/v1/npm/osv/PROTET-NPM-YYYY-N.json GET /intel/v1/stats.json