Skip to content

The HTTP interface

An Airnode is two endpoints, and it describes both itself. This is the pull model: a consumer asks and the Airnode answers, and a polling module that pushes on a timer does not exist yet.

Every response allows any origin (Access-Control-Allow-Origin: *). An Airnode holds no cookies or caller identity a cross-origin read could leak, and a browser running an agent is a consumer like any other.

GET / describes the Airnode

GET / returns an OpenAPI 3.1 document for the Airnode itself, so an agent that already speaks OpenAPI needs nothing bespoke to call it. Discovery is free and machine-readable. The document is titled after the API it wraps, lists the Airnode's own URL under servers, and covers both endpoints.

Every operation the Airnode serves appears as one accepted shape of the POST / request body, as a oneOf of variants. A variant pins the operation field to one operationId with a const and declares that operation's parameters as a strict object: the required ones are listed and unknown properties are refused. Each parameter is published exactly as the upstream's document declared it, nesting and all, so a caller can tell which parameters go with which operation without a separate lookup. A parameter that takes a closed set publishes it as an enum. A caller then reads the values from the schema rather than from a remark in the description.

The shape every response takes is published under components.schemas.Attestation, and everything a verifier needs sits under the x-airnode extension:

json
"x-airnode": {
  "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "version": "0.1.0",
  "attestation": {
    "scheme": "eip191-secp256k1",
    "digest": "keccak256(abi.encodePacked(bytes32 requestHash, uint256 timestamp, bytes dataBytes))",
    "dataBytes": "UTF-8 of data, or of its compact JSON when data is not a string",
    "requestHash": "keccak256(utf8(JSON.stringify([operation, parameterEntriesSortedByName]))), where an object value is itself entries sorted by name, at any depth, and an array keeps its order",
    "verify": "Recover the signer of the signature over the digest and check it equals the address above."
  }
}

version is the Airnode build serving the document, which is not the same thing as info.version above: that one is the wrapped API's own. It is there so a consumer that read the document once and has been calling against it since can tell that what it is calling changed.

What the document leaves out matters as much as what it contains: the upstream URL and the way parameters map onto it never leave the process. Publishing them would let a consumer skip the Airnode and call the provider's API directly.

POST / performs an operation

The body names an operation and its parameters:

json
{ "operation": "coinPrice", "parameters": { "ids": "ethereum", "vs_currencies": "usd" } }

The response is the upstream data plus the attestation over it. data carries the upstream body as JSON when the upstream answered JSON, and as text otherwise:

json
{
  "airnode": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "requestHash": "0x…",
  "timestamp": "1752580000",
  "data": { "ethereum": { "usd": 1920.51 } },
  "signature": "0x…"
}

An unknown operation is a 404, a missing, unknown or mistyped parameter is a 400, and an upstream failure is a 502. An upstream response too large to hold and sign is a 502 as well, since the whole body is signed rather than streamed; the limit is 50 MB by default. Unknown parameters are rejected rather than dropped, so a caller mistyping a name hears about it instead of silently receiving an attestation over a request they did not mean to make. A value outside a declared enum is not one of these. The Airnode publishes the set but does not enforce it, so such a value reaches the upstream, which answers it.

Ask for only the fields you need

A response is often far larger than the part you want, and an agent pays for every token of it. responseProjection names the fields to keep:

json
{
  "operation": "coinPrice",
  "parameters": { "ids": "ethereum", "vs_currencies": "usd" },
  "responseProjection": { "price": "/ethereum/usd" }
}

Each value is an RFC 6901 JSON Pointer into the upstream response, and each key is a name you choose. data carries those names instead of the whole body:

json
"data": { "price": 1920.51 }

The Airnode still fetches the whole response, so a projection saves context and not upstream latency. The projection is part of the signed request: requestHash covers it, so the attestation states which fields the caller asked for and what came back, and a projected response cannot pass for an unprojected one. Trimming the response yourself after the call leaves you holding data the signature no longer covers.

A pointer the response does not have is a 400 rather than a null, so a typo is never signed. A pointer that is not a JSON Pointer at all is a 400 before the upstream is called, so it costs nothing.

Payments

An operation can be priced with x402, which fits the free GET / and paid POST / split exactly: discovery is free, the resource is paid. Pricing is opt-in per deployment. An Airnode that names no price for an operation serves it free, exactly as before, so a free Airnode is unaffected by any of this.

A priced operation answers an unpaid POST / with 402 and a body of payment requirements:

json
{
  "x402Version": 1,
  "error": "Payment required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "1000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x…",
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ]
}

The caller signs an EIP-3009 transferWithAuthorization matching one entry, base64-encodes the signed payload, and retries with it in the X-PAYMENT header. The Airnode has a facilitator verify the authorization, performs the operation, then has the facilitator settle it on-chain, and returns the attestation with the settlement receipt in the X-PAYMENT-RESPONSE header. The Airnode never touches the chain itself: an x402 facilitator does the verifying and submitting and pays the gas. Settlement is USDC on Base.

Verification happens before the upstream is called and settlement only after it succeeds, so a call that fails upstream is never charged, and a caller who is charged always gets the data. Prices are per operation: an unpaid POST / reads the requested operation from the body and returns requirements for that operation's price, so the single shared POST / URL is not a limitation. Every price an Airnode charges is also published under x-airnode.payment in GET /, so an agent learns what a call costs from the same document that tells it how to make one.

The one deployment detail that matters: a facilitator that settles on Base has to be named, such as Coinbase's CDP facilitator. There is no default, so an Airnode is never left charging callers through a facilitator the operator did not choose.