Call an API the way you call a function.

Web Function is a small convention for HTTP APIs. The path names the action. A JSON object carries the arguments. The response is the return value.

POST https://api.example.com/find-user-by

200

Request

{
  "email": "ada@example.com"
}

Response

{
  "id": "usr_18",
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}

200 means the body is the return value. The URL is only the function name.

What Web Function is

An endpoint is a function you invoke over HTTP. You POST to a path, send one JSON object, and read one JSON value back. A package is a JSON document that lists those functions, their arguments, their return types, and their docs, so a person, a client, or an agent can discover them.

POST
The only invocation method. The last segment of the path is the function name.
JSON in
The body is one object. Its keys are the arguments. Accept and Content-Type are application/json.
JSON out
The response may be any JSON value: object, array, number, string, boolean, or null.
Status
200 is the return value. 400 is a bad request. Any other status is unexpected, and the client surfaces it.

Auth, pagination, errors, events, versioning, deferral, and pipelining are optional flags on top of that core. You can ship with the four rules above and add a flag when you need it.

Who it's for

People designing an API

You name the action — find-user-by, send-receipt, cancel-order — and put the data in the body. The design conversation stays on what the function does.

People calling an API

curl, fetch, Postman, and a generated client all send the same POST. If you can send a JSON object, you can call the function before any SDK exists.

People running an API

Gateways, traces, and alerts already understand a path and a status code. Web Function uses that, so the platform you have keeps doing the work.

People building agents

A tool call is a name, a JSON input, and a JSON result. An endpoint is already that shape, so each function can be exposed as a tool directly.

What using it looks like

In the browser's network panel, the row is the function name. Click it and you get a JSON object in and a JSON value out, pretty-printed. The same call is what you send from curl, fetch, Postman, or a client.

Network

https://api.example.com/find-user-by

Name Status Type

Request

{
  "email": "ada@example.com"
}

Response

{
  "id": "usr_18",
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}

The row name is the last path segment, so the list stays readable as the API grows. The body is the return value.

The same call, anywhere

One POST. The tool you already have is enough.

$ curl -X POST https://api.example.com/find-user-by \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com"}'

The package is a list you can edit

Each endpoint carries its own arguments. You can read this in a diff and change it by hand. Publishing the description is serving the JSON. A repeated shape can be named and referenced as object.user when you want it; a small package stays a flat list.

{
  "base_url": "https://api.example.com",
  "endpoints": [
    {
      "name": "find-user-by",
      "docs": "Look up a user by email.",
      "flags": ["error_triple"],
      "arguments": [
        { "name": "email", "type": "string.email", "flags": ["required"] }
      ],
      "returns": "object",
      "attributes": [
        { "name": "id", "type": "string" },
        { "name": "name", "type": "string" },
        { "name": "email", "type": "string.email" }
      ]
    }
  ]
}

Why use it

The shape of the call is the feature. Clients, devtools, logs, gateways, and agents all get simpler because every function is a name, a JSON object, and a status code.

  1. 01

    Devtools show the function

    The network row is the last path segment, so a long list of calls stays scannable. Open a row and the preview is pretty-printed JSON on both sides: the arguments you sent, and the value you got back. Debugging a call is reading it.

  2. 02

    Arguments stay out of the URL

    Emails, ids, and filters travel in the body. They stay out of the address bar, browser history, and the Referer header on the next navigation. The URL identifies the function. A bearer token, when you use one, travels in the Authorization header.

  3. 03

    Logs and alerts are status codes

    Group by endpoint name, then split on status. 200 returned a value. 400 was a bad request from the caller. Any other 4xx or 5xx — including a 401 or 429 added by a gateway — is unexpected, and clients surface that code. A targeted alert is a filter on those two fields.

  4. 04

    Your gateway already understands it

    A call is an ordinary POST of application/json to a stable path. URL rewrites, internal dispatch, body-size limits, and per-path rate limiting apply directly. The edge does its job with the HTTP it already speaks.

  5. 05

    A client is a thin loop

    Every API shares the wire format, so a client reads the package, POSTs to base_url plus the endpoint name, and branches on status. That is small enough to write by hand, and small enough to generate. Official clients cover Ruby, Go, PHP, JavaScript and TypeScript, C#, Python, and Java.

  6. 06

    Agents map one endpoint to one tool

    find-user-by is the tool name. The package arguments are the tool input. The 200 body is the tool result. The action is already in the name, which is the shape tool calling expects.

  7. 07

    Packages are written by hand

    Endpoints sit in a list, arguments sit on the endpoint, and you can review the file in git. Describing the API is editing JSON and serving it. There is no compile step between the document and the API.

  8. 08

    Notifications are a stream of events

    When something happens live, the package points at a Server-Sent Events URL. Each message is a JSON object with an event name. The client receives the event and acts on it. Live updates do not require a state-sync protocol. A stream URL may carry a token, because browser EventSource cannot set an Authorization header.

    const source = new EventSource(eventSourceUrl)
    
    source.onmessage = (message) => {
      const data = JSON.parse(message.data)
      // data.event is the name; the other keys are the payload
    }

Clients and the CLI

One package feeds the clients, the generator, and the converters. github.com/webfunction-protocol publishes a client for each of these languages:

wfn validates a package, generates a typed client, and converts the same document to OpenAPI or a Postman collection.

wfn validate --url https://api.example.com/package -o report.json
wfn codegen --target ruby --url https://api.example.com/package -o client.rb
wfn convert --target openapi --url https://api.example.com/package -o openapi.json
wfn convert --target postman --url https://api.example.com/package -o collection.json

Codegen targets: ruby, go, php, js, csharp, python, java. Convert targets: openapi, postman.

The specifications

Start with the two documents that define a call and its description. The rest are flags you opt into.

Also: Authentication, Pagination, Error, Event, Versioning, Deferral, Pipelining, Generics.