Documentation

Overview

StubSmith is a traffic-capture and test-fixture platform built around three stages: capture, anonymize, and serve. Your services send HTTP call records to a lightweight Go ingest service (POST /v1/captures), which applies your project's anonymization rules and writes sanitized captures to the database. A fail-closed pending gate holds captures until rules are reviewed and accepted, preventing any unanonymized data from reaching permanent storage.

Each captured request is classified into a request type — a (method, path pattern) pair. You configure which fields to mask per request type in the Request Types editor. Once data is flowing, the fixtures API (GET /v1/fixtures) returns every distinct response variant your app has seen for a given request type, ready to be parametrized over by your test suite. The Python SDK wraps this into a single function call.


Capturing traffic

Python SDK

Install from clients/python. Calling stubsmith.install() patches both requests and httpx (whichever is importable) so every outbound HTTP call is forwarded asynchronously — fire-and-forget, failures silently swallowed.

Python
import stubsmith

# Add to your application entrypoint or conftest.py
stubsmith.install(
    url="http://localhost:8081/v1/captures",
    api_key="sk-your-project-key",
)

# All requests made via requests or httpx are now captured.

Express middleware

Use examples/express-middleware.js. It intercepts request and response data on res.finish and posts to the ingest endpoint. Place it early in your Express middleware chain.

JavaScript (Express)
const captureMiddleware = require('./express-middleware');

// Add early in your middleware chain
app.use(captureMiddleware({
  url: process.env.STUBSMITH_URL,     // http://<ingest-host>:8081/v1/captures
  key: process.env.STUBSMITH_API_KEY,
}));

Ingest endpoint

Both integrations post to the Go ingest service atPOST /v1/captures(default port 8081), authenticated withAuthorization: Bearer <project-key>. The ingest service is separate from the Node API (default port 3000).


Anonymization pipeline

StubSmith applies anonymization in layers so that no sensitive data reaches permanent storage:

  1. Ingest → pending request type. When a new request arrives the Go ingest service resolves (or creates) the matching request type. The request/response bodies are passed through everyenabled global anonymizer rule set — masking configured fields and applying regex replacements — before being stored as the sample. The same global-rules pass derives initial suggestions and pre-populates themask_paths column for that request type, which surface as pre-checked suggestions in the Request Types editor. Nothing is written to the permanent capture store at this stage.
  2. Configure → enable permanent capture. A request type stays in the pending state and no captures are retained until an operator opens it in the Request Types editor, reviews the sample, setsmask_paths, and saves the configuration. Only after that does the ingest service begin writing matching captures to permanent storage.
  3. Capture → multi-layer scrub. Each incoming capture for a configured request type is processed in order: (a) per-request-type mask_paths replace matched fields with typed placeholders; (b) every enabled global anonymizer rule set is applied as a defense-in-depth pass over headers and bodies; (c) the built-in PII scrubber runs pattern-based redaction over the capture source, path, and raw request line (not the bodies — those are covered by steps a and b); (d) the email-leak guard scans all remaining string values — including the fully masked bodies — and routes any capture where an email-like pattern is still detected to a quarantine table rather than permanent storage, writing acapture_alert for operator review.

Global anonymizer rule sets

A project can have any number of named global rule sets. Each rule set is independently toggled on or off; all enabled sets are merged and applied in the two places described above (sample masking and capture defense-in-depth). Manage rule sets in the Global Anonymizer section of the dashboard.

Rule sets support two mechanisms:

Field masks — dot-path references into the request or response object (e.g. resp_body.customer.email,req_headers.authorization). Matched fields are replaced with a redacted placeholder.

Regex masks — pattern+replacement pairs run against the serialized body, useful for catching values that shift between fields.

Per-request-type mask paths

Per-endpoint mask paths are configured in the Request Types editor and provide precision masking on top of the global rules. Dynamic path segments that vary between requests can be abstracted using{param} patterns (e.g. /v1/charges/{id}), grouping all matching concrete paths under a single request type and absorbing redundant concrete rows.


Using fixtures in tests

GET /v1/fixtures is the primary consumer endpoint. It returns all recorded response variants for a given request type — one per unique status code when distinct=status is set — so your parametrized tests cover every real behaviour the API has exhibited.

Query parameters

