EN

Production v3 API

Build with inspectable x402 evidence.

Query public evidence without authentication, or sign in with an Ethereum wallet to register and operate endpoints. Responses expose named observations—never a composite trust score.

Base URL https://ifandonlyif.io

01 · Quickstart

Query public evidence.

No API key is required for published evidence cards.

Read an illustrated evidence example

GET evidence by endpoint URL
curl -G "https://ifandonlyif.io/api/v3/evidence/check" \
  --data-urlencode \
  "url=https://api.example.com/paid-resource"
Two publication paths

observed cards can be public before an owner claims the endpoint, if it is active, not opted out and has a discovery observation. They carry an unverified-ownership label and a fixed disclaimer. First verified-tier publication needs ownership verification and at least three passing initial or scheduled observations. A 404 means no public card, not a safety verdict. Lookup reads saved evidence and starts no probe.

Submit for monitoring on the homepage calls POST /api/v3/discovery/submit with {"url":"https://api.example.com/paid"}. This public route needs discovery ingestion enabled and allows 10 submissions/minute per IP plus the general rate limit. A 202 with queued means a candidate was recorded (immediate enqueue is best-effort); known does not re-enqueue it. Neither guarantees monitoring or publication. The worker applies robots.txt, opt-out, eligibility and backoff rules first. The payment-network selector only compares observed options; it does not restrict what the worker validates.

After submission, the page polls GET /api/v3/discovery/status?url=… every five seconds for at most one minute, then loads the public card if available. This read-only route returns {"url":"…","status":"pending"}, optionally with retry_after_seconds. Other states: available, deferred, unavailable, unobserved, disabled. It is no-store, limited to 30/minute per IP. Check progress resumes reads without resubmitting. Delays, errors and unavailable results stop automatic checks. Rejection and opt-out details stay private.

A user-initiated lookup that returns 404 reads submission status once. Only a confirmed unobserved offers submission; pending or deferred work offers progress. A valid URL format is not monitoring eligibility. Lookup never submits, and submitting does not require claiming ownership.

Success envelope

200 application/json
{
  "success": true,
  "data": {
    "endpoint": {
      "id": "ENDPOINT_UUID",
      "url": "https://api.example.com/paid-resource",
      "name": "Example x402 API"
    },
    "operational_status": "passing",
    "ownership": {
      "status": "verified",
      "method": "iff-well-known"
    },
    "protocol": {
      "status": "pass",
      "version": 2,
      "payment_options": [],
      "check_codes": ["x402_v2_valid"]
    },
    "availability": [
      {
        "window": "24h",
        "samples": 288,
        "reachable_rate": 100,
        "protocol_pass_rate": 99.7,
        "p50_latency_ms": 84,
        "p95_latency_ms": 190
      }
    ],
    "freshness": {
      "status": "fresh",
      "observed_at": "2026-07-12T04:30:00Z",
      "interval_seconds": 300
    },
    "evidence": {
      "report_hash": "HEX_SHA256",
      "monitor_id": "iff-production-monitor",
      "monitor_version": "3.0.0",
      "monitor_signature_algorithm": "ML-DSA-65",
      "monitor_signature": "BASE64_ML_DSA_65_SIGNATURE",
      "monitor_public_key": "BASE64_ML_DSA_65_PUBLIC_KEY"
    }
  }
}

02 · Authentication

Sign in with a wallet.

Owner setup uses the API, not a wallet-connect dashboard. You need an Ethereum externally owned account (EOA) that can produce a 65-byte EIP-191 personal_sign signature, and deployment access to the endpoint's origin. Contract-wallet signatures (EIP-1271 / EIP-6492) are not supported. Owner routes use a short-lived access token issued after the login challenge.

  1. Request a challenge

    The challenge is domain, URI, chain, wallet, nonce, and expiry bound.

  2. Sign the exact message

    Pass the returned message unchanged to your wallet's personal_sign with the same address. Check the domain, wallet and expiry in the prompt. This is a login signature, not a payment or token approval. IFF never needs your private key or recovery phrase.

  3. Exchange and rotate

    Receive access_token, refresh_token, token_type and expires_in. Use the access token as $ACCESS_TOKEN below. Every refresh token is single-use: replace both tokens after refreshing.

1. Create challenge
curl "https://ifandonlyif.io/api/v3/auth/challenge" \
  -H "content-type: application/json" \
  -d '{"wallet_address":"0xYOUR_WALLET"}'
2. Verify signature · request shape
curl "https://ifandonlyif.io/api/v3/auth/verify" \
  -H "content-type: application/json" \
  -d '{
    "challenge_id":"CHALLENGE_UUID",
    "wallet_address":"0xYOUR_WALLET",
    "message":"EXACT_MESSAGE_FROM_SERVER",
    "signature":"0xSIGNATURE"
  }'

