> ## Documentation Index
> Fetch the complete documentation index at: https://pulkit-fix-docs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Verification API

> Reference for verifyHumanProofRequest, checkAttestation, and the Attestations API endpoints used to verify an agent's pairing.

Reference for what the two paths in [Verify an Agent](/agents/verify-human-agent-pairing) call. The server helper path uses `verifyHumanProofRequest`. The direct API path calls the REST endpoints below itself. `checkAttestation` is a lookup for a DID you already trust.

Checked against `@billionsnetwork/x402-human-proof-server` 0.1.9 and `@billionsnetwork/x402-human-proof-client` 0.1.7. Examples query production. For testnet URLs, see [Environments](/agents/environments).

<Warning>
  This page covers the verification slice of the Attestations API only. The API is internal infrastructure, not a general-purpose public API, and every endpoint is rate-limited. If your integration needs a higher limit, contact the Billions integration team.
</Warning>

## verifyHumanProofRequest

`@billionsnetwork/x402-human-proof-server`. Verifies a signed human proof, derives the agent DID from the signer, and looks up the DID's ownership attestation and the human's nullifier through the API. It does not need x402: you can call it from any server, with no payment or facilitator involved.

```javascript theme={null}
import { verifyHumanProofRequest, createPoUVerifier } from "@billionsnetwork/x402-human-proof-server";

const verifier = createPoUVerifier({
  attestationsApiBaseUrl: "https://attestations-api.billions.network/api/v1/attestations",
  nullifierApiBaseUrl: "https://attestations-api.billions.network/api/v1/nullifier",
});

const result = await verifyHumanProofRequest(verifier, {
  resource: "https://rewards.example.com/claim",
  humanProofHeader,
});
```

**Arguments**

| Name | Type | Description |
| - | - | - |
| `verifier` | `PoUVerifier` | From `createPoUVerifier`, below. |
| `resource` | `string` | The resource the proof must be signed for. The signed `uri` must equal it. |
| `humanProofHeader` | `string \| null` | The JSON proof, from the `HUMAN-PROOF` header. |
| `onEvent` | `(event) => void` | Optional. Called with `human_verified` or `human_not_registered`. |

**Returns** when the proof is valid and the DID is paired:

```javascript theme={null}
{
  allowed: true,
  did: "did:iden3:billions:main:…",   // agent DID, derived from the signer
  chainId: "eip155:84532",             // from the signed proof
  resolution: {
    humanId: "3217…3818",              // the human's nullifier
    verifiedAt: "2026-03-05T05:24:23.000Z",
    attestationId: "0xe22e…ab8b",
    humanDid: "did:iden3:billions:main:…",
    humanAddress: "0x…",
    agentDid: "did:iden3:billions:main:…",
    agentAddress: "0x…",
  },
}
```

`humanAddress` is the attestation's `fromEthereumAddress`. For pairings made with the Verified Agent Identity skill, that is the relay that submitted the transaction, not the person's wallet. Identify the person by `humanDid` or `humanId`.

**Returns** otherwise: `{ allowed: false, reason, did? }`. `did` is present for `not_registered`.

| `reason` | Meaning |
| - | - |
| `missing_header` | No proof was passed |
| `invalid_header` | The proof isn't valid JSON, or is missing `address`, `signature`, `chainId` or `type` |
| `invalid_message` | Missing `domain`, `version`, `nonce` or `issuedAt` |
| `resource_mismatch` | The signed `uri` is missing or doesn't match `resource` |
| `invalid_issued_at` | `issuedAt` isn't a parseable date |
| `invalid_expiration_time` | `expirationTime` isn't a parseable date |
| `message_expired` | `expirationTime` has passed |
| `invalid_signature_type` | `type` is neither `eip191` nor `ed25519`. Smart-contract wallet proofs (`eip1271`) end up here. |
| `invalid_signature` | Signature recovery failed, or the recovered address doesn't match the claimed one |
| `not_registered` | The derived DID has no ownership attestation, or no nullifier resolves for its human |

Treat every `reason` as "not verified". Branch on it only to show the agent a more useful error.

**Throws** when the Attestations or nullifier API returns an error status or can't be reached. Catch it and treat it as "not verified", or retry later.

