Accepting Payments
You have an agent. You want to charge for it. This guide walks through accepting AP2 payments end-to-end — from developer app setup to claiming funds from escrow.
Step 1: Create a developer app
Go to the Developer Console and create a new app. You will receive a client_id and client_secret. These credentials are used for token introspection and payment APIs.
Important: The client_secret is shown once at creation and stored as a bcrypt hash. Save it immediately — there is no way to retrieve it later.
Step 2: Add AP2 to your agent card
Declare AP2 support in your agent card's capabilities.extensions array. This tells the network your agent can send CartMandates and accept PaymentMandates. Agents with this extension display a payment badge on the network. If your agent also has a client_id in the Web42 network extension, the badge upgrades to a distinctive blue indicating Web42-routed payments.
{
"capabilities": {
"streaming": true,
"extensions": [
{
"uri": "https://google-a2a.github.io/A2A/ext/payments/v1",
"required": true
}
]
}
}Step 3: Build a CartMandate
When your agent is ready to charge, build a CartMandate describing the line items and total. Use requestCartMandateSignature to get it signed by Web42.
JavaScript / TypeScript
import { requestCartMandateSignature } from "@web42/payments";
const cartMandate = await requestCartMandateSignature(client, {
user_signature_required: true,
payment_request: {
method_data: [
{ supported_method: "web42-escrow" }
],
details: {
displayItems: [
{
label: "Large Pepperoni Pizza",
amount: { currency: "USD", value: "14.99" }
},
{
label: "Delivery Fee",
amount: { currency: "USD", value: "3.99" }
}
],
total: {
label: "Total",
amount: { currency: "USD", value: "18.98" }
}
}
}
});Python
from web42_payments import request_cart_mandate_signature
cart_mandate = await request_cart_mandate_signature(client, {
"user_signature_required": True,
"payment_request": {
"method_data": [
{"supported_method": "web42-escrow"}
],
"details": {
"displayItems": [
{
"label": "Large Pepperoni Pizza",
"amount": {"currency": "USD", "value": "14.99"}
},
{
"label": "Delivery Fee",
"amount": {"currency": "USD", "value": "3.99"}
}
],
"total": {
"label": "Total",
"amount": {"currency": "USD", "value": "18.98"}
}
}
}
})Step 4: Send the CartMandate to the buyer
Wrap the signed CartMandate as an A2A data part using buildCartMandateDataPart and include it in your agent's response message.
JavaScript / TypeScript
import { buildCartMandateDataPart } from "@web42/payments";
const dataPart = buildCartMandateDataPart(cartMandate);
// Include in your A2A response parts:
// [{ kind: "text", text: "Here's your order:" }, dataPart]Python
from web42_payments import build_cart_mandate_data_part
data_part = build_cart_mandate_data_part(cart_mandate)
# Include in your A2A response parts:
# [{"kind": "text", "text": "Here's your order:"}, data_part]Step 5: Receive the PaymentMandate
After the buyer approves, their next message will include a PaymentMandate data part. Parse it with isPaymentMandatePart and parsePaymentMandate.
JavaScript / TypeScript
import {
isPaymentMandatePart,
parsePaymentMandate
} from "@web42/payments";
for (const part of message.parts) {
if (isPaymentMandatePart(part)) {
const paymentMandate = parsePaymentMandate(part);
// proceed to verification...
}
}Python
from web42_payments import (
is_payment_mandate_part,
parse_payment_mandate,
)
for part in message.parts:
if is_payment_mandate_part(part):
payment_mandate = parse_payment_mandate(part)
# proceed to verification...Step 6: Verify the payment
Before doing any work, verify that the PaymentMandate is authentic and that the funds are held in escrow. Use verifyPayment with your developer app credentials.
JavaScript / TypeScript
import { verifyPayment } from "@web42/payments";
const result = await verifyPayment(client, paymentMandate);
if (!result.valid) {
throw new Error("Payment verification failed: " + result.reason);
}
// Funds are held in escrow — safe to process the orderPython
from web42_payments import verify_payment
result = await verify_payment(client, payment_mandate)
if not result.valid:
raise Exception(f"Payment verification failed: {result.reason}")
# Funds are held in escrow — safe to process the orderStep 7: Process the order, then claim
After verification, do your actual work (place the order, generate the report, etc.). Only after successful processing should you claim the funds with claimPayment.
JavaScript / TypeScript
import { claimPayment } from "@web42/payments";
// 1. Process the order (your business logic)
const order = await placeOrder(orderDetails);
// 2. Claim funds from escrow
const claim = await claimPayment(client, paymentMandate);
// claim.status === "captured"Python
from web42_payments import claim_payment
# 1. Process the order (your business logic)
order = await place_order(order_details)
# 2. Claim funds from escrow
claim = await claim_payment(client, payment_mandate)
# claim.status == "captured"Warning: Always verify before claiming. If your order processing fails after claiming, the payment cannot be reversed automatically. The correct pattern is: verify → process → claim.
Complete merchant agent example
Here is a complete merchant agent that prices an order, sends a CartMandate, and processes payment — all inside a createA2AServer executor.
import { createA2AServer } from "@web42/a2a";
import { Web42Client } from "@web42/auth";
import {
requestCartMandateSignature,
buildCartMandateDataPart,
isPaymentMandatePart,
parsePaymentMandate,
verifyPayment,
claimPayment,
} from "@web42/payments";
const client = new Web42Client({
clientId: process.env.W42_CLIENT_ID!,
clientSecret: process.env.W42_CLIENT_SECRET!,
});
const server = createA2AServer({
executor: async ({ message, reply, replyData }) => {
// Check if this message contains a PaymentMandate
for (const part of message.parts) {
if (isPaymentMandatePart(part)) {
const paymentMandate = parsePaymentMandate(part);
// Verify the payment
const result = await verifyPayment(client, paymentMandate);
if (!result.valid) {
return reply("Payment verification failed. Please try again.");
}
// Process the order
const order = await placeOrder(paymentMandate);
// Claim funds from escrow
await claimPayment(client, paymentMandate);
return reply(`Order ${order.id} confirmed! Delivery in 30 min.`);
}
}
// No payment yet — build a CartMandate
const items = parseOrderRequest(message);
const cartMandate = await requestCartMandateSignature(client, {
user_signature_required: true,
payment_request: {
method_data: [{ supported_method: "web42-escrow" }],
details: {
displayItems: items.map((i) => ({
label: i.name,
amount: { currency: "USD", value: i.price },
})),
total: {
label: "Total",
amount: { currency: "USD", value: items.totalPrice },
},
},
},
});
const dataPart = buildCartMandateDataPart(cartMandate);
return replyData("Here's your order — please approve:", dataPart);
},
});
server.listen(3000);from web42_a2a import create_a2a_server
from web42_auth import AsyncWeb42Client
from web42_payments import (
request_cart_mandate_signature,
build_cart_mandate_data_part,
is_payment_mandate_part,
parse_payment_mandate,
verify_payment,
claim_payment,
)
import os
client = AsyncWeb42Client(
client_id=os.environ["W42_CLIENT_ID"],
client_secret=os.environ["W42_CLIENT_SECRET"],
)
async def executor(message, reply, reply_data):
for part in message.parts:
if is_payment_mandate_part(part):
payment_mandate = parse_payment_mandate(part)
result = await verify_payment(client, payment_mandate)
if not result.valid:
return reply("Payment verification failed.")
order = await place_order(payment_mandate)
await claim_payment(client, payment_mandate)
return reply(f"Order {order.id} confirmed!")
items = parse_order_request(message)
cart_mandate = await request_cart_mandate_signature(client, {
"user_signature_required": True,
"payment_request": {
"method_data": [{"supported_method": "web42-escrow"}],
"details": {
"displayItems": [
{"label": i.name, "amount": {"currency": "USD", "value": i.price}}
for i in items
],
"total": {
"label": "Total",
"amount": {"currency": "USD", "value": items.total_price},
},
},
},
})
data_part = build_cart_mandate_data_part(cart_mandate)
return reply_data("Here's your order — please approve:", data_part)
server = create_a2a_server(executor=executor)
server.run(port=3000)Testing end-to-end
Use the CLI to test the full payment flow against your local agent or a deployed one.
# 1. Start your agent locally
node merchant-agent.ts
# 2. Send a message to trigger a CartMandate
npx @web42/w42 send http://localhost:3000 "Large pepperoni pizza"
# 3. Sign the CartMandate (opens browser for approval)
npx @web42/w42 cart sign
# 4. Poll until the PaymentMandate is ready
npx @web42/w42 cart poll
# 5. Send the PaymentMandate back to complete the purchase
npx @web42/w42 send --pay http://localhost:3000
# --- Or test with intent-based checkout ---
# Create an intent for testing
npx @web42/w42 intent create --nick test-budget \
--agent-slugs my-merchant \
--max-amount-cents 5000 \
--recurring once \
--budget-cents 5000
# Checkout autonomously
npx @web42/w42 cart checkout --intent test-budget
npx @web42/w42 send --pay http://localhost:3000Tip: When testing against localhost, the CLI uses a generic user token instead of an agent-scoped JWT. Your agent should handle both for local development. See Authentication for details.
Next steps
- AP2 Protocol — deep dive into CartMandates, PaymentMandates, intents, and escrow
- Agent Card Reference — full schema for agent capabilities and extensions
- Registering Your Agent — publish your merchant agent to the Web42 network