curl cannot sign; the signature comes from your wallet. The example is a request template. Submit the original challenge_id and message with the wallet's signature, building the body with a JSON serializer such as JSON.stringify({ challenge_id: challenge.challenge_id, wallet_address: wallet, message: challenge.message, signature }). Do not paste literal newlines into JSON or rebuild the message. Keep tokens out of URLs, screenshots, source code and shell history.

3. Rotate refresh token
curl "https://ifandonlyif.io/api/v3/auth/refresh" \
  -H "content-type: application/json" \
  -d '{"refresh_token":"REFRESH_TOKEN"}'

Challenges expire after five minutes and can be consumed once. Owner responses are served with Cache-Control: no-store.

03 · Ownership

Register and prove endpoint control.

Registration accepts only public HTTPS endpoints on port 443 without credentials, query strings, fragments or redirects. It creates a claim attempt, not owner access.

The response has a unique claim_attempt_id and an ownership object with challenge, expires_at, verification_url and document. The challenge lasts 24 hours. Any authenticated wallet may create an independent attempt; a pending attempt cannot read private observations, probe manually, manage webhooks, link ERC-8004 or unpublish.

Register endpoint
curl "https://ifandonlyif.io/api/v3/endpoints" \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "url":"https://api.example.com/paid-resource",
    "name":"Example x402 API",
    "description":"Payment-gated resource"
  }'

201 Created: new endpoint and claim attempt. 200 OK: new attempt on an existing endpoint. Save endpoint_id as ENDPOINT_UUID and keep claim_attempt_id for polling. Neither queues verification or grants access. Do not re-register while waiting.

For one endpoint, publish the response's ownership.document unchanged at ownership.verification_url, for example:

https://api.example.com/.well-known/iff-trust.json

Endpoints on the same origin share this file. Before adding one, back up the current document. Wrap an existing single claim as the first item of {"claims":[...]} and append the new ownership.document, or append to the existing array. Keep every other endpoint's claim and exact challenge. Never replace the file with the newest response; IFF does not merge it.

The legacy single-claim format remains supported. The multi-endpoint format has one top-level field, claims: 1–64 objects with exactly endpoint_id, owner_wallet and challenge, using the API's nonempty values (canonical lowercase UUID, Ethereum wallet address, exact challenge). Wallet comparison ignores case. The body must fit in 16 KiB. Unknown or duplicate JSON fields, duplicate endpoint IDs, mixed formats or any malformed entry reject the whole file. When renewing, replace only that endpoint's item. Keep valid claims online for periodic domain-control rechecks.

Queue ownership verification
curl -X POST \
  "https://ifandonlyif.io/api/v3/endpoints/ENDPOINT_UUID/verify-ownership" \
  -H "authorization: Bearer $ACCESS_TOKEN"

This verification request returns 202 Accepted: the job is queued, not yet verified. Follow Retry-After (currently 5 seconds), then poll with the same wallet's access token:

Check your ownership attempt
curl "https://ifandonlyif.io/api/v3/endpoints?page=1&page_size=20" \
  -H "authorization: Bearer $ACCESS_TOKEN"

Find your saved attempt ID in data.claim_attempts[].id (check later pages if needed) and read status, outcome_code and verification_failures. pending is not success; verified means proof passed; failed, expired and superseded are resolved. data.endpoints lists authoritative ownership. A later registration does not rotate another wallet's challenge. Only a successful same-origin proof replaces an owner; transfer disables the old webhooks and resets publication eligibility.

If an attempt stays pending after a failed check, fix the deployed file and queue verification again within 24 hours. For an expired or resolved attempt, register the URL again, publish the new document and queue the new attempt. Verification 409: no usable pending attempt. 503: queue unavailable, not a pass; retry with backoff. 401: refresh the token or log in again.

After ownership passes

Owner access and first publication are separate. A verified-tier card becomes public after ownership verification and at least three passing initial or scheduled observations; manual probes do not count. A public 404 during this wait is not an ownership failure. Diagnose with the owner-only history:

Read private observation history
curl "https://ifandonlyif.io/api/v3/endpoints/ENDPOINT_UUID/observations?page=1&page_size=20" \
  -H "authorization: Bearer $ACCESS_TOKEN"

Read data.observations and each probe_type. To request a private diagnostic, run the command below, then poll this history for a new manual observation. 202 only confirms queueing. A manual result never changes the public card, publication eligibility, manifests or notifications.

Queue a private diagnostic
curl -X POST \
  "https://ifandonlyif.io/api/v3/endpoints/ENDPOINT_UUID/probes" \
  -H "authorization: Bearer $ACCESS_TOKEN"

Next, set up change notifications. To stop monitoring and hide the card, use the owner-only action below. It also deactivates webhooks and pending deliveries, but keeps immutable observations, claim-attempt audit and transparency-log entries. A cached card can stay visible for up to 30 seconds. To resume, register the URL and prove control again.

