Operations

Turn a public page into a JSON API.

Give Wayfern the address of a public page with a {parameter} where the input goes, and two example values. Wayfern loads the page in its own browsers, finds the request the page makes for its data, and turns it into a versioned template. From then on, one call with new arguments returns that data as JSON.

What it is

An operation is a read of a public, signed-out page, saved with typed parameters and typed output fields. Calls answer over plain HTTP when the site allows it, and fall back to a warm Wayfern browser and then to running the page. When the site changes, a repair runs in the background, not on your call.

  1. 1 · Learn

    The page, its parameters and two examples each. 3 credits when it finds the data.

  2. 2 · Create

    Pick a candidate and its fields. The server validates it, then saves version 1.

  3. 3 · Call

    One POST with your arguments. 1 credit per call that answers.

From a page to a call

Use an API token with the web scope. The dashboard does the same steps with previews at Operations → New operation.

1. Learn
curl -X POST 'https://api.wayfern.com/api/v1/projects/prj_31f0/operations/learn' \
  -H "Authorization: Bearer $WF_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
  "trigger": {
    "url": "https://shop.example/search?q={query}"
  },
  "params": [
    {
      "name": "query",
      "type": "string",
      "examples": [
        "desk lamp",
        "office chair"
      ]
    }
  ],
  "options": {
    "proxy": "auto"
  }
}'
2. Create from the best candidate
curl -X POST 'https://api.wayfern.com/api/v1/projects/prj_31f0/operations' \
  -H "Authorization: Bearer $WF_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
  "learnId": "lrn_4kQ9w2Zc",
  "auto": true
}'
3. Call
curl -X POST 'https://api.wayfern.com/api/v1/operations/op_7Hx2mP/call' \
  -H "Authorization: Bearer $WF_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
  "args": {
    "query": "standing desk"
  }
}'

How learning works

Learning loads the trigger page 3 times, each in a fresh anonymous Wayfern browser: with example 1, with example 2, and with example 1 again as a new visitor. Each run waits for the network to go quiet, at most 20 s, and a challenge page that clears itself is waited for up to 15 s. All runs together stop at 60 s. No language model is involved.

  • Find. Every request the page made is searched for your example values. Assets, analytics, preflights and redirects are skipped.
  • Compare. A value that changes from run 1 to run 2 and comes back in run 3 is a parameter slot. One that changes between run 1 and run 3 is per-visit and is filled fresh on every call.
  • Rank. Candidates that carry your examples, answer 2xx with JSON from the page’s own XHR or fetch come first. When no request qualifies, the page HTML or JSON embedded in it is a candidate.
  • Outline. Each candidate comes with where the examples sit, the suggested extract path, proposed fields with up to three sample values each, the list size, the lowest tier it can use and warnings.
  • Validate. Creating the operation replays example 2 and example 1 and checks that the fields come back typed. A bot wall or a login page is refused.

What is refused

Login walls

Wayfern visits as an anonymous, signed-out visitor. A page that asks to sign in, and an operation whose validation lands on a login page, are refused. There is no way to add your own login, cookies or session.

Writes

Only reads are learned: GET and HEAD, and POST only for a GraphQL query without a mutation, or a body the page sent on its own while loading. PUT, PATCH and DELETE never. When a call runs the page, once it has loaded, every request that is not a GET is stopped except the one the operation reads.

Private hosts

Addresses that resolve to private, loopback or internal networks are refused, on the trigger page and again on every redirect.

robots.txt

The robots.txt of the trigger page must allow WayfernBot. When it does not, learning stops with a message that says so.

Credentials in the request

Cookie and Authorization values, per-visit tokens and anything equal to a cookie or storage value are taken out of the learned request. If one is still found in any encoding, the learn is refused.

Tiers

Every call starts at the cheapest tier that can work and moves up only when the site blocks it. The tier in a response says which one answered.

Fig 1 Three ways to answer
Flip a condition, make a call · drag to turn

  • The call, and the gate answering it
  • Gates: cache, plain HTTP, warm page, page run
  • Flaps shut over gates a call must skip

