REQ API-029, DOC-004. Generated from contracts/openapi.yaml, which is the source of truth — this file is a rendering of it, not a second description.
Everything here is served on loopback only, and every route except /state/health and /pair requires Authorization: Bearer <token>. Mutating requests also require an X-Pporlock-Client header naming the caller (ui, cli, extension, mcp), which is what makes the audit log meaningful — and what makes unmask refusable for everyone but the UI.
pporlock control API 0.1.0
| Method | Path | Summary |
|---|---|---|
GET |
/state/health |
Liveness. The only unauthenticated route (SPEC-0 §6.1). |
GET |
/state |
Daemon status, active profile, dev toggles, counters, module errors |
POST |
/state |
Set development toggles, start/stop the proxy listener |
GET |
/metrics |
Throughput, latency percentiles, per-module cost, attribution coverage |
GET |
/audit |
Origin-tagged log of state changes (REQ MCP-031) |
GET |
/flows |
Ring buffer, filtered and paginated |
DELETE |
/flows |
Clear the ring buffer |
GET |
/flows/{flow_id} |
One flow with full provenance |
POST |
/flows/{flow_id}/suggest-rule |
Candidate rule matching this flow (REQ WUI-008, MCP-014) |
GET |
/events |
Server-Sent Events stream of flows, module errors, and state changes |
GET |
/modules |
List modules with load state, errors, quarantine, and stats |
POST |
/modules |
Create a module |
GET |
/modules/{name} |
Manifest, parsed rules, Python source, asset listing |
PUT |
/modules/{name} |
Replace module files |
PATCH |
/modules/{name} |
Set enabled, priority, or the module's declared settings |
DELETE |
/modules/{name} |
Remove a module |
GET |
/modules/{name}/report |
A module's own report, rendered by the module |
POST |
/modules/reload |
Force reload of all modules |
POST |
/validate |
Validate a candidate module without installing it (REQ API-027) |
GET |
/profiles |
List profiles |
POST |
/profiles |
Create a profile |
GET |
/profiles/{name} |
One profile |
PUT |
/profiles/{name} |
Replace a profile |
DELETE |
/profiles/{name} |
|
POST |
/profiles/{name}/activate |
Activate a profile without restarting the daemon (REQ MOD-042) |
GET |
/sessions |
List recorded sessions |
POST |
/sessions |
Start recording. Opt-in, off by default (REQ CAP-020). |
GET |
/sessions/{session_id} |
Session metadata |
PATCH |
/sessions/{session_id} |
Rename a session (REQ CAP-021) |
DELETE |
/sessions/{session_id} |
Delete a session and its database file |
GET |
/sessions/{session_id}/export |
Export a session (REQ CAP-024) |
POST |
/sessions/{session_id}/stop |
Stop recording |
GET |
/sessions/{session_id}/flows |
Session flows, same filter vocabulary as /flows |
POST |
/sessions/{session_id}/dryrun |
Evaluate candidate modules against a session without touching live traffic |
GET |
/config |
Effective configuration, with defaults resolved |
PUT |
/config |
Update buffering, capture, budget, redaction, and logging settings |
GET |
/exclusions |
ClientHello exclusion list (REQ PXY-014) |
PUT |
/exclusions |
Replace the exclusion list |
POST |
/pair/begin |
Open a pairing window and return the code (REQ API-012) |
POST |
/pair |
Redeem a pairing code for the bearer token (REQ API-012) |
POST |
/attribution |
Batched (request key -> tab_id) associations (SPEC-0 §3.6) |
GET |
/rules |
The rules currently in force, as loaded (REQ API-022) |
PUT |
/rules |
Replace the rule set without restarting the proxy (REQ MOD-004) |
Liveness. The only unauthenticated route (SPEC-0 §6.1).
Kept cheap and failing closed, because the extension polls it to decide whether to clear Chrome's proxy configuration (REQ EXT-010, PXY-008).
| Status | Meaning |
|---|---|
200 |
Alive |
Daemon status, active profile, dev toggles, counters, module errors
| Status | Meaning |
|---|---|
200 |
Current state |
401 |
Set development toggles, start/stop the proxy listener
Request body: StatePatch
| Status | Meaning |
|---|---|
200 |
Updated state |
401 |
|
403 |
Throughput, latency percentiles, per-module cost, attribution coverage
| Status | Meaning |
|---|---|
200 |
Metrics |
Origin-tagged log of state changes (REQ MCP-031)
| Parameter | In | Type | Notes |
|---|---|---|---|
limit |
query | integer |
|
cursor |
query | string |
| Status | Meaning |
|---|---|
200 |
Audit entries |
Ring buffer, filtered and paginated
| Parameter | In | Type | Notes |
|---|---|---|---|
host |
query | string |
|
path |
query | string |
|
method |
query | string |
|
status |
query | string |
|
content_type |
query | string |
|
dest |
query | string |
|
tab_id |
query | integer |
|
modified |
query | boolean |
|
blocked |
query | boolean |
|
module |
query | string |
|
note_code |
query | string |
|
since |
query | string |
|
until |
query | string |
|
q |
query | string |
Substring over URL |
limit |
query | integer |
|
cursor |
query | string |
|
detail |
query | summary | full | bodies |
Representation level (SPEC-0 §6.3). Bodies dominate response size. |
| Status | Meaning |
|---|---|
200 |
A page of flows |
Clear the ring buffer
| Status | Meaning |
|---|---|
204 |
Cleared |
One flow with full provenance
| Parameter | In | Type | Notes |
|---|---|---|---|
flow_id |
path | string |
required |
detail |
query | summary | full | bodies |
Representation level (SPEC-0 §6.3). Bodies dominate response size. |
unmask |
query | string |
Reveal one masked value, by field path. Live ring-buffer flows only, requires the bearer token, and is audited. |
| Status | Meaning |
|---|---|
200 |
The flow |
404 |
Candidate rule matching this flow (REQ WUI-008, MCP-014)
| Parameter | In | Type | Notes |
|---|---|---|---|
flow_id |
path | string |
required |
Request body: object
| Status | Meaning |
|---|---|
200 |
Candidate rule |
400 |
intent was missing or not one of the four supported values. |
Server-Sent Events stream of flows, module errors, and state changes
Filtered server-side so a narrow client filter reduces event volume rather than merely hiding rows. Clients send Last-Event-ID on reconnect; where replay is impossible the server emits stream.gap (SPEC-0 §7.2).
| Parameter | In | Type | Notes |
|---|---|---|---|
tab_id |
query | integer |
|
kinds |
query | string |
Comma-separated event types |
| Status | Meaning |
|---|---|
200 |
SSE stream |
List modules with load state, errors, quarantine, and stats
| Status | Meaning |
|---|---|
200 |
Modules |
Create a module
Creating a module never enables it (REQ MCP-030).
Request body: ModuleWrite
| Status | Meaning |
|---|---|
201 |
Created, disabled |
400 |
Manifest, parsed rules, Python source, asset listing
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
| Status | Meaning |
|---|---|
200 |
Module detail |
404 |
Replace module files
Updating a module never enables it (REQ MCP-030).
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
Request body: ModuleWrite
| Status | Meaning |
|---|---|
200 |
Updated |
Set enabled, priority, or the module's declared settings
Still narrow: everything else about a module lives in its files, and a PATCH that could rewrite behaviour would bypass the reload that makes a change visible in the module's load state.
config carries values for the fields the module's author declared in settings:, and nothing else — an undeclared key, or a value of the wrong type, is a 400 and nothing is written, so a form with one bad field does not half apply. A module that declares no settings accepts no config at all. Values are persisted in the module-state sidecar rather than written back into the author's manifest, and take effect on the running module without reloading it.
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
Request body: object
| Status | Meaning |
|---|---|
200 |
Updated |
400 |
Remove a module
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
| Status | Meaning |
|---|---|
204 |
Deleted |
A module's own report, rendered by the module
Modules that accumulate something worth reading — an audit, a tally, a diff — render it themselves through an on_report hook, and the daemon serves the result here (OI-29). Served from the control origin rather than through the proxy, because a report about browsing is most wanted when you are not browsing, and a URL that only resolves inside intercepted traffic cannot be linked to from the UI.
The body is module-authored, so it is returned under a sandbox Content-Security-Policy — a unique opaque origin with no script and no same-origin access to the control API. Module code is trusted and could reach the token by other means; this refuses to add a convenient path rather than claiming to be a boundary.
404 when the module does not exist or declares no on_report. 502 when the module raises, or returns a content type outside text/html, text/plain, text/csv and application/json.
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
| Status | Meaning |
|---|---|
200 |
The report, as the module rendered it |
404 |
No such module |
502 |
The module raised |
Force reload of all modules
| Status | Meaning |
|---|---|
200 |
Reload result |
Validate a candidate module without installing it (REQ API-027)
Request body: ModuleWrite
| Status | Meaning |
|---|---|
200 |
Validation result |
List profiles
| Status | Meaning |
|---|---|
200 |
Profiles |
Create a profile
Request body: profile.schema
| Status | Meaning |
|---|---|
201 |
Created |
One profile
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
| Status | Meaning |
|---|---|
200 |
Profile |
Replace a profile
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
Request body: profile.schema
| Status | Meaning |
|---|---|
200 |
Updated |
Refuses to delete 'default' (REQ MOD-041).
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
| Status | Meaning |
|---|---|
204 |
Deleted |
409 |
Activate a profile without restarting the daemon (REQ MOD-042)
| Parameter | In | Type | Notes |
|---|---|---|---|
name |
path | string |
required |
| Status | Meaning |
|---|---|
200 |
New state |
List recorded sessions
| Status | Meaning |
|---|---|
200 |
Sessions |
Start recording. Opt-in, off by default (REQ CAP-020).
Request body: object
| Status | Meaning |
|---|---|
201 |
Recording started |
Session metadata
| Parameter | In | Type | Notes |
|---|---|---|---|
session_id |
path | string |
required |
| Status | Meaning |
|---|---|
200 |
Session metadata |
Rename a session (REQ CAP-021)
| Parameter | In | Type | Notes |
|---|---|---|---|
session_id |
path | string |
required |
Request body: object
| Status | Meaning |
|---|---|
200 |
Updated session metadata |
Delete a session and its database file
| Parameter | In | Type | Notes |
|---|---|---|---|
session_id |
path | string |
required |
| Status | Meaning |
|---|---|
204 |
Deleted |
Export a session (REQ CAP-024)
Exports carry redacted values, never raw ones. A session never held the real value: redaction is applied at write time (REQ CAP-045), so there is nothing here for an export to leak.
| Parameter | In | Type | Notes |
|---|---|---|---|
session_id |
path | string |
required |
format |
query | har | pporlock |
| Status | Meaning |
|---|---|
200 |
The exported session |
Stop recording
| Parameter | In | Type | Notes |
|---|---|---|---|
session_id |
path | string |
required |
| Status | Meaning |
|---|---|
200 |
Recording stopped |
Session flows, same filter vocabulary as /flows
SPEC-0 §6.8 says "the same filter vocabulary as §6.5", and this listed four of the seventeen. The prose is normative (CLAUDE.md's precedence rule), so the document is widened to match rather than the other way round — and a contract test written against the narrow version would have been right for the wrong reason (docs/open-issues.md OI-5).
tab_id is the one exception: attribution is a property of the live
browser session, and a recorded session's tab ids refer to tabs that no
longer exist.
| Parameter | In | Type | Notes |
|---|---|---|---|
session_id |
path | string |
required |
host |
query | string |
|
path |
query | string |
|
method |
query | string |
|
status |
query | string |
|
content_type |
query | string |
|
dest |
query | string |
|
modified |
query | boolean |
|
blocked |
query | boolean |
|
module |
query | string |
|
note_code |
query | string |
|
since |
query | string |
|
until |
query | string |
|
q |
query | string |
Substring over URL |
limit |
query | integer |
|
cursor |
query | string |
|
detail |
query | summary | full | bodies |
Representation level (SPEC-0 §6.3). Bodies dominate response size. |
| Status | Meaning |
|---|---|
200 |
A page of flows |
Evaluate candidate modules against a session without touching live traffic
Executes the candidate module's Python hooks (REQ CAP-032). Uses the same Evaluator and ModuleLoader as live traffic — there is no second implementation (REQ CAP-031).
| Parameter | In | Type | Notes |
|---|---|---|---|
session_id |
path | string |
required |
Request body: DryRunRequest
| Status | Meaning |
|---|---|
200 |
Dry run result |
Effective configuration, with defaults resolved
| Status | Meaning |
|---|---|
200 |
Effective configuration |
Update buffering, capture, budget, redaction, and logging settings
listen_host values are validated to be loopback and rejected otherwise (REQ API-010).
Request body: object
| Status | Meaning |
|---|---|
200 |
Updated configuration |
ClientHello exclusion list (REQ PXY-014)
| Status | Meaning |
|---|---|
200 |
Exclusions |
Replace the exclusion list
Takes effect without a daemon restart (REQ PXY-014). Removing a certificate-pinning or financial host may break that site or draw its traffic into the capture buffer.
Request body: Exclusions
| Status | Meaning |
|---|---|
200 |
Updated |
Open a pairing window and return the code (REQ API-012)
Authenticated, unlike /pair itself: only something that can already
read the token — the CLI or the web UI — may open a window. That is
what makes the code safe to read aloud. It is worthless without a human
having just asked for it, and it expires.
pporlock pair is this route.
| Status | Meaning |
|---|---|
200 |
A pairing window is open |
Redeem a pairing code for the bearer token (REQ API-012)
Available only within a short window opened by pporlock pair or a web
UI button, and only from a chrome-extension:// origin. The extension
never reads the filesystem.
Request body: object
| Status | Meaning |
|---|---|
200 |
Token issued |
403 |
Batched (request key -> tab_id) associations (SPEC-0 §3.6)
Best-effort. Joined against the ring buffer within a bounded window; matched flows emit flow.updated. Attribution never blocks a flow.
Request body: object
| Status | Meaning |
|---|---|
200 |
Accepted count |
The rules currently in force, as loaded (REQ API-022)
The rules the running proxy is evaluating, which is the union of
rules.yaml and every active module's rules. Disabled rules are not
included: this is what is in force, not what is on disk.
| Status | Meaning |
|---|---|
200 |
The active rule set |
Replace the rule set without restarting the proxy (REQ MOD-004)
The new set is compiled before it is swapped in, so a rule that does not compile leaves the running rules untouched rather than emptying them — a 400 here means nothing changed.
The swap replaces an immutable snapshot, so a flow already in flight finishes against the rules it started with.
Request body: object
| Status | Meaning |
|---|---|
200 |
The rule set now in force |
400 |
A rule did not compile. The running rules are unchanged. |
See also: SPEC-0 §6 for the normative prose, and contracts/openapi.yaml for the machine-readable source.