Stop monitoring and unpublish · changes endpoint state
curl -X DELETE \
  "https://ifandonlyif.io/api/v3/endpoints/ENDPOINT_UUID" \
  -H "authorization: Bearer $ACCESS_TOKEN"

A successful stop returns 204 No Content. Owner management routes return 404 when the wallet has no current verified or stale claim (a pending attempt is not enough). With a stale claim, history and stopping work, but creating a webhook or manual probe returns 409 until ownership is verified again.

Evidence boundary

Ownership proves control of the monitored origin only. It does not prove who operates the backend, what happens after payment, or that the endpoint is safe.

04 · Reference

Endpoint reference.

Path parameters ending in :id are UUIDs. Pagination takes page and page_size up to 100. Every GET route also answers HEAD. Public routes and POST /api/v3/verify send Access-Control-Allow-Origin: *; authenticated routes do not.

Public and authentication

MethodPathPurpose
GET/healthLiveness plus coarse dependency booleans (no service name, version, or component labels).
GET/api/v3/evidence/check?url=…Published evidence card by normalized endpoint URL.
GET/api/v3/evidence/endpoints/:idPublished evidence card by endpoint ID.
GET/api/v3/evidence/manifests/:idImmutable daily evidence manifest with ETag, by manifest ID -- a separate id space from the endpoint ID above. Get a manifest ID from an evidence card's anchor.manifest_uri; passing an endpoint ID here 404s with a message that says so.
GET/api/v3/log/anchors/latestLatest submitted or confirmed ERC-8004 anchor for a signed transparency-log tree head.
POST/api/v3/auth/challengeCreate a one-time wallet challenge.
POST/api/v3/auth/verifyVerify EIP-191 signature and issue token pair.
POST/api/v3/auth/refreshRotate a single-use refresh token.
POST/api/v3/verifyPreflight-check a received x402 requirement against independent observation.
GET/api/v3/receipts/keysCurrent and retained public keys for the optional, experimental Service Receipt v2 (v1 keys listed as previous).
GET/api/v3/badge/:id.jsonshields.io endpoint-badge JSON for an endpoint's current requirement consistency.

Authenticated wallet and endpoint owner

Every route below requires Authorization: Bearer <access_token>. Registration, polling and verification are for the wallet that created the attempt; management routes need verified or stale ownership.

MethodPathPurpose
POST/api/v3/endpointsCreate an independent, non-authorizing claim attempt and ownership challenge.
GET/api/v3/endpointsList authoritative endpoints and this wallet's claim-attempt audit statuses.
DELETE/api/v3/endpoints/:idUnpublish an endpoint while retaining immutable evidence.
POST/api/v3/endpoints/:id/verify-ownershipQueue well-known ownership verification.
POST/api/v3/endpoints/:id/probesQueue an owner-only manual observation.
GET/api/v3/endpoints/:id/observationsRead paginated full observation history.
POST/api/v3/endpoints/:id/webhooksCreate an owner webhook for status or requirement changes; the secret is returned once.
GET/api/v3/endpoints/:id/webhooksList active targets without secrets.
DELETE/api/v3/endpoints/:id/webhooks/:webhookIdDeactivate a webhook and pending deliveries.
POST/api/v3/endpoints/:id/erc8004Link an owner-held ERC-8004 agent identity.

05 · Response model

Read dimensions independently.

ownership
Current domain-control status, method, and verification times.
protocol
Latest x402 parse result, version, named checks, and advertised payment options.
availability
24-hour and 7-day samples, percentage rates, and p50/p95 latency.
freshness
Whether the latest observation remains inside the publication freshness window.
evidence
Canonical report hash, monitor identity, ML-DSA-65 signature (older reports: Ed25519), and public key.
anchor
Optional daily manifest chain status and transaction provenance.

Rates are percentages from 0 to 100. Missing measurements are omitted or null; do not infer a pass.

06 · Webhooks

Verify every delivery.

Owner subscriptions receive endpoint.status_changed when derived operational status changes and endpoint.requirement_changed when eligible observations show a different payment requirement fingerprint (not proof of an attack). Manual probes trigger neither; an observed-tier card grants no subscription rights.

Create subscription
curl "https://ifandonlyif.io/api/v3/endpoints/ENDPOINT_UUID/webhooks" \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H "content-type: application/json" \
  -d '{"target_url":"https://hooks.example.com/iff"}'

Creation needs verified ownership and returns 201 Created with id, target_url and secret. Save the ID as WEBHOOK_UUID and store the secret on your receiver's server; it is shown once and not recoverable. Targets must be public HTTPS/443 without credentials, query strings, fragments or redirects. A duplicate active target or more than 10 active subscriptions returns 409.

