Canopy

POST /api/quote

Most callers should use canopy.check(url) instead. This page is for SDK builders and developers calling the wire protocol directly.

POST /api/quote performs a server-side probe of a paywalled URL, parses the resulting 402 (x402 or MPP), runs the agent's policy in dry-run mode, and returns the parsed offer plus an allowed / pending_approval / denied verdict. Never signs. Cached per (org, url, method) for 60 seconds.

Base URL: https://trycanopy.ai

Request

POST /api/quote
Authorization: Bearer <apiKey>
Content-Type: application/json

Body

FieldTypeDescription
urlstringResource URL to probe. https:// (or http:// in dev). Loopback / RFC1918 hosts are rejected.
agent_idstringThe agent ID (agt_…) whose policy should evaluate the offer.
method"GET" | "POST"Optional. HTTP method to probe with. Defaults to GET; falls back to POST on 405.

Common response fields

Every successful response (200, 202, or 403) carries the parsed offer:

FieldTypeDescription
status"allowed" | "pending_approval" | "denied"Policy verdict in dry-run mode.
rail"x402" | "mpp"Which payment protocol the URL uses.
chain_idinteger8453 for Base, 4217 for Tempo.
amount_usdnumberPrice parsed from the 402 challenge.
recipient.addressstringOn-chain recipient (server-derived from offer / challenge).
recipient.slugstring | nullService slug from the registry, when the realm is registered.
recipient.namestring | nullService name from the registry.
resource_urlstringThe probed URL, echoed for convenience.
schemestring | nullx402 only — typically "exact".
networkstringWire network identifier ("base", "tempo", "eip155:8453").
realmstring | nullRFC 7235 auth-realm from the MPP WWW-Authenticate; null on x402.
cachedbooleanTrue when the response came from the per-(org, url) 60s cache.

pending_approval adds reason: string and approval_threshold_usd: number | null. denied adds reason: string.

200 — allowed

{
  "status": "allowed",
  "rail": "x402",
  "chain_id": 8453,
  "amount_usd": 0.10,
  "recipient": {
    "address": "0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97",
    "slug": "stableenrich",
    "name": "StableEnrich"
  },
  "resource_url": "https://stableenrich.dev/api/apollo/people-enrich",
  "scheme": "exact",
  "network": "base",
  "realm": null,
  "cached": false
}

202 — pending_approval

The amount exceeds the agent's approval threshold. Same offer fields plus:

{
  "status": "pending_approval",
  "reason": "Amount $7.50 exceeds approval threshold of $5",
  "approval_threshold_usd": 5,
  "rail": "...",
  "amount_usd": 7.50,
  "recipient": { "...": "..." }
}

403 — denied

The cap, allowlist, or registry-as-trust-boundary check failed. Same offer fields plus:

{
  "status": "denied",
  "reason": "Spend cap exceeded for this period",
  "rail": "...",
  "amount_usd": 25.00,
  "recipient": { "...": "..." }
}

400 — bad request

ReasonWhen
url is requiredMissing or empty url.
Invalid URLnew URL(url) throws.
URL is not allowed (private/loopback hosts blocked)Host is localhost, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0, *.local, or *.internal.
URL did not return a payment challenge (status N)Probe got a non-402 response.
URL returned 402 but no parseable x402 or MPP challenge.402 body / headers not in either format.
Could not reach URL: ...Network failure (DNS, TLS, timeout).
Cannot price ..., Unsupported x402 network ..., MPP method ... not supported yetRecognized challenge but Canopy can't price it.

401 / 404 / 503

StatusMeaning
401Invalid / revoked API key.
404Agent not found in this org.
503Org treasury not provisioned.

Example

curl --request POST \
  --url https://trycanopy.ai/api/quote \
  --header "Authorization: Bearer ak_live_xxxxxxxxxxxxxxxx" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://stableenrich.dev/api/apollo/people-enrich",
    "agent_id": "agt_xxxxxxxx"
  }'

A 200 response means the policy would have allowed the call had you signed it. Treasury balance is not checked by quotecanopy.fetch() (or /api/sign) is where the rail funding is verified at sign time.