ParameterRequiredDescription
requestTypeIdOne ofUUID of a configured request type. Mutually exclusive with method+path.
method + pathOne ofHTTP method and path. Resolved against configured request types (dynamic pattern match first, then exact); falls back to exact path match if no request type matches.
distinctNoPass status to return one fixture per unique HTTP status code. Currently the only supported value.
limitNoMaximum fixtures to return (default 20, max 100). Selection scans the most recent 500 captures for the given method.

curl example

curl
# Requires env vars: STUBSMITH_API_URL, STUBSMITH_API_KEY
curl -G "${STUBSMITH_API_URL}/v1/fixtures" \
  --data-urlencode "method=POST" \
  --data-urlencode "path=/v1/charges/{id}" \
  --data-urlencode "distinct=status" \
  -H "Authorization: Bearer ${STUBSMITH_API_KEY}"

Example response

JSON
{
  "ok": true,
  "request_type": {
    "id": "rt_01j9x2...",
    "method": "POST",
    "path_pattern": "/v1/charges/{id}",
    "is_dynamic": true,
    "status": "configured"
  },
  "count": 2,
  "fixtures": [
    {
      "id": "cap_01j9...",
      "captured_at": "2025-11-12T10:22:04Z",
      "method": "POST",
      "path": "/v1/charges/ch_abc123",
      "status": 200,
      "duration_ms": 84,
      "request": {
        "headers": { "content-type": "application/json" },
        "body": { "amount": 4200, "currency": "usd", "customer": "[REDACTED]" }
      },
      "response": {
        "headers": { "content-type": "application/json" },
        "body": { "id": "ch_abc123", "status": "succeeded", "amount": 4200 }
      }
    },
    {
      "id": "cap_01j8...",
      "captured_at": "2025-11-10T08:14:51Z",
      "method": "POST",
      "path": "/v1/charges/ch_def456",
      "status": 402,
      "duration_ms": 61,
      "request": {
        "headers": { "content-type": "application/json" },
        "body": { "amount": 9900, "currency": "usd", "customer": "[REDACTED]" }
      },
      "response": {
        "headers": { "content-type": "application/json" },
        "body": { "error": { "code": "card_declined", "message": "Your card was declined." } }
      }
    }
  ]
}

pytest (Python SDK)

Set STUBSMITH_API_URL and STUBSMITH_API_KEY before running tests, or pass them as keyword arguments to stubsmith.fixtures(). The call is a blocking fetch that occurs at pytest collection time.

pytest
import pytest
import stubsmith

fixtures = stubsmith.fixtures("POST /v1/charges/{id}", distinct="status")

@pytest.mark.parametrize("fx", fixtures, ids=lambda f: str(f.status))
def test_handles_every_recorded_response(fx):
    body = fx.response.json()
    # assert your application contract against body and fx.status
    ...

Pattern resolution note: passing a path containing{param} segments resolves only against configured request types that have a matching pattern. Passing a concrete path (e.g. /v1/charges/ch_abc123) will match a pattern-based request type via segment matching, or fall back to an exact path match if no pattern type is configured.


API reference

All endpoints except the Go ingest are served by the Node API. Project API keys authenticate with Authorization: Bearer <project-key>. Dashboard operations require an admin JWT from POST /v1/auth/login.

MethodPathAuthPurpose
POST/v1/capturesProject keyIngest a captured request/response (Go service, port 8081)
GET/v1/capturesJWT or project keyList captures for a project (newest first, max 100)
GET/v1/captures/:idJWT or project keyFull capture detail; append ?includeBodies=true to resolve offloaded body references
GET/v1/request-typesJWT or project keyList request types (method, path, pattern, status) for a project
GET/v1/request-types/:idJWT or project keyFull detail including sample capture and mask_paths
PUT/v1/request-types/:idJWT or project keySave mask_paths and mark request type as configured
PUT/v1/request-types/:id/patternJWT or project keySet a path pattern; absorbs matching concrete request type rows
GET/v1/anonymizer/rulesJWT or project keyList named global anonymizer rule sets for a project
POST/v1/anonymizer/rulesJWT or project keyCreate a new named rule set; 409 if name already exists
PUT/v1/anonymizer/rules/:idJWT or project keyPartially update a rule set — accepts any combination ofname, rules, and enabled
DELETE/v1/anonymizer/rules/:idJWT or project keyDelete a rule set
GET/v1/fixturesJWT or project keyFetch fixture variants for a request type (see above)
GET/v1/replays legacyJWT or project keyList replay records — prefer the fixtures API for test parametrization
POST/v1/replays legacyJWT or project keyEnqueue a replay job for a capture — prefer the fixtures API instead