Each call is answered the cheapest way that works: the cache, then plain HTTP, then a warm page held open for 5 minutes after its last use, then running the page. A block at plain HTTP moves the call up, and the operation remembers it. Every successful call is 1 credit, at any tier.

TierWhat runs the call
Tier 1 · Plain HTTPWayfern’s servers send the learned request with the browser’s headers and no cookies. It can answer in well under a second, without a browser. Not used when the site needs cookies a visitor’s browser gets first.
Tier 2 · Warm browserA Wayfern browser keeps one page open on the site, per project and exit, for 5 minutes after its last use, and sends the request from inside it with the anonymous cookies the site set there.
Tier 3 · Runs the pageA fresh Wayfern browser page opens the trigger page with your arguments and reads the response that matches. It accepts the response only when the request carried your arguments where the template puts them.
  • minTier is the lowest tier an operation can use, found while learning. A site that needs a visitor’s cookies never uses tier 1; one that signs every request per visit needs tier 3.
  • A call starts at the higher of minTier and the tier that last worked for that exit. It moves up on blocked, and once on rate after a pause. Tier 3 is the last.
  • Set options.maxTier to cap a call, for example at 1 to never use a browser. A call to an operation that needs a higher tier fails.
  • Proxy auto starts on a datacenter exit and moves to residential when blocked. Residential bandwidth is metered.

Repair and versions

A call whose answer no longer fits the operation is classed drift: it still tries tier 3 once, and a repair starts in the background. A repair learns the page again with the stored examples, keeps the parameters and fields as they are, and changes only the request. It runs one at a time, at most 3 times a day per operation, and is free.

  • Repair on its own (default). A repair that passes validation becomes the new active version; the old one is kept.
  • Approve first. With it off, the repair is a proposed version and health is degraded until an owner or admin approves or rejects it.
  • Roll back. Any earlier version can be activated again.
  • Scheduled checks. Every 6, 12, 24 (default) or 72 hours, or off, both examples are replayed for operations called in the last 14 days. Free.
  • After a failed repair. If an operation fails again within 10 minutes of a repair, calls go straight to tier 3 for 30 minutes. After 3 failed repairs, health is broken and the project owner gets an email, at most once a day.
HealthMeaning
Healthy okRecent calls and checks answered.
Degraded degradedThe site changed. A repair waits for an owner or admin to approve it.
Broken brokenRepairs keep failing. Calls may fail until a repair works or you roll back.
Unknown unknownNo call or check has run yet.

REST endpoints

Every route is under https://api.wayfern.com/api/v1 and takes a Bearer API token (wf_…) with the web scope, or a dashboard session. Answers use the Web Data envelope: success, data and the credits the call debited.

Learn a page

POST /api/v1/projects/{projectId}/operations/learn Project member

Loads the trigger page 3 times and returns the requests that carry your examples, best first. Takes tens of seconds. Costs 3 credits when it returns candidates, nothing otherwise. The draft is kept 30 minutes.

Request body

{
  "trigger": {
    "url": "https://shop.example/search?q={query}"
  },
  "params": [
    {
      "name": "query",
      "type": "string",
      "examples": [
        "desk lamp",
        "office chair"
      ]
    }
  ],
  "options": {
    "proxy": "auto"
  }
}

Response: 201 Created

