Skip to main content

Getting Started

You have an airnode URL and want to get signed data. This walkthrough takes you from zero to a verified response.

Step 1: Check if the airnode is running

curl http://airnode.example.com/health
{
"status": "ok",
"airnode": "0xd1e98F3Ac20DA5e4da874723517c914a31b0e857"
}

Save the airnode address. You need it to verify signatures.

Step 2: Verify the operator

Before trusting an airnode, verify that it is operated by the API provider — not a third party. Use DNS identity verification (ERC-7529) to confirm the airnode address is associated with the API provider's domain. A first-party airnode (operated by the data source) provides the strongest trust guarantees. See the Trust Model for why this matters.

Step 3: Find your endpoint ID

The airnode operator provides the endpoint ID for each API route. It is a bytes32 hash derived from the endpoint's API specification (URL, path, method, non-secret parameters, and encoding). The operator's documentation lists available endpoint IDs and their expected parameters.

Step 4: Make a request

Call the endpoint with parameters in the JSON body.

curl -X POST http://airnode.example.com/endpoints/0x1c3e0fa5ac82e5514e0e9abac98e0b8e6c58b7bea12ae0393e4e4abe64ab9620 \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-key" \
-d '{"parameters":{"ids":"ethereum","vs_currencies":"usd"}}'

The response contains the data and a signature:

{
"airnode": "0xd1e98F3Ac20DA5e4da874723517c914a31b0e857",
"endpointId": "0x1c3e0fa5ac82e5514e0e9abac98e0b8e6c58b7bea12ae0393e4e4abe64ab9620",
"timestamp": 1700000000,
"data": "0x0000000000000000000000000000000000000000000000d8e29e69b3e1e80000",
"signature": "0x3a4e...signed"
}

If the endpoint has no encoding configured, you get rawData instead of data:

{
"airnode": "0xd1e98F3Ac20DA5e4da874723517c914a31b0e857",
"endpointId": "0x1c3e0fa5ac82e5514e0e9abac98e0b8e6c58b7bea12ae0393e4e4abe64ab9620",
"timestamp": 1700000000,
"rawData": { "ethereum": { "usd": 3842.17 } },
"signature": "0x3a4e...signed"
}

Step 5: Verify the signature

Recover the signer address and compare it to the airnode address from /health.

import { recoverAddress, hashMessage, keccak256, encodePacked, type Hex } from 'viem';

const endpointId = '0x1c3e0fa5ac82e5514e0e9abac98e0b8e6c58b7bea12ae0393e4e4abe64ab9620' as Hex;
const timestamp = 1700000000n;
const data = '0x0000000000000000000000000000000000000000000000d8e29e69b3e1e80000' as Hex;
const signature = '0x3a4e...signed' as Hex;
const expectedAirnode = '0xd1e98F3Ac20DA5e4da874723517c914a31b0e857';

// Reconstruct the message hash
const messageHash = keccak256(encodePacked(['bytes32', 'uint256', 'bytes'], [endpointId, timestamp, data]));

// Recover the signer
const recovered = await recoverAddress({
hash: hashMessage({ raw: messageHash }),
signature,
});

if (recovered.toLowerCase() !== expectedAirnode.toLowerCase()) {
throw new Error('Signature verification failed');
}

For raw responses, hash the JSON before verifying. You must use stable (sorted-key) JSON serialization to match what the airnode produces:

import { keccak256, toHex } from 'viem';

// Stable stringify: sorts object keys alphabetically at every level
function stableStringify(value: unknown): string {
if (value === undefined) return 'null';
if (value === null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map((v) => stableStringify(v)).join(',')}]`;
const sorted = Object.entries(value as Record<string, unknown>)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
return `{${sorted.join(',')}}`;
}

const rawDataHash = keccak256(toHex(stableStringify(rawData)));
// Use rawDataHash in place of `data` in the encodePacked call above

Step 6: Decode the data

Encoded responses contain ABI-encoded values. Decode them with viem.

import { decodeAbiParameters } from 'viem';

// For an int256 value (e.g., price with 18 decimals)
const [value] = decodeAbiParameters([{ type: 'int256' }], data);

// Convert from 18 decimals to human-readable
const price = Number(value) / 1e18;
console.log(`ETH price: $${price}`); // ETH price: $3842.17

Raw responses need no decoding. Access the JSON directly:

const ethPrice = rawData.ethereum.usd; // 3842.17

You're done (if you only need the value off-chain)

At this point you have:

  • A verified airnode address (proves who signed it).
  • A decoded value (the price, the temperature, whatever the endpoint serves).
  • A timestamp (so you can decide if the value is fresh enough for your use case).

That's the full off-chain consumer loop. Use the value in your app, run your own staleness check (Date.now() / 1000 - timestamp < maxAgeSeconds), and store or forward the signed payload if you want to prove provenance to a downstream system later — (airnode, endpointId, timestamp, data, signature) is a self-contained attestation that anyone can re-verify.

Step 7: Submit on-chain (optional)

If you instead want a smart contract to consume the data, hand (airnode, endpointId, timestamp, data, signature) to AirnodeVerifier.verifyAndFulfill(...). The verifier checks the signature and forwards to your callback. See On-Chain Integration for contract examples.

Choosing encoding at request time

Some endpoints mark one or more encoding fields with the wildcard '*', letting the client choose them per request. Supply the matching reserved parameter in the request body for each '*' field:

{
"parameters": {
"ids": "ethereum",
"vs_currencies": "usd",
"_type": "int256",
"_path": "$.ethereum.usd",
"_times": "1e18"
}
}
  • _type -- the Solidity ABI type to encode as (int256, uint256, bool, bytes32, address, string, bytes). Only consumed when the operator set encoding.type: '*'.
  • _path -- JSONPath expression to extract from the upstream response. Only consumed when encoding.path: '*'.
  • _times -- multiplier applied before encoding (numeric types only). Only consumed when encoding.times: '*'.

If a wildcard field's matching reserved parameter is missing, the server returns 400. If the operator pinned a field (concrete value rather than '*'), any client-supplied reserved parameter for it is silently ignored — the operator's value wins. Endpoints with no encoding block at all return raw JSON; reserved parameters cannot synthesize encoding out of nothing.

What can go wrong

StatusErrorWhat to do
400Missing required parameter(s): XAdd the missing parameters to your request body.
400Endpoint requires `_type` request parameterThe operator marked type: '*'. Supply _type in parameters.
400Endpoint requires `_path` request parameterThe operator marked path: '*'. Supply _path in parameters.
400Endpoint requires `_times` request parameterThe operator marked times: '*'. Supply _times in parameters.
401Missing X-Api-Key headerThe endpoint requires authentication. Add X-Api-Key: your-key to the request.
401Invalid API keyThe key value is wrong. Check with the airnode operator.
404Endpoint not foundThe endpoint ID is incorrect. Verify the ID with the operator.
413Request body too largeThe request body exceeds 64KB. Reduce the payload size.
415Content-Type must be application/jsonSet Content-Type: application/json.
429Too Many RequestsWait and retry. The airnode has a request rate limit configured.
502API call failedThe upstream API is unreachable or returning errors. Try again later.
502No value found at path: $.XThe upstream response shape changed or the path is wrong. Contact the operator.