openapi: '3.0.3' info: title: IRDB — IP Reputation Database version: '1.0.0' description: | Self-hosted IP reputation service: ingest abuse reports, distribute tailored block lists. ## Versioning Single major version `v1`. Changes within `v1` are additive only — new endpoints, new optional fields, new optional query params. Breaking changes ship as `v2`. ## Endpoint groups - **Public**: machine clients (reporters, consumers). - **Admin**: UI BFF + admin-kind tokens. RBAC enforced server-side. - **Auth**: UI BFF only — bridges browser auth to user records. Marked `x-internal: true`. - **Internal jobs**: not in this spec. Scheduler-only, network-restricted. ## Authentication Bearer token in the `Authorization` header. Four token kinds: `reporter`, `consumer`, `admin`, `service`. See `doc/auth-flows.md`. ## Errors Uniform envelope: `{"error":"","details":{...}}`. Validation errors include `details`. Authentication failures return `401` `unauthorized`. Authorization failures return `403`. ## Rate limiting Public endpoints: 60 req/s/token (configurable). On exhaustion: `429` with `Retry-After: 1`. license: name: TBD servers: - url: http://localhost:8081 description: Default compose deployment - url: https://reputation-api.example.com description: Production (replace hostname) tags: - name: Public description: 'Machine clients: reporters and blocklist consumers.' - name: Admin description: UI BFF + admin-kind tokens. - name: Auth description: UI BFF only. Service token required, no impersonation. security: - BearerAuth: [] paths: '/api/v1/report': post: tags: - Public summary: Submit an abuse report description: | Token kind: `reporter`. Rate limit: 60 req/s per token (configurable). Returns `202 Accepted` on success. security: - BearerAuth: [] requestBody: required: true content: 'application/json': schema: '$ref': '#/components/schemas/ReportRequest' responses: '202': description: Report accepted content: 'application/json': schema: '$ref': '#/components/schemas/ReportResponse' '400': description: Validation failed content: 'application/json': schema: '$ref': '#/components/schemas/Error' '401': description: Bad / wrong-kind token content: 'application/json': schema: '$ref': '#/components/schemas/Error' '429': description: Rate limited headers: Retry-After: schema: type: integer description: Seconds to wait before retrying. content: 'application/json': schema: '$ref': '#/components/schemas/Error' '/api/v1/blocklist': get: tags: - Public summary: Pull a tailored blocklist description: | Token kind: `consumer`. The consumer's bound policy decides which IPs/CIDRs land in the output. Cached internally for 30 s per consumer. Honour `If-None-Match` to skip retransfer. `?format=json` returns structured rows; default is `text/plain`, one entry per line. security: - BearerAuth: [] parameters: - name: format in: query schema: type: string enum: - text - json default: text - name: If-None-Match in: header schema: type: string responses: '200': description: Current blocklist headers: ETag: schema: type: string X-Blocklist-Generated-At: schema: type: string format: date-time X-Blocklist-Entries: schema: type: integer X-Blocklist-Policy: schema: type: string content: 'text/plain': schema: type: string example: | 203.0.113.42 198.51.100.0/24 'application/json': schema: '$ref': '#/components/schemas/BlocklistJson' '304': description: Not modified — body matches `If-None-Match` '401': description: Bad / wrong-kind token '/api/v1/admin/me': get: tags: - Admin summary: Current acting identity security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: Identity info content: 'application/json': schema: '$ref': '#/components/schemas/User' '/api/v1/admin/ips': get: tags: - Admin summary: Search IPs security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: q in: query schema: type: string - name: category in: query schema: type: string - name: min_score in: query schema: type: number format: float - name: max_score in: query schema: type: number format: float - name: country in: query schema: type: string - name: asn in: query schema: type: integer - name: status in: query schema: type: string enum: - scored - manual - allowlisted - clean - '$ref': '#/components/parameters/Page' - '$ref': '#/components/parameters/PageSize' responses: '200': description: Page of IPs content: 'application/json': schema: type: object properties: page: type: integer minimum: 1 example: 1 page_size: type: integer minimum: 1 maximum: 200 example: 50 total: type: integer minimum: 0 example: 1284 items: type: array items: type: object '/api/v1/admin/ips/countries': get: tags: - Admin summary: Country code dropdown source security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: '`[{code, count}]`' content: 'application/json': schema: type: object properties: items: type: array items: type: object properties: code: type: string count: type: integer '/api/v1/admin/ips/{ip}': get: tags: - Admin summary: IP detail security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: ip in: path required: true schema: type: string responses: '200': description: IP detail content: 'application/json': schema: '$ref': '#/components/schemas/IpDetail' '404': description: Invalid IP / not found '/api/v1/admin/stats/dashboard': get: tags: - Admin summary: Dashboard stats (30s cached) security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: Dashboard payload content: 'application/json': schema: type: object '/api/v1/admin/manual-blocks': get: tags: - Admin summary: List manual blocks security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: list post: tags: - Admin summary: Create manual block description: Operator+ role required. `kind=ip` requires `ip`; `kind=subnet` requires `cidr`. security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: type: object properties: kind: type: string enum: - ip - subnet ip: type: string cidr: type: string reason: type: string expires_at: type: string format: date-time responses: '201': description: Created content: 'application/json': schema: '$ref': '#/components/schemas/ManualBlock' '/api/v1/admin/manual-blocks/{id}': get: tags: - Admin summary: Show manual block security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '200': description: manual block content: 'application/json': schema: '$ref': '#/components/schemas/ManualBlock' delete: tags: - Admin summary: Delete manual block security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '204': description: Deleted '/api/v1/admin/allowlist': get: tags: - Admin summary: List allowlist security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: list post: tags: - Admin summary: Create allowlist entry security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: type: object properties: kind: type: string enum: - ip - subnet ip: type: string cidr: type: string reason: type: string responses: '201': description: Created content: 'application/json': schema: '$ref': '#/components/schemas/AllowlistEntry' '/api/v1/admin/allowlist/{id}': get: tags: - Admin summary: Show allowlist entry security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '200': description: allowlist entry content: 'application/json': schema: '$ref': '#/components/schemas/AllowlistEntry' delete: tags: - Admin summary: Delete allowlist entry security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '204': description: Deleted '/api/v1/admin/reporters': get: tags: - Admin summary: List reporters security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: list post: tags: - Admin summary: Create reporter security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: '$ref': '#/components/schemas/Reporter' responses: '201': description: Created content: 'application/json': schema: '$ref': '#/components/schemas/Reporter' '/api/v1/admin/reporters/{id}': get: tags: - Admin summary: Show reporter security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '200': description: reporter content: 'application/json': schema: '$ref': '#/components/schemas/Reporter' patch: tags: - Admin summary: Update reporter security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer requestBody: content: 'application/json': schema: '$ref': '#/components/schemas/Reporter' responses: '200': description: Updated content: 'application/json': schema: '$ref': '#/components/schemas/Reporter' delete: tags: - Admin summary: Soft-delete reporter security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '204': description: Deleted '409': description: Has reports — flagged inactive instead. '/api/v1/admin/consumers': get: tags: - Admin summary: List consumers security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: list post: tags: - Admin summary: Create consumer security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: '$ref': '#/components/schemas/Consumer' responses: '201': description: Created content: 'application/json': schema: '$ref': '#/components/schemas/Consumer' '/api/v1/admin/consumers/{id}': get: tags: - Admin summary: Show consumer security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '200': description: consumer content: 'application/json': schema: '$ref': '#/components/schemas/Consumer' patch: tags: - Admin summary: Update consumer security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer requestBody: content: 'application/json': schema: '$ref': '#/components/schemas/Consumer' responses: '200': description: Updated delete: tags: - Admin summary: Soft-delete consumer security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '204': description: Deleted '/api/v1/admin/tokens': get: tags: - Admin summary: List tokens description: Service tokens are filtered out unconditionally. security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: list post: tags: - Admin summary: Create token description: Returns the raw token ONCE in `raw_token`. Service tokens are not creatable here. security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: type: object properties: kind: type: string enum: - reporter - consumer - admin reporter_id: type: integer consumer_id: type: integer role: type: string enum: - viewer - operator - admin expires_at: type: string format: date-time responses: '201': description: Created content: 'application/json': schema: '$ref': '#/components/schemas/TokenCreated' '/api/v1/admin/tokens/{id}': delete: tags: - Admin summary: Revoke token security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '204': description: Revoked '/api/v1/admin/tokens/{id}/purge': delete: tags: - Admin summary: Permanently delete a revoked token description: | Hard-deletes the row. Requires the token to be already revoked (`revoked_at` set); active tokens return 409 to keep "revoke first, then prune" the only path that removes data. Service tokens cannot be deleted. security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '204': description: Deleted '403': description: Service token; not deletable via API '404': description: Not found '409': description: Token still active; revoke it first '/api/v1/admin/categories': get: tags: - Admin summary: List categories security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: list post: tags: - Admin summary: Create category security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: '$ref': '#/components/schemas/Category' responses: '201': description: Created content: 'application/json': schema: '$ref': '#/components/schemas/Category' '/api/v1/admin/categories/{id}': get: tags: - Admin summary: Show category security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '200': description: category content: 'application/json': schema: '$ref': '#/components/schemas/Category' patch: tags: - Admin summary: Update category security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer requestBody: content: 'application/json': schema: '$ref': '#/components/schemas/Category' responses: '200': description: Updated delete: tags: - Admin summary: Hard-delete category (refused if in use) security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '204': description: Deleted '409': description: Category in use; soft-delete via PATCH `is_active=false`. '/api/v1/admin/policies': get: tags: - Admin summary: List policies security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: list post: tags: - Admin summary: Create policy security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: '$ref': '#/components/schemas/Policy' responses: '201': description: Created content: 'application/json': schema: '$ref': '#/components/schemas/Policy' '/api/v1/admin/policies/{id}': get: tags: - Admin summary: Show policy security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '200': description: policy content: 'application/json': schema: '$ref': '#/components/schemas/Policy' patch: tags: - Admin summary: Update policy (replaces thresholds wholesale when present) security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer requestBody: content: 'application/json': schema: '$ref': '#/components/schemas/Policy' responses: '200': description: Updated delete: tags: - Admin summary: Delete policy (refused if used by consumers) security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '204': description: Deleted '409': description: Policy in use '/api/v1/admin/policies/{id}/preview': get: tags: - Admin summary: Preview policy (count + sample) security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: id in: path required: true schema: type: integer responses: '200': description: Preview '/api/v1/admin/audit-log': get: tags: - Admin summary: Filtered audit log security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: actor_kind in: query schema: type: string enum: - user - admin-token - reporter - consumer - system - name: actor_id in: query schema: type: integer - name: action in: query schema: type: string - name: entity_type in: query schema: type: string - name: entity_id in: query schema: type: string - name: from in: query schema: type: string format: date-time - name: to in: query schema: type: string format: date-time - '$ref': '#/components/parameters/Page' - '$ref': '#/components/parameters/PageSize' responses: '200': description: Audit page content: 'application/json': schema: type: object properties: page: type: integer minimum: 1 example: 1 page_size: type: integer minimum: 1 maximum: 200 example: 50 total: type: integer minimum: 0 example: 1284 items: type: array items: '$ref': '#/components/schemas/AuditEntry' '/api/v1/admin/jobs/status': get: tags: - Admin summary: Jobs status (Viewer) security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: Jobs status content: 'application/json': schema: type: object properties: now: type: string format: date-time jobs: type: object additionalProperties: '$ref': '#/components/schemas/JobStatus' '/api/v1/admin/jobs/trigger/{name}': post: tags: - Admin summary: Manually trigger a job (Admin) description: 'Whitelisted params: `full`, `max_rows`, `reenrich`. Other body fields are dropped.' security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' - name: name in: path required: true schema: type: string requestBody: content: 'application/json': schema: type: object properties: full: type: boolean max_rows: type: integer reenrich: type: boolean responses: '200': description: Job ran content: 'application/json': schema: '$ref': '#/components/schemas/JobOutcome' '404': description: Unknown job '409': description: Lock held; status=`skipped_locked` '412': description: refresh-geoip without credential '/api/v1/admin/config': get: tags: - Admin summary: Effective config (secrets masked) description: Admin only. security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: Config sections content: 'application/json': schema: type: object properties: sections: type: object additionalProperties: type: object '/api/v1/admin/app-settings': get: tags: - Admin summary: Runtime feature flags (Admin) description: | Returns the runtime-mutable feature flags. Currently exposes the audit-emission toggles for high-volume public endpoints (`audit_report_received_enabled`, `audit_blocklist_request_enabled`). security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: Current toggle state content: 'application/json': schema: type: object additionalProperties: type: boolean patch: tags: - Admin summary: Update runtime feature flags (Admin) description: | Updates one or more runtime feature flags. Body keys not listed are left untouched. Returns the post-update snapshot. security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: type: object properties: audit_report_received_enabled: type: boolean audit_blocklist_request_enabled: type: boolean responses: '200': description: Updated snapshot content: 'application/json': schema: type: object additionalProperties: type: boolean '400': description: Validation error '/api/v1/admin/maintenance/purge': post: tags: - Admin summary: Wipe operational data (Admin) description: | Deletes reports, scores, enrichment, manual blocks, allowlist, audit log, job history, reporters, consumers, policies, and non-service tokens. Preserves users, OIDC role mappings, abuse categories, and the `service`-kind token. Requires `confirm: "PURGE"` in the body — any other value returns 400. security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' requestBody: required: true content: 'application/json': schema: type: object required: - confirm properties: confirm: type: string enum: - PURGE responses: '200': description: Purge succeeded content: 'application/json': schema: type: object properties: status: type: string example: purged deleted: type: object additionalProperties: type: integer '400': description: Missing or wrong `confirm` value '/api/v1/admin/maintenance/seed-demo': post: tags: - Admin summary: Load demo dataset (Admin) description: 'Populates reporters, consumers, IPs, reports, manual blocks, allowlist, and synthetic GeoIP for demos and screenshots. Triggers a full score recompute on completion. Idempotent: returns 409 if demo data is already present.' security: - BearerAuth: [] parameters: - '$ref': '#/components/parameters/ActingUserId' responses: '200': description: Demo data inserted content: 'application/json': schema: type: object properties: status: type: string example: seeded summary: type: object additionalProperties: type: integer recompute: '$ref': '#/components/schemas/JobOutcome' '409': description: Demo data already present '412': description: No categories configured '/api/v1/auth/users/upsert-oidc': post: tags: - Auth summary: Upsert an OIDC-authenticated user description: | **UI BFF only.** Service-token-required, no impersonation header. Resolves the user record + role (via `oidc_role_mappings`) for a freshly-validated ID token. x-internal: true security: - BearerAuth: [] requestBody: required: true content: 'application/json': schema: type: object properties: subject: type: string email: type: string display_name: type: string groups: type: array items: type: string responses: '200': description: User record content: 'application/json': schema: '$ref': '#/components/schemas/User' '/api/v1/auth/users/upsert-local': post: tags: - Auth summary: Upsert the local-admin user description: '**UI BFF only.** Called after the UI validates the local-admin password.' x-internal: true security: - BearerAuth: [] requestBody: required: true content: 'application/json': schema: type: object properties: username: type: string responses: '200': description: User record content: 'application/json': schema: '$ref': '#/components/schemas/User' '/api/v1/auth/users/{id}': get: tags: - Auth summary: Refetch a user record description: '**UI BFF only.** Used to refresh role / display_name during a session.' x-internal: true security: - BearerAuth: [] parameters: - name: id in: path required: true schema: type: integer responses: '200': description: User record content: 'application/json': schema: '$ref': '#/components/schemas/User' components: securitySchemes: BearerAuth: type: http scheme: bearer description: | Token in the form `irdb__<32 base32 chars>`. See `doc/auth-flows.md` for the four token kinds. parameters: Page: name: page in: query schema: type: integer minimum: 1 default: 1 PageSize: name: page_size in: query schema: type: integer minimum: 1 maximum: 200 default: 50 ActingUserId: name: X-Acting-User-Id in: header description: | Required when authenticating with a `service` token. The api applies RBAC for the named user. Ignored on other token kinds. schema: type: integer schemas: Error: type: object required: - error properties: error: type: string example: unauthorized details: type: object description: Field-level errors for `validation_failed`. additionalProperties: type: string ReportRequest: type: object required: - ip - category properties: ip: type: string example: '203.0.113.42' category: type: string example: brute_force metadata: type: object description: Free-form per-report data, max 4 KB after json_encode. additionalProperties: true ReportResponse: type: object properties: report_id: type: integer example: 12345 ip: type: string example: '203.0.113.42' received_at: type: string format: date-time BlocklistEntry: type: object properties: ip_or_cidr: type: string example: '203.0.113.42' categories: type: array items: type: string score: type: number format: float example: 1.42 reason: type: string enum: - scored - manual example: scored BlocklistJson: type: object properties: count: type: integer example: 42 generated_at: type: string format: date-time policy: type: string example: moderate entries: type: array items: '$ref': '#/components/schemas/BlocklistEntry' Token: type: object properties: id: type: integer kind: type: string enum: - reporter - consumer - admin prefix: type: string example: irdb_adm reporter_id: type: integer nullable: true consumer_id: type: integer nullable: true role: type: string nullable: true enum: - viewer - operator - admin - null expires_at: type: string format: date-time nullable: true revoked_at: type: string format: date-time nullable: true last_used_at: type: string format: date-time nullable: true TokenCreated: allOf: - '$ref': '#/components/schemas/Token' - type: object properties: raw_token: type: string description: Returned ONCE on creation — copy it now, never displayed again. example: irdb_adm_AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD Reporter: type: object properties: id: type: integer name: type: string example: web-prod-01 description: type: string nullable: true trust_weight: type: number format: float minimum: 0 maximum: 2 is_active: type: boolean audit_enabled: type: boolean description: When false, suppresses `report.received` audit rows for this reporter even if the global toggle is on. Consumer: type: object properties: id: type: integer name: type: string example: edge-fw-01 description: type: string nullable: true policy_id: type: integer is_active: type: boolean audit_enabled: type: boolean description: When false, suppresses `blocklist.requested` audit rows for this consumer even if the global toggle is on. last_pulled_at: type: string format: date-time nullable: true Category: type: object properties: id: type: integer slug: type: string example: brute_force name: type: string description: type: string nullable: true decay_function: type: string enum: - linear - exponential decay_param: type: number format: float is_active: type: boolean Policy: type: object properties: id: type: integer name: type: string example: moderate description: type: string nullable: true include_manual_blocks: type: boolean thresholds: type: object description: '`{category_slug: threshold}`' additionalProperties: type: number format: float ManualBlock: type: object properties: id: type: integer kind: type: string enum: - ip - subnet ip: type: string nullable: true cidr: type: string nullable: true reason: type: string nullable: true expires_at: type: string format: date-time nullable: true created_at: type: string format: date-time AllowlistEntry: type: object properties: id: type: integer kind: type: string enum: - ip - subnet ip: type: string nullable: true cidr: type: string nullable: true reason: type: string nullable: true created_at: type: string format: date-time IpDetail: type: object properties: ip: type: string is_ipv4: type: boolean scores: type: array items: type: object properties: category: type: string score: type: number format: float last_report_at: type: string format: date-time nullable: true report_count_30d: type: integer enrichment: type: object properties: country_code: type: string nullable: true asn: type: integer nullable: true as_org: type: string nullable: true enriched_at: type: string format: date-time nullable: true status: type: string enum: - scored - manually_blocked - allowlisted - clean manual_block: type: object nullable: true allowlist: type: object nullable: true history: type: array items: type: object has_more: type: boolean AuditEntry: type: object properties: id: type: integer occurred_at: type: string format: date-time actor_kind: type: string enum: - user - admin-token - reporter - consumer - system actor_id: type: string nullable: true action: type: string example: manual_block.created entity_type: type: string nullable: true entity_id: type: string nullable: true entity_label: type: string nullable: true description: Human-readable identifier for the target (name, slug, IP, CIDR, prefix). Frozen at write time. details: type: object nullable: true additionalProperties: true description: 'For update events, contains a `changes` map of `{field: {from, to}}` for every modified field.' source_ip: type: string nullable: true JobStatus: type: object properties: name: type: string default_interval_seconds: type: integer max_runtime_seconds: type: integer overdue: type: boolean last_run: type: object nullable: true properties: id: type: integer status: type: string enum: - success - failure - skipped_locked - running items_processed: type: integer triggered_by: type: string enum: - schedule - manual - api started_at: type: string format: date-time nullable: true finished_at: type: string format: date-time nullable: true error_message: type: string nullable: true JobOutcome: type: object properties: job: type: string status: type: string enum: - success - failure - skipped_locked - running items_processed: type: integer duration_ms: type: integer run_id: type: integer nullable: true error: type: string nullable: true User: type: object properties: id: type: integer email: type: string nullable: true display_name: type: string role: type: string enum: - viewer - operator - admin source: type: string enum: - oidc - local - admin-token is_local: type: boolean Pagination: type: object properties: page: type: integer minimum: 1 example: 1 page_size: type: integer minimum: 1 maximum: 200 example: 50 total: type: integer minimum: 0 example: 1284