{
  "success": true,
  "data": {
    "learnId": "lrn_4kQ9w2Zc",
    "expiresAt": "2026-10-12T10:30:00.000Z",
    "candidates": [
      {
        "id": "cnd_1",
        "rank": 1,
        "method": "GET",
        "host": "api.shop.example",
        "path": "/v2/search",
        "graphqlOperation": null,
        "format": "json",
        "source": "fetch",
        "reasons": [
          "Both examples sit in query:q",
          "JSON with a list of 24 items that hold the example"
        ],
        "warnings": [],
        "minTier": 1,
        "anonymousState": "none",
        "slots": [
          {
            "param": "query",
            "at": "query:q"
          }
        ],
        "extractPath": "/results",
        "list": true,
        "listSize": 24,
        "fields": [
          {
            "name": "title",
            "source": "/title",
            "type": "string",
            "required": true,
            "samples": [
              "Brass desk lamp",
              "Clamp lamp",
              "LED desk lamp"
            ]
          },
          {
            "name": "price",
            "source": "/price/amount",
            "type": "number",
            "required": true,
            "samples": [
              49.9,
              29,
              35.5
            ]
          },
          {
            "name": "url",
            "source": "/link",
            "type": "url",
            "required": true,
            "samples": [
              "https://shop.example/p/1042",
              "https://shop.example/p/877",
              "https://shop.example/p/913"
            ]
          }
        ]
      }
    ],
    "warnings": []
  },
  "credits": 3
}

Read a learn draft

GET /api/v1/projects/{projectId}/operations/learn/{learnId} Project member

The same answer as the learn, with credits 0, while the draft is kept. Free.

Request and response as in Learn a page.

Create an operation

POST /api/v1/projects/{projectId}/operations Project member

Turns a candidate into version 1. Omit fields to keep every proposed field; send {"learnId", "auto": true} to take the best candidate as it is. Before saving, the server replays example 2 and example 1 and refuses a bot wall, a login page or output that does not match the fields.

Request body

{
  "learnId": "lrn_4kQ9w2Zc",
  "candidateId": "cnd_1",
  "slug": "shop-search",
  "name": "Shop search",
  "description": "Products for a search term",
  "fields": [
    {
      "source": "/title",
      "name": "title",
      "type": "string",
      "required": true
    },
    {
      "source": "/price/amount",
      "name": "price",
      "type": "number",
      "required": true
    }
  ]
}

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "op_7Hx2mP",
    "projectId": "prj_31f0",
    "slug": "shop-search",
    "name": "Shop search",
    "description": "Products for a search term",
    "site": "shop.example",
    "status": "active",
    "health": "ok",
    "activeVersionId": "ver_1",
    "activeVersion": 1,
    "trigger": {
      "url": "https://shop.example/search?q={query}",
      "waitForSelector": null
    },
    "params": [
      {
        "name": "query",
        "type": "string",
        "required": true,
        "description": null,
        "examples": [
          "desk lamp",
          "office chair"
        ]
      }
    ],
    "fields": [
      {
        "name": "title",
        "source": "/title",
        "type": "string",
        "required": true
      },
      {
        "name": "price",
        "source": "/price/amount",
        "type": "number",
        "required": true
      }
    ],
    "list": true,
    "minTier": 1,
    "autoRepair": true,
    "checkIntervalHours": 24,
    "cacheTtlSeconds": 0,
    "defaultProxy": "auto",
    "defaultCountry": null,
    "solveChallenges": true,
    "createdBy": "usr_92a1",
    "createdAt": "2026-10-12T10:02:11.000Z",
    "updatedAt": "2026-10-12T10:02:11.000Z",
    "lastCalledAt": null,
    "lastCheckedAt": null,
    "stats": null
  },
  "credits": 0
}

List operations

GET /api/v1/projects/{projectId}/operations?page=1&pageSize=20 Project member

The operations of a project with their health and the last 7 days of numbers.

Response: 200 OK