Creating a subscription sends no test event. Events follow changes in eligible observations, not each probe. Validate the headers below before relying on notifications.

X-IFF-Timestamp: 1700000000 X-IFF-Signature: sha256=<hex digest> Idempotency-Key: <delivery uuid>
  1. Reject timestamps outside your allowed clock-skew window.
  2. Compute HMAC-SHA256 over timestamp + "." + raw_body using the one-time secret.
  3. Compare the hex digest in constant time.
  4. Deduplicate by Idempotency-Key; repeated deliveries should still receive 2xx.
List active subscriptions · no secrets returned
curl "https://ifandonlyif.io/api/v3/endpoints/ENDPOINT_UUID/webhooks" \
  -H "authorization: Bearer $ACCESS_TOKEN"
Stop one subscription · changes notification state
curl -X DELETE \
  "https://ifandonlyif.io/api/v3/endpoints/ENDPOINT_UUID/webhooks/WEBHOOK_UUID" \
  -H "authorization: Bearer $ACCESS_TOKEN"

The list is in data. Deleting returns 204 No Content and stops pending deliveries without unpublishing the endpoint. If the secret is lost, delete the subscription and create a new one. Never expose the secret in client-side code.

07 · Preflight

Check before you pay.

Compare a received x402 payment requirement with IFF's independent, signed observation before acting on it. It never probes the URL and stores only a normalized URL, and only when nothing has been observed. Verdicts are consistent, diverged, unobserved or stale: no composite score and no safety, delivery or trust claim.

Verify a requirement
curl "https://ifandonlyif.io/api/v3/verify" \
  -H "content-type: application/json" \
  -d '{
    "url":"https://api.example.com/v1/quote",
    "payment_required":{"x402Version":2,"accepts":[{
      "scheme":"exact",
      "network":"eip155:8453",
      "asset":"0xab12cd34ab12cd34ab12cd34ab12cd34ab12cd34",
      "amount":"1000",
      "payTo":"0xef56ab78ef56ab78ef56ab78ef56ab78ef56ab78",
      "maxTimeoutSeconds":60
    }]}
  }'
TypeScript SDK
npm install @ifandonlyif/x402-preflight — use verify(), wrapFetch(), or the included MCP command. Read the SDK guide.
Direct API
Use POST /api/v3/verify from any language. No API key is required for public preflight checks.
Badge
![x402 evidence](https://img.shields.io/endpoint?url=https://ifandonlyif.io/api/v3/badge/ENDPOINT_UUID.json)

Experimental capability showcase: Service Receipt v2

Service Receipt v2 is an optional signed artifact for one verification result, outside the core preflight contract. It binds the response, verification-request digest and a five-minute action window under IFF's dedicated ML-DSA-65 receipt key. Omitting receipt or sending null leaves the response unchanged; {} or "version":"2" issues v2, "1" returns 422. Existing v1 receipts still verify.

Opt in to the experimental receipt
"receipt":{"version":"2","nonce":"checkout_7f3a"}
Signed artifact
A successful opt-in adds service_receipt without changing the other response fields.
Local verifier
Paste the standalone envelope or full API response into the receipt showcase. Verification runs locally in the browser.
Discover current and previous receipt keys
curl "https://ifandonlyif.io/api/v3/receipts/keys"
Not an x402 payment receipt

A Service Receipt does not prove payment, service delivery, caller identity, remote execution, result correctness, or safety. It records that a receipt key signed the enclosed result.

Keep verification checks separate

A valid ML-DSA-65 (v2) or Ed25519 (v1) signature shows the embedded key signed the receipt (provenance, not attestation); it does not show the key belongs to the claimed issuer. A same-origin HTTPS key-directory match is recognized; the strongest policy also matches the exact issuer and an independently pinned full sha256: fingerprint. The decoded signed subject is authoritative; compare it with the surrounding response. Referenced evidence stays unverified until its own proof is checked. IFF currently sets compute_proof to null.

08 · Errors & limits

Fail without inferring evidence.

Error envelope
{
  "error": true,
  "message": "human-readable reason"
}
StatusMeaning
400Malformed JSON, UUID, URL, pagination, or input shape.
401Missing, invalid, expired, replayed, or wrong token type.
403The authenticated wallet does not own the requested resource.
404No public evidence or requested owner resource exists.
409Ownership state, duplicate target, or resource state conflicts.
422Invalid x402 requirement, receipt version, or receipt nonce.
429Rate limit reached; retry with backoff.
503Evidence repository, queue, registry, receipt issuance, or another dependency is unavailable.
Security assumptions

IFF is an external observer. It runs no paid canary transactions, makes no payment-safety claim, guarantees no delivery after payment and does not decide whether an endpoint is trustworthy. Monitor signatures show provenance and tamper evidence, not attestation.