An agent's first visit, request by request (for developers) Free & Pro
The six HTTP requests a personal agent makes to shop at a store running Agent Adapt, with every field: discovery, the signed token request, DPoP, search, cart, checkout link.
Updated October 12, 2026
This is the signed-out path through a store running Agent Adapt: six requests from a cold start to a checkout link, with the fields the store actually reads and returns. It is the same sequence the plugin’s own test agent runs on every release. The developers page lists everything the store offers; this guide walks one path through it. Sign-in, the agent’s browser, operations and MCP are the next steps and are linked at the end.
Throughout, the store is https://store.example, the agent’s client_id is https://agent.example/agent.json, and the agent holds two private keys: its signing key, whose public half is in its JWKS, and a fresh DPoP key for this user at this store.
Before the first request: what the agent publishes
{
"client_id": "https://agent.example/agent.json",
"client_name": "Example Assistant",
"logo_uri": "https://agent.example/logo.png",
"jwks_uri": "https://agent.example/jwks.json",
"redirect_uris": ["https://agent.example/oauth/callback"],
"token_endpoint_auth_method": "private_key_jwt"
}
The store fetches this document and the JWKS over HTTPS when it first sees the agent, refuses private and loopback addresses, caps the size, follows only a few redirects, and caches both for up to a day. jwks_uri and every redirect URI must be on agent.example or a subdomain. Add "extensions": {"operations": {"version": 1}} to receive operations from a Pro store.
1. Discovery
GET https://store.example/.well-known/poppy.json
GET https://store.example/.well-known/oauth-authorization-server/agent-adapt
From the first, read auth.issuer and apis[]. From the second (RFC 8414, at the issuer’s path-inserted location), read token_endpoint. Both are public and cacheable for five minutes. Then fetch the OpenAPI document named in apis[]; its servers[0].url is the API base, https://store.example/wp-json/agent-adapt/v1.
2. Start a Session
One POST to the token endpoint, form-encoded, carrying two signed JWTs and a DPoP proof.
POST https://store.example/agent-adapt/oauth/token
Content-Type: application/x-www-form-urlencoded
DPoP: <proof, see below>
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<session assertion>
&client_id=https://agent.example/agent.json
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<client assertion>
The client assertion authenticates the agent (RFC 7523 client authentication). Header {"alg":"ES256","typ":"JWT","kid":"<key id>"}; payload:
{ "iss": "https://agent.example/agent.json", "sub": "https://agent.example/agent.json",
"aud": "https://store.example/agent-adapt/oauth/token",
"iat": 1760270400, "exp": 1760270460, "jti": "<128 random bits>" }
The session assertion says which user this Session is for. Same header; payload:
{ "iss": "https://agent.example/agent.json", "sub": "<opaque User ID for this user at this store>",
"aud": "https://store.example/agent-adapt/oauth/token",
"iat": 1760270400, "exp": 1760270460, "jti": "<128 random bits>" }
Rules the store enforces: aud is the token endpoint, as a single string; iss equals the authenticated client_id; the JWT is signed with a key from the agent’s JWKS (an embedded key is ignored); ES256 or RS256 only; at most 300 seconds old; each jti accepted once. The User ID in sub should be random per user per store and must not derive from personal data; the store stores only its hash.
The DPoP proof (RFC 9449) is a third JWT, signed with the DPoP key, header {"alg":"ES256","typ":"dpop+jwt","jwk":{<public DPoP key>}}, payload:
{ "jti": "<random>", "htm": "POST", "htu": "https://store.example/agent-adapt/oauth/token", "iat": 1760270400 }
Response (Cache-Control: no-store):
{ "access_token": "<opaque>", "token_type": "DPoP", "expires_in": 3600,
"scope": "", "session_id": "ses_…", "signed_in": false }
Keep session_id. To renew the Session later, repeat the request with session_id=ses_…; a different User ID or client for that Session gets invalid_session. Session Tokens last an hour; the Session itself lasts 30 days from its last use.
Errors come back as {"error": "...", "error_description": "..."}: invalid_client for an agent the store cannot verify or has blocked, invalid_grant for a bad assertion, invalid_dpop_proof for a bad proof, rate_limited with Retry-After at 120 token requests a minute per address.
3. Search
Every API call carries the token and a fresh proof. The proof now includes ath, the base64url SHA-256 of the access token, and htu is the exact URL being called, without the query string.
GET https://store.example/wp-json/agent-adapt/v1/catalog/search?q=stormline+jacket
Authorization: DPoP <access_token>
DPoP: <proof with htm GET, htu .../catalog/search, ath>
Response:
{ "products": [ {
"id": 128, "name": "Stormline Insulated Jacket", "type": "variable", "sku": "SL-INS",
"url": "https://store.example/product/stormline-insulated-jacket/",
"summary": "…", "price": {"amount": "149.00", "currency": "GBP"},
"regular_price": {"amount": "149.00", "currency": "GBP"}, "sale_price": null,
"price_range": {"min": {"amount": "149.00", "currency": "GBP"}, "max": {"amount": "169.00", "currency": "GBP"}},
"on_sale": false, "in_stock": true, "purchasable": true, "has_options": true,
"low_stock_remaining": null, "image": "https://store.example/…/jacket.jpg",
"categories": [ {"id": 12, "name": "Jackets", "slug": "jackets"} ]
} ], "page": 1, "per_page": 10 }
Each term in q is matched against product titles and SKUs. Optional parameters: in_stock, on_sale, page, per_page (up to 50) and orderby (relevance, price, date, popularity, rating). Money is always {amount, currency} with the amount as a decimal string. GET /catalog/products/128 returns the full product, adding description, attributes and variations. Hidden products are absent from search and return 404 by ID. GET responses carry an ETag and honour If-None-Match.
Without a valid token the store answers 401 with WWW-Authenticate: DPoP error="invalid_token", algs="ES256 RS256". A Bearer scheme, a missing proof, a proof signed by a different key, a wrong ath or a wrong htu are each refused; a token in the query string is refused outright.
4. Build the cart
POST https://store.example/wp-json/agent-adapt/v1/cart/items
Authorization: DPoP <access_token>
DPoP: <proof>
Content-Type: application/json
{ "product_id": 131, "quantity": 2 }
product_id is a product or variation ID. For a variation with "any" attributes, add "attributes": {"Size": "M"}. The store answers 201 with the whole cart:
{ "items": [ { "key": "<line key>", … } ], "coupons": [],
"totals": { "subtotal": {…}, "discount": {…}, "tax": {…}, "shipping": {…}, "total": {"amount": "298.00", "currency": "GBP"} } }
This is a real WooCommerce cart for the Session, so prices, tax and stock rules are the store’s. Adding a hidden or out-of-stock product fails. Change a line with PATCH /cart/items/{key} and {"quantity": 1}, remove it with DELETE /cart/items/{key}, apply a coupon with POST /cart/coupons and {"code": "…"}, and read the cart at any time with GET /cart.
5. Hand over for checkout
POST https://store.example/wp-json/agent-adapt/v1/cart/handoff
Authorization: DPoP <access_token>
DPoP: <proof>
{ "url": "https://store.example/agent-adapt/cart/<token>", "expires_at": "2026-10-12T12:30:00+00:00" }
Give the URL to the user to open in their own browser. It is single-use and lasts 30 minutes; it adds the Session’s items and coupons to the browser’s own cart and lands on checkout, where the user pays. It carries no account access. An empty cart gets 409 cart_empty. The resulting order records the agent in WooCommerce’s order attribution.
This handoff is advertised in poppy.json as the extension getagentadapt.com/checkout-handoff (version 1), because Draft 0.1 of the protocol leaves payments open.
6. Renew, and sign out
Before the hour is up, repeat step 2 with session_id to get a fresh token for the same Session. To end it, POST the token to https://store.example/agent-adapt/oauth/revoke (RFC 7009) with the same client authentication.
What comes next
- Signing the user in for orders, reordering and addresses:
authorization_endpointwith PKCE S256 andsession_idat the code exchange, or the device flow. The consent page is the store’s own; the user can grant fewer scopes than asked, so readscopein the response. Administrator and shop-manager accounts are refused. - The agent’s browser: post a
poppy-browser+jwtassertion toweb.browser_session_endpointas a form body and the browser joins the Session’s cart. - Operations (Pro): proposals answer
202with anoperation; confirm the latestrevisionwithapproved_by. - MCP (Pro): ask the token endpoint for a Bearer token with
resourceset to the MCP URL and no DPoP proof; it works at the MCP server only.
All of it is in the developers page and the store’s own OpenAPI document, which is generated from the registry that answers the calls.
Common questions
Where do I get test vectors or a reference client?
The protocol’s authors have said a reference implementation is coming. Until then, the plugin’s test agent (PHP, no dependencies beyond OpenSSL) exercises every request above against a real store over TLS, including the failing cases, and is the most complete worked example we know of.
Do I need to register my agent with the store?
No. Publish your client metadata and JWKS, and the store verifies you on your first request. Stores may block you, and a Pro store may run an allowlist, but there is no registration step in the protocol.
Can I use RS256 instead of ES256?
Yes, with RSA keys of at least 2048 bits. ES256 is preferred. HMAC algorithms and alg: none are refused.
What if I get use_dpop_nonce?
Agent Adapt does not currently require DPoP nonces, so you will not see it from a store running it. A conforming agent should still handle it by retrying once with the DPoP-Nonce the server sends.
Still stuck? Email [email protected].