{
  "success": true,
  "data": {
    "operations": [
      {
        "id": "op_7Hx2mP",
        "projectId": "prj_31f0",
        "slug": "shop-search",
        "name": "Shop search",
        "description": "Products for a search term",
        "site": "shop.example",
        "status": "active",
        "health": "ok",
        "activeVersionId": "ver_1",
        "activeVersion": 1,
        "trigger": {
          "url": "https://shop.example/search?q={query}",
          "waitForSelector": null
        },
        "params": [
          {
            "name": "query",
            "type": "string",
            "required": true,
            "description": null,
            "examples": [
              "desk lamp",
              "office chair"
            ]
          }
        ],
        "fields": [
          {
            "name": "title",
            "source": "/title",
            "type": "string",
            "required": true
          },
          {
            "name": "price",
            "source": "/price/amount",
            "type": "number",
            "required": true
          }
        ],
        "list": true,
        "minTier": 1,
        "autoRepair": true,
        "checkIntervalHours": 24,
        "cacheTtlSeconds": 0,
        "defaultProxy": "auto",
        "defaultCountry": null,
        "solveChallenges": true,
        "createdBy": "usr_92a1",
        "createdAt": "2026-10-12T10:02:11.000Z",
        "updatedAt": "2026-10-12T10:02:11.000Z",
        "lastCalledAt": null,
        "lastCheckedAt": null,
        "stats": null
      }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 20
  },
  "credits": 0
}

Read an operation

GET /api/v1/projects/{projectId}/operations/{operationId} Project member

Parameters with their examples, output fields, settings and numbers.

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "op_7Hx2mP",
    "projectId": "prj_31f0",
    "slug": "shop-search",
    "name": "Shop search",
    "description": "Products for a search term",
    "site": "shop.example",
    "status": "active",
    "health": "ok",
    "activeVersionId": "ver_1",
    "activeVersion": 1,
    "trigger": {
      "url": "https://shop.example/search?q={query}",
      "waitForSelector": null
    },
    "params": [
      {
        "name": "query",
        "type": "string",
        "required": true,
        "description": null,
        "examples": [
          "desk lamp",
          "office chair"
        ]
      }
    ],
    "fields": [
      {
        "name": "title",
        "source": "/title",
        "type": "string",
        "required": true
      },
      {
        "name": "price",
        "source": "/price/amount",
        "type": "number",
        "required": true
      }
    ],
    "list": true,
    "minTier": 1,
    "autoRepair": true,
    "checkIntervalHours": 24,
    "cacheTtlSeconds": 0,
    "defaultProxy": "auto",
    "defaultCountry": null,
    "solveChallenges": true,
    "createdBy": "usr_92a1",
    "createdAt": "2026-10-12T10:02:11.000Z",
    "updatedAt": "2026-10-12T10:02:11.000Z",
    "lastCalledAt": "2026-10-12T11:40:00.000Z",
    "lastCheckedAt": null,
    "stats": null
  },
  "credits": 0
}

Change settings

PATCH /api/v1/projects/{projectId}/operations/{operationId} Project owner or admin

Any of name, description, status (active or paused), autoRepair, checkIntervalHours (0, 6, 12, 24 or 72), cacheTtlSeconds, defaultProxy, defaultCountry and solveChallenges.

Request body

{
  "autoRepair": false,
  "checkIntervalHours": 6,
  "cacheTtlSeconds": 300
}

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "op_7Hx2mP",
    "projectId": "prj_31f0",
    "slug": "shop-search",
    "name": "Shop search",
    "description": "Products for a search term",
    "site": "shop.example",
    "status": "active",
    "health": "ok",
    "activeVersionId": "ver_1",
    "activeVersion": 1,
    "trigger": {
      "url": "https://shop.example/search?q={query}",
      "waitForSelector": null
    },
    "params": [
      {
        "name": "query",
        "type": "string",
        "required": true,
        "description": null,
        "examples": [
          "desk lamp",
          "office chair"
        ]
      }
    ],
    "fields": [
      {
        "name": "title",
        "source": "/title",
        "type": "string",
        "required": true
      },
      {
        "name": "price",
        "source": "/price/amount",
        "type": "number",
        "required": true
      }
    ],
    "list": true,
    "minTier": 1,
    "autoRepair": false,
    "checkIntervalHours": 6,
    "cacheTtlSeconds": 300,
    "defaultProxy": "auto",
    "defaultCountry": null,
    "solveChallenges": true,
    "createdBy": "usr_92a1",
    "createdAt": "2026-10-12T10:02:11.000Z",
    "updatedAt": "2026-10-12T10:02:11.000Z",
    "lastCalledAt": null,
    "lastCheckedAt": null,
    "stats": null
  },
  "credits": 0
}

Delete an operation