**Does not check**: whether the nonce was issued by you or used before, and the signed `domain`. Do both yourself, as in [Path A, Step 3](/agents/verify-human-agent-pairing#path-a-server-helper).

### createPoUVerifier

| Option | Default | Description |
| - | - | - |
| `attestationsApiBaseUrl` | `https://attestations-api.billions.network/api/v1/attestations` | Where ownership attestations are looked up. |
| `nullifierApiBaseUrl` | `https://attestations-api.billions.network/api/v1/nullifier` | Where nullifiers are resolved. |
| `agentOwnershipSchema` | `0xca354bee…2911` (`AgentOwnership`) | The schema that counts as a pairing. |
| `hasAttestation` | API lookup | Override the check for additional schemas. |

The lookup takes the most recent ownership attestation for the DID. The API leaves revoked attestations out. Expiry is not checked.

## checkAttestation

`@billionsnetwork/x402-human-proof-client`. Returns whether a DID holds at least one attestation for a schema.

```javascript theme={null}
import { checkAttestation, DEFAULT_AGENT_OWNERSHIP_SCHEMA } from "@billionsnetwork/x402-human-proof-client";

const paired = await checkAttestation(agentDid, DEFAULT_AGENT_OWNERSHIP_SCHEMA, {
  attestationsApiBaseUrl: "https://attestations-api.billions.network/api/v1/attestations",
});
```

Returns `true` or `false`. Revoked attestations are not counted.

<Warning>
  This is a lookup, not a verification path. It does not show that the caller controls the DID. Only call it with a DID your own signature check produced, never with a DID an agent reported about itself.
</Warning>

<Note>
  `checkAttestation` returns `false` when the API responds with an error status. A `false` can mean "not paired" or "API unavailable". If you need to tell them apart, call the REST endpoint below directly.
</Note>

## REST endpoints

Base URL: `https://attestations-api.billions.network/api/v1`. These answer the pairing question only. Pair them with your own proof of control, as in [Path B](/agents/verify-human-agent-pairing#path-b-direct-api).

### Find a DID's ownership attestation

```bash theme={null}
curl "https://attestations-api.billions.network/api/v1/attestations?schemaId=0xca354bee6dc5eded165461d15ccb13aceb6f77ebbb1fd3fe45aca686097f2911&recipientDid=did:iden3:billions:main:2VmAkXrihYaLjks4i8XQYu98VLgbUUoeobmDUJp4ZM"
```

Filter by `recipientEthereumAddress=<0x…>` instead of `recipientDid` when you start from a recovered signer address, as Path B does. A paired DID returns one item:

```json theme={null}
{
  "totalItems": 1,
  "uniqueItems": 1,
  "totalPages": 1,
  "pageNumber": 1,
  "pageSize": 10,
  "nextPage": null,
  "data": [
    {
      "id": "0xe22e0c8bdd9ed997d62f4dd8ed547a2e9e6f2a7e4416b6f303e3a7273207ab8b",
      "creationTime": 1772688263,
      "schemaInfo": {
        "index": "1",
        "id": "0xca354bee6dc5eded165461d15ccb13aceb6f77ebbb1fd3fe45aca686097f2911",
        "name": "AgentOwnership",
        "schemaSignatures": [{ "name": "data", "type": "bytes" }]
      },
      "fromName": "",
      "fromDid": "did:iden3:billions:main:2VsocCwupnsqT3xHyPTYrdkwjMBYiWuvwe2mRK8HK3",
      "fromId": "25418487188279101345594527843687782240092919663607313438873907510818025729",
      "fromEthereumAddress": "0xB3F5d3DD47F6ca17468898291491eBDA69a67797",
      "toName": "Jarvis",
      "toDid": "did:iden3:billions:main:2VmAkXrihYaLjks4i8XQYu98VLgbUUoeobmDUJp4ZM",
      "toId": "14336761652768509472823833665155878975354096967746601418028400374727094529",
      "toEthereumAddress": "0x9D90Ce9f1E4633576001Dc1A2406Fe3757870b44",
      "decodedDataJson": "[{\"name\":\"data\",\"type\":\"bytes\",\"signature\":\"bytes data\",\"value\":{\"name\":\"data\",\"type\":\"bytes\",\"value\":\"0x\"}}]"
    }
  ]
}
```

A DID with no pairing returns `200` with `"totalItems": 0` and `"data": []`. [Inspect Attestations](/agents/attestation-production-lookup#3-read-the-result) explains every field.

### Get one attestation

```bash theme={null}
curl "https://attestations-api.billions.network/api/v1/attestations/<attestation-id>"
```

Adds `txid`, `expirationTime`, `revocable`, `revoked`, `revocationTime` and `rawData` to the fields above, and returns revoked attestations too. An unknown ID returns `404` with `{"message":"Attestation not found"}`.

### All attestations for an identity

```bash theme={null}
curl "https://attestations-api.billions.network/api/v1/identities/<did-or-0x-address-or-iden3-id>/attestations"
```

Every attestation where the identity is the attester or the recipient, in the same paginated envelope plus an `identity` field. Pass an agent DID to find its human, or a human DID to find all of that person's agents. This is the lookup the Explorer runs.

### Resolve a nullifier

```bash theme={null}
curl "https://attestations-api.billions.network/api/v1/nullifier?userId=25418487188279101345594527843687782240092919663607313438873907510818025729"
```

```json theme={null}
{
  "userId": "25418487188279101345594527843687782240092919663607313438873907510818025729",
  "nullifier": "3217936374450070950907367963005573940584526403461189936317035691903800703818"
}
```

`userId` is the human's iden3 ID, the `fromId` of an ownership attestation. Both values are strings, because they are too large for a JavaScript number. A missing `userId` returns `400` with `{"message":"userId query parameter is required"}`, and a non-integer one returns `400` with `{"message":"userId must be a valid integer"}`.

## Missing, revoked and failed lookups

| Situation | What the API returns | What to do |
| - | - | - |
| The DID was never paired | `200`, `totalItems: 0` | Not verified. Point the agent's owner to [Pair an Agent](/agents/identity-skill). |
| The pairing was revoked | Left out of the list, so the same as never paired. `GET /attestations/<id>` still returns it, with `revoked: true`. | Not verified. |
| The attestation has expired | Still listed. The API does not filter on `expirationTime`. | Check `expirationTime` yourself if your schema sets one. `0` means it never expires. |
| The API is down or rate-limited | An error status. `503` and `504` have both been seen in practice. | Not verified. Retry with backoff. Never fall back to "verified". |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.