Browser automation works—until it doesn’t. If you’re building Claude Skills or client automations, the fastest path to reliable execution is often private API automation: call the same JSON endpoints the web app calls, instead of driving the DOM.
This post is a practical playbook you can run in under an hour: HAR → replay → adapter → tool contract → guardrails. It’s the same “API-first, token-in token-out” philosophy we’re building toward at nNode—because agents are far more deterministic when you give them structured inputs/outputs instead of HTML.
Why private API automation beats browser automation (most days)
Playwright/Puppeteer failures usually look like:
- Selectors change (A/B tests, redesigns, “minor” UI updates)
- Anti-bot / challenge pages appear
- Timing flakes (spinners, lazy-loading, race conditions)
- Token burn: the agent has to “see” and reason over lots of DOM
Meanwhile, many web apps run on stable XHR/fetch calls (or a single GraphQL endpoint). If you can capture and replay those requests, you can get:
- Faster runs (no rendering)
- More deterministic I/O (JSON)
- Easier retries + idempotency
- Cleaner audit logs
The 15-minute checklist: is this a good private API automation target?
Before you invest, confirm:
- Network tab shows XHR/fetch with JSON responses.
- Stable identifiers exist (record IDs, emails, slugs).
- Auth is reproducible (cookie session, bearer token, or SSO you can refresh).
- Write actions are “safe-able” (you can add approvals, idempotency keys, dedupe).
- Rate limits / abuse controls are understood (expect 429s; plan backoff).
If the app is purely server-rendered HTML with no JSON calls, stop—you’ll likely need browser automation.
Step 1 — Capture a clean HAR (and filter out the noise)
Record one “golden path” flow (e.g., Search → Open record → Create → Confirm), then export a HAR.
HAR hygiene tips:
- Filter to
fetch/xhr(ignore images, JS bundles, fonts) - Re-run the flow once to confirm which calls are essential
- Identify the smallest set of requests that produces the outcome
If you want a quick way to list candidate endpoints from a HAR:
// har-list-endpoints.mjs
import fs from "node:fs";
const har = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const entries = har.log.entries;
const endpoints = entries
.filter(e => ["xhr", "fetch"].includes(e._resourceType))
.map(e => ({
method: e.request.method,
url: e.request.url.split("?")[0],
status: e.response.status,
mime: e.response.content.mimeType
}));
console.table(endpoints);
Run:
node har-list-endpoints.mjs ./capture.har
Step 2 — Find the “contract endpoints” (read path + write path)
For agent integrations, you usually want two categories:
- Read path:
list/search,get-by-id,lookup-by-email, etc. - Write path:
create,update,action(approve, send, archive)
If it’s GraphQL
GraphQL often uses one URL (e.g., /graphql). Stability comes from:
operationNamevariables- (sometimes) persisted queries
Your adapter should treat GraphQL like a set of named operations instead of “one endpoint.”
Step 3 — Make auth boring (cookies, headers, refresh)
Auth is where “it worked on my laptop” automations go to die.
Common patterns:
- Session cookies: easiest to capture, but you need a re-login strategy.
- Bearer tokens: often stored in localStorage; can expire; refresh token flow may exist.
- CSRF tokens: sent as a header or cookie pair; must be preserved.
Practical approach for agencies:
- Start with cookie-based replay (fastest to validate).
- Then add a refresh step (programmatic login or “human reauth” fallback).
- Store secrets in a vault; never commit HARs to git (they frequently contain tokens).
Step 4 — Replay outside the browser (curl → minimal client)
Take one essential request and reproduce it with curl. Your goal: one command that works consistently.
curl 'https://app.example.com/api/v1/items' \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-H 'x-csrf-token: <csrf>' \
-H 'cookie: session=<session_cookie>' \
--data '{"query":"invoices","page":1}'
Once curl works, wrap it in a tiny adapter.
Step 5 — Normalize into a tool contract (inputs/outputs your agent can trust)
This is where private API automation becomes “agent-ready.” Don’t expose raw app payloads—define a stable contract.
Example tool schema (what you want your agent to see):
{
"name": "search_invoices",
"description": "Search invoices in ExampleApp by customer email and status.",
"input_schema": {
"type": "object",
"properties": {
"customer_email": {"type": "string"},
"status": {"type": "string", "enum": ["open", "paid", "void"]},
"page": {"type": "integer", "minimum": 1, "default": 1}
},
"required": ["customer_email"]
},
"output_schema": {
"type": "object",
"properties": {
"invoices": {
"type": "array",
"items": {
"type": "object",
"properties": {
"invoice_id": {"type": "string"},
"amount": {"type": "number"},
"currency": {"type": "string"},
"status": {"type": "string"}
},
"required": ["invoice_id", "status"]
}
},
"next_page": {"type": ["integer", "null"]}
},
"required": ["invoices"]
}
}
Key idea: your agent shouldn’t care whether the underlying system is REST, GraphQL, or “weird private JSON.”
Step 6 — Production hardening: retries, idempotency, safe re-runs
Automations fail. Your job is to make failures predictable.
Retries with backoff + jitter
function sleep(ms: number) {
return new Promise(r => setTimeout(r, ms));
}
async function withRetry<T>(fn: () => Promise<T>, max = 4) {
for (let attempt = 0; attempt <= max; attempt++) {
try {
return await fn();
} catch (err: any) {
const retryable = [429, 500, 502, 503, 504].includes(err?.status);
if (!retryable || attempt === max) throw err;
const base = 500 * 2 ** attempt;
const jitter = Math.floor(Math.random() * 200);
await sleep(base + jitter);
}
}
throw new Error("unreachable");
}
Idempotency keys for writes
For POST actions that create side effects, add a client-generated idempotency key if the API supports it (common header name: Idempotency-Key). If it doesn’t, implement a dedupe layer on your side (e.g., request fingerprint → “already processed” record).
Step 7 — Safety & governance (agency-ready)
This is where nNode’s “guardrails and control” mindset matters in real deployments.
Add:
- Allowlist of domains + endpoints (block everything else)
- Approval gates for writes (POST/PATCH/DELETE)
- Redaction (strip cookies/tokens from logs; store only metadata)
- Audit logs with request IDs and business context (“who approved what”)
A simple but powerful pattern is “approve on diff”: show the human the normalized tool input before executing the write.
Maintenance plan: detect breaking changes early
Private APIs change—but usually in ways you can monitor.
- Write contract tests that run daily (or per deploy)
- Version your adapter (v1, v2) instead of hot-editing
- Alert on error rates and auth failures (auth issues look like sudden 401/403 spikes)
Example workflow decomposition (template)
Here’s a generic, repeatable pattern:
- Search (
search_*tool) → returns IDs - Enrich (
get_*_by_id) → returns normalized fields - Write (
create_*/update_*) → requires approval + idempotency - Notify (Slack/Email) → includes audit trail
This separation is what makes API-first agents faster and safer than “one giant browser script.”
Want this as a repeatable system, not a one-off script?
If you’re an automation agency or building Claude-powered workflows for internal teams, nNode is designed around API-first execution—turning real app requests into stable, auditable tools with guardrails so you can ship automations that survive UI churn.
When you’re ready, take a look at nnode.ai and try mapping one of your most brittle browser flows into a HAR-to-tool integration.