DELETE /api/v1/projects/{projectId}/operations/{operationId} Project owner or admin

Removes the operation with its versions and events. Calls to it fail from then on.

Response: 204 No Content, no body.

Call an operation

POST /api/v1/projects/{projectId}/operations/{operationId}/call Project member

Fills the template with args and answers with the data. 1 credit per call that answers, cache hits included; a failed call is free. options may set maxTier, proxy, country and cacheTtlSeconds.

Request body

{
  "args": {
    "query": "standing desk"
  },
  "options": {
    "maxTier": 2
  }
}

Response: 200 OK

{
  "success": true,
  "data": {
    "result": [
      {
        "title": "Oak standing desk",
        "price": 389
      },
      {
        "title": "Standing desk frame",
        "price": 219.5
      }
    ],
    "tier": 1,
    "cached": false,
    "version": 1,
    "ms": 640,
    "truncated": false
  },
  "credits": 1
}

Call, short form

POST /api/v1/operations/{operationId}/call Project member

The same call without the project in the path; membership is checked from the operation. The form to use from your own code.

Request and response as in Call an operation.

List versions

GET /api/v1/projects/{projectId}/operations/{operationId}/versions Project member

Every version, newest first: learned, repaired, edited or rolled back, and which one is active.

Response: 200 OK

{
  "success": true,
  "data": {
    "versions": [
      {
        "id": "ver_2",
        "version": 2,
        "state": "proposed",
        "source": "repair",
        "parentVersionId": "ver_1",
        "validatedAt": "2026-10-14T03:12:40.000Z",
        "createdBy": null,
        "createdAt": "2026-10-14T03:12:40.000Z",
        "match": {
          "method": "GET",
          "host": "api.shop.example",
          "pathPattern": "/v3/search"
        },
        "minTier": 1
      }
    ]
  },
  "credits": 0
}

Activate a version

POST /api/v1/projects/{projectId}/operations/{operationId}/versions/{versionId}/activate Project owner or admin

Approves a proposed repair, or rolls back to an earlier version. The version that was active is kept.

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "ver_2",
    "version": 2,
    "state": "active",
    "source": "repair",
    "parentVersionId": "ver_1",
    "validatedAt": "2026-10-14T03:12:40.000Z",
    "createdBy": null,
    "createdAt": "2026-10-14T03:12:40.000Z",
    "match": {
      "method": "GET",
      "host": "api.shop.example",
      "pathPattern": "/v3/search"
    },
    "minTier": 1
  },
  "credits": 0
}

Reject a repair

POST /api/v1/projects/{projectId}/operations/{operationId}/versions/{versionId}/reject Project owner or admin

Turns down a proposed repair. Calls keep using the active version.

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "ver_2",
    "version": 2,
    "state": "rejected",
    "source": "repair",
    "parentVersionId": "ver_1",
    "validatedAt": "2026-10-14T03:12:40.000Z",
    "createdBy": null,
    "createdAt": "2026-10-14T03:12:40.000Z",
    "match": {
      "method": "GET",
      "host": "api.shop.example",
      "pathPattern": "/v3/search"
    },
    "minTier": 1
  },
  "credits": 0
}

Check now

POST /api/v1/projects/{projectId}/operations/{operationId}/check Project member

Replays both examples and records the result. Free, at most 10 an hour per operation.

Response: 200 OK

{
  "success": true,
  "data": {
    "outcome": "ok",
    "health": "ok",
    "ms": 702,
    "checkedAt": "2026-10-14T09:00:05.000Z"
  },
  "credits": 0
}

List events

GET /api/v1/projects/{projectId}/operations/{operationId}/events?page=1&pageSize=20 Project member

Checks, drift, repairs, approvals and rollbacks, newest first. Kept 90 days. Details never hold response bodies or secrets.

Response: 200 OK

{
  "success": true,
  "data": {
    "events": [
      {
        "id": "evt_9",
        "kind": "repair_proposed",
        "versionId": "ver_2",
        "detail": {
          "reason": "drift",
          "from": "/v2/search",
          "to": "/v3/search"
        },
        "createdAt": "2026-10-14T03:12:41.000Z"
      }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 20
  },
  "credits": 0
}

Call outcomes

Every call is judged from what came back, not the status code alone: a bot wall on a 200, HTML where JSON was expected or a missing field all count. Only ok and empty answer and are billed. Failures carry the class in reason and a hint in next.

reasonMeaning
okThe site answered with data.
emptyThe site answered with no rows for these arguments.
driftThe site changed its request or response. A repair starts in the background.
blockedThe site refused every tier the call could use.
rateToo many requests to this site right now.
inputThe examples still answer, so the site refused these arguments.
errorThe call failed for another reason.

Error body of a failed call

{
  "message": "The site refused every tier this call could use.",
  "reason": "blocked",
  "next": "Try proxy \"residential\", or allow tier 3."
}

A 402 means the balance could not cover the call. Requests to one site are spaced to 2 a second per project; a call waits up to 10 s for its turn, then fails with rate.

MCP tools

The Wayfern MCP server has the same operations as tools, for a token with the mcp scope and the same project membership. Learning and calls are billed as over REST.

ToolCostWhat it does
list_operationsFreeThe operations of your projects, with their parameters and health.
describe_operationFreeOne operation: parameters with examples, output fields and the output schema.
call_operation1 credit per call that answersCall an operation with operation_id and args. Lists come back as columns and rows, up to 20,000 characters.
learn_operation3 credits when it finds candidatesLearn a public page from a trigger URL and two examples per parameter. Returns ranked candidates with their outlines, so an agent can choose without reading bodies.
create_operationFreeCreate an operation from a learn: a chosen candidate and fields, or the best candidate as it is.

tools/call request

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "call_operation",
    "arguments": {
      "operation_id": "op_7Hx2mP",
      "args": {
        "query": "standing desk"
      }
    }
  }
}

Limits

Also listed with every other cap on the Limits page.

WhatLimitNotes
Parameters per operationUp to 10Each needs two different example values.
Trigger URL length2,048 characters
Learning3 page runs, 60 s in totalEach run waits for the network to go quiet, at most 20 s.
Learn draft kept30 minutesCreate the operation from it before then, or learn again.
Captured body per candidate2 MB
Operations per project200
Call output1 MB over RESTPast it the result is cut and truncated is true. MCP returns up to 20,000 characters.
Cache TTL0 to 24 hoursOff by default.
Requests to one site2 a second per project, 20 a second in totalA call waits up to 10 s for its turn, then fails with rate.
Repairs3 a day per operationOne at a time. Free.
Scheduled checksoff, 6 h, 12 h, 24 h, 72 hDefault every 24 h, only for operations called in the last 14 days. Free.
Check now10 an hour per operationFree.
Events kept90 days

Billing

Operations draw from the same credit balance as everything else, and charges go to the project owner. Every rate is also on Pricing & billing.

ItemCreditsNotes
Learning3 credits1 credit per page run, 3 runs. Charged only when learning returns candidates.
Call1 creditPer call that answers, at any tier, cache hits included. Failed calls are free.
Repairs and scheduled checksFreeOur maintenance. At most 3 repairs a day per operation.
Residential bandwidth320 per GBMetered per byte when a call or a learn uses a residential exit. Datacenter exits are free.
Challenge solve1 creditPer solved Turnstile or reCAPTCHA while running the page, when Solve challenges is on.

Privacy

  • No bodies are stored with an operation. Versions hold the request template, the fields and the output schema. Events hold short facts, never bodies or secrets.
  • Learn drafts last 30 minutes. They hold the candidate outlines and the least of the captured requests and responses needed to build the operation, with cookie and storage values removed and bodies capped at 2 MB. Then they are deleted.
  • No cookies or credentials are stored. Templates never contain Cookie, Authorization or other credential values. Cookies a site sets for an anonymous visitor stay in the warm browser page and end with it.
  • Results are cached only when you ask. The cache is off by default; with a TTL set, answers are kept for that long, at most 24 hours.