Pay per job with x402
x402 lets an agent pay for one Emboss job over the A2A endpoint
with USDC on the Base network, with no Emboss account. Emboss's agent card
at https://api.getemboss.ai/.well-known/agent-card.json advertises the
https://github.com/google-a2a/a2a-x402/v0.1 extension. This page is the
x402 sibling of Pay per call with MPP; see the
pay-per-call hub for the other payment rail.
Ephemeral processing: source and output documents are deleted 60 to 70 minutes after the last activity on a form. The pay-per-call API always runs under ephemeral processing.
The paid skills
make_fillable: turn a flat or scanned PDF form into a fillable PDF with detected fields (from $0.05).fill_from_context: fill a PDF form using facts found in supporting documents (from $0.08).fill_form: fill a PDF form from structured data such as JSON or key-value answers (from $0.07).send_fax: send a PDF to a US, Canadian or Mexican fax number, taking atoDataPart and a PDF FilePart. Priced per transmitted page, with no floor price. The task staysworkinguntil the fax is delivered or fails; poll it withtasks/get.
suggest_mapping and fill_batch work on a form already in an Emboss account
(they take its form_id), so they need an account and cannot be paid per job.
Reading a filled form back is paid per call over HTTP instead, with /pay/read
(see Over HTTP below). The first three prices are floor prices for a short form;
longer forms cost more. See Pricing for the full schedule.
Check the price first
The quote_job skill is free over A2A and returns the exact price for the
PDF you send, with no payment and no job created. The pay-per-call API's
POST https://api.getemboss.ai/pay/quote returns the same prices without A2A.
{
"currency": "USD",
"expires_at": "2026-09-05T18:31:49.316308+00:00",
"pages": 1.0,
"price_cents": "5",
"quote_id": "f28119f5-0f99-4226-b596-7df0cd7cb197"
}
The quote_id in the response is payable later with the execute_quote
skill, or you can send the paid skill directly and pay the challenge it
returns.
Paying
Send the SendMessage request without payment. Emboss creates a task in
state TASK_STATE_INPUT_REQUIRED with an x402.payment.status of
payment-required and an x402.payment.required block naming the price,
the network, and the address to pay. Sign an EIP-3009
TransferWithAuthorization for the exact amount, resubmit a new
SendMessage on the same task with the signed payload, and Emboss settles
it through the Coinbase facilitator on Base. The completed task carries
x402.payment.receipts and, on a make_fillable or fill_form job, an
artifact with the result and a download link.
A paid job that takes longer than about 4 seconds answers working instead;
check the task with tasks/get (GetTask) until it ends (completed,
failed or rejected), and once it is completed the result and the
receipt are there. A task ends rejected, with the reason, when the request
could not be done, such as a fill whose values match no field. If the
payment did not go through, the task can still complete; see the rules
below. On make_fillable, fill_form and fill_from_context the payment is
taken only after the job has run; a paid send_fax is charged just before
the fax is sent.
Here is the real x402.payment.required block from a live make_fillable
task:
{
"accepts": [
{
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bda02913",
"description": "Emboss make_fillable (quote 3e714663-ff2c-45ac-ae11-3c7015facfef)",
"extra": {
"name": "USD Coin",
"version": "2"
},
"maxAmountRequired": "50000",
"maxTimeoutSeconds": 900.0,
"mimeType": "application/json",
"network": "base",
"payTo": "0x57a4ba72d5cf3f4fe01c65a23aa449522d063869",
"resource": "urn:emboss:quote:3e714663-ff2c-45ac-ae11-3c7015facfef",
"scheme": "exact"
}
],
"x402Version": 1.0
}
And the receipt Emboss returned once that job settled:
{
"network": "base",
"payer": "0xBd11871b4BEE0269fC24cA6864da57fA754aBfac",
"success": true,
"transaction": "0x51511ce3cae9407222b1fe3867ff163ffe48d040f82742b70e1ed59574209001"
}
The completed task's artifact carries the result data alongside a
FilePart whose url is the download link, such as
https://getemboss.ai/d/<token>:
{
"artifact_id": "9d8c2b1a-4e3f-4a5b-8c6d-7e9f0a1b2c3d",
"artifact_token": "<artifact token>",
"artifacts": [
{
"artifact_id": "9d8c2b1a-4e3f-4a5b-8c6d-7e9f0a1b2c3d",
"mime_type": "application/pdf",
"role": "fillable"
}
],
"fields": [
{
"description": "The full name of the applicant.",
"group": "applicant_info",
"id": 0,
"kind": "text",
"label": "Name:",
"options": null,
"required": true
},
{
"description": "The date the form is being completed.",
"group": "applicant_info",
"id": 1,
"kind": "date",
"label": "Date:",
"options": null,
"required": true
},
{
"description": "The email address of the applicant.",
"group": "applicant_info",
"id": 2,
"kind": "text",
"label": "Email:",
"options": null,
"required": true
}
],
"form_id": "fa147595-8c16-46eb-ad35-40c8e3fc7e09",
"pages": 1,
"quote_id": "f8b49202-b7a9-4854-ba44-5e54dd0bec7c",
"status": "ready",
"title": "Regression Intake Form",
"warnings": []
}
warnings lists anything Emboss noticed about the form; it is empty for an
ordinary form like this one. On a LiveCycle dynamic form it carries
xfa_dynamic_form, and the result adds a message saying this is a
LiveCycle dynamic form, to open it in Acrobat or Reader, print it to PDF,
and upload that copy; see XFA forms. artifact_token is
the proof of ownership that lets a payer with no Emboss account reuse this
file in a later operation, such as sending it as a fax; send it with the
artifact_id. See Artifacts and lineage.
Python, with the official x402 SDK (pip install "x402==2.21.0" eth-account):
# copied from scripts/x402_live_e2e.py, which made the first two paid x402 jobs
from x402.client import x402ClientSync
from x402.schemas.v1 import PaymentRequiredV1
from x402.mechanisms.evm.exact.v1.client import ExactEvmSchemeV1
def sign_payment(required):
acct = agent_account()
client = x402ClientSync().register_v1("base", ExactEvmSchemeV1(acct))
pr = PaymentRequiredV1.model_validate(required)
payload = client.create_payment_payload(pr)
return payload.model_dump(by_alias=True)
def a2a_send(client, skill, parts, *, task_id=None, metadata=None, rpc_id=1):
"""One anonymous SendMessage. First call: params.metadata.skillId + parts.
Resubmission: message.task_id + message.metadata (x402 status + payload),
no skillId (a2a-x402 v0.1 section 4.5), exactly as the gated tests do."""
msg = {"message_id": f"m-{rpc_id}-{int(time.time())}", "role": "ROLE_USER", "parts": parts}
params = {"message": msg}
if task_id:
msg["task_id"] = task_id
msg["metadata"] = metadata or {}
else:
params["metadata"] = {"skillId": skill}
r = client.post(A2A_URL, json={"jsonrpc": "2.0", "id": rpc_id, "method": "SendMessage", "params": params},
headers={"Content-Type": "application/json", "A2A-Version": "1.0"}, timeout=600)
body = r.json()
if "error" in body:
raise RuntimeError(f"A2A error {body['error']}")
return body["result"]["task"]
Resubmit on the same task_id with the signed payload:
payload = sign_payment(required)
task2 = a2a_send(client, args.skill, [{"data": {}}], task_id=task["id"],
metadata={"x402.payment.status": "payment-submitted", "x402.payment.payload": payload},
rpc_id=2)
JavaScript, with the official x402 SDK
(npm i @x402/core@2.25.0 @x402/evm@2.25.0 viem@2.56.3):
// copied from scripts/x402_live_e2e.mjs, which made a paid x402 job
import { privateKeyToAccount } from "viem/accounts";
import { x402Client } from "@x402/core/client";
import { ExactEvmSchemeV1 } from "@x402/evm/v1";
const account = privateKeyToAccount(process.env.TEMPO_PRIVATE_KEY);
async function send(params, id) {
const r = await fetch(A2A_URL, {
method: "POST",
headers: { "Content-Type": "application/json", "A2A-Version": "1.0" },
body: JSON.stringify({ jsonrpc: "2.0", id, method: "SendMessage", params }),
});
const body = await r.json();
if (body.error) throw new Error(`A2A error ${JSON.stringify(body.error)}`);
return body.result.task;
}
async function signPayment(required) {
const client = new x402Client();
client.registerV1("base", new ExactEvmSchemeV1(account));
const payload = await client.createPaymentPayload(required);
return JSON.parse(JSON.stringify(payload)); // plain JSON, no bigint/class instances
}
The first call and the paid resubmission:
const task = await send({ message: { message_id: `m-1-${Date.now()}`, role: "ROLE_USER", parts }, metadata: { skillId: "make_fillable" } }, 1);
const required = task.status.message.metadata["x402.payment.required"];
const payload = await signPayment(required);
const task2 = await send({ message: { message_id: `m-2-${Date.now()}`, role: "ROLE_USER", task_id: task.id, parts: [{ data: {} }],
metadata: { "x402.payment.status": "payment-submitted", "x402.payment.payload": payload } } }, 2);
Both clients sign the same thing under the hood: an EIP-3009
TransferWithAuthorization typed-data message authorizing the exact
maxAmountRequired in USDC to payTo, which the SDK builds from the
x402.payment.required block.
Over HTTP
x402 also works directly against the pay-per-call API,
with no A2A envelope. Every response from POST https://api.getemboss.ai/pay/<op>
carries an MPP challenge (WWW-Authenticate: Payment) and an x402 v2
requirement in a PAYMENT-REQUIRED header, base64-encoded JSON. Pay either
one. The x402 retry is the identical multipart request plus a
PAYMENT-SIGNATURE header carrying the signed payload. The pay-per-call API
also takes read-back this way, which is not a paid skill over A2A:
POST https://api.getemboss.ai/pay/read reads a filled PDF's form boxes back as
labelled values (from $0.01).
The uploaded file is held until the read is paid for (deleted with the price
if no payment arrives within the hour) and deleted as soon as the read returns;
the values read stay on the quote for 24 hours so a lost answer can be fetched
again. POST https://api.getemboss.ai/pay/fax sends a PDF to a US, Canadian or Mexican fax number (to in E.164), priced per
transmitted page. See Read a filled form back and
Send a fax.
A bodyless POST gets a floor-price 402, useful for discovery tools that
probe a route before sending a real file. Here is the decoded
PAYMENT-REQUIRED from a bodyless probe against /pay/make-fillable; the
bazaar extension is the Bazaar discovery declaration for the route, elided
here to its shape:
A JSON body with no pdf_url or pdf_base64 gets this same probe. A PAYMENT-SIGNATURE the pay-per-call API cannot read, or one whose payload has no nonce, is answered with a fresh 402 whose detail names the problem, never a 400, so a client can sign again and retry without a separate error path.
The pay-per-call API takes the PDF as JSON or as multipart. The JSON body is what Circle's CLI and SDK send; the multipart body is what the captured samples below were made with, and both price identically.
{"pdf_url": "https://example.com/form.pdf", "values": {"Full name": "Ada Lovelace"}}
{
"accepts": [
{
"amount": "50000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bda02913",
"extra": {
"name": "USD Coin",
"quoteId": "probe",
"version": "2"
},
"maxTimeoutSeconds": 899,
"network": "eip155:8453",
"payTo": "0x57a4ba72d5cf3f4fe01c65a23aa449522d063869",
"scheme": "exact"
}
],
"extensions": { "bazaar": { ... } },
"resource": {
"description": "Turn a flat or scanned PDF form into a fillable PDF with detected fields. Also: detect form fields in a PDF; add fillable fields to a document; extract a PDF form schema; make a government or business form fillable. Price scales with page count; this 402 carries the exact amount for the PDF sent.",
"mimeType": "application/json",
"serviceName": "Emboss",
"tags": ["pdf", "forms", "x402", "mpp", "document-processing", "pdf-form-fields", "form-schema", "make-fillable"],
"url": "https://api.getemboss.ai/pay/make-fillable"
},
"x402Version": 2
}
Pay only a priced 402. Sending an actual PDF to the same route returns the
exact amount for that file, with a real quoteId that correlates the retry
to the quote. The amount is USDC atomic units (6 decimals; 50000 here is
about $0.05) and equals the atomic field
the free POST /pay/quote endpoint returns for the same file:
{
"accepts": [
{
"amount": "50000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bda02913",
"extra": {
"name": "USD Coin",
"quoteId": "af8779a3-cf34-4ddf-adda-3a1ae02d88c0",
"version": "2"
},
"maxTimeoutSeconds": 900,
"network": "eip155:8453",
"payTo": "0x57a4ba72d5cf3f4fe01c65a23aa449522d063869",
"scheme": "exact"
}
],
"extensions": { "bazaar": { ... } },
"resource": {
"description": "Turn a flat or scanned PDF form into a fillable PDF with detected fields. Also: detect form fields in a PDF; add fillable fields to a document; extract a PDF form schema; make a government or business form fillable. Price scales with page count; this 402 carries the exact amount for the PDF sent.",
"mimeType": "application/json",
"serviceName": "Emboss",
"tags": ["pdf", "forms", "x402", "mpp", "document-processing", "pdf-form-fields", "form-schema", "make-fillable"],
"url": "https://api.getemboss.ai/pay/make-fillable"
},
"x402Version": 2
}
Circle's CLI pays the pay-per-call API directly from an agent wallet with a Gateway balance. Copied from the run that made the first Gateway-settled job:
circle services pay https://api.getemboss.ai/pay/make-fillable -X POST -d '{"pdf_url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"}' --address 0xccd12f8a05714196a8ab1f9b4631dc96b7757584 --chain BASE --max-amount 0.10
{"job_id":"d0926b48-be08-40bc-93fc-28a433e67c34","status_url":"https://api.getemboss.ai/pay/jobs/d0926b48-be08-40bc-93fc-28a433e67c34?token=<token>","quote_id":"d0926b48-be08-40bc-93fc-28a433e67c34"}
The pay-per-call API answered 202 (the payment is authorized). Polling the status_url shows the filled form, and the Gateway balance went from 0.50 USDC to 0.44 USDC (the 0.06 USDC quote for a 6-page make-fillable), settled through Circle Gateway on Base.
Echo accepted unchanged when you sign: the pay-per-call API rebuilds the
requirement from the quote and rejects a signature whose accepted
requirement does not match field by field. Pay the priced 402 and resend the same multipart
request with PAYMENT-SIGNATURE set. A successful response is a 202 with
{job_id, status_url, quote_id, status} plus a PAYMENT-RESPONSE header.
The paid call answers within about 10 seconds: status is ready when the
job finished in that time, working when it is still running, or failed.
Here is the decoded response from a paid run:
{
"success": true,
"transaction": "0xe1d920d01cce47b6744bd5d5d36cf59fd97fcf875d5e85e9297eb9b673a9644e",
"network": "eip155:8453",
"payer": "0xBd11871b4BEE0269fC24cA6864da57fA754aBfac"
}
The 402 already carries job_id and status_url. Check status_url
every few seconds until status is ready or failed, then download
result.download_url. The status words are explained under
Checking on a job.
For a fax, ready means the carrier accepted it; keep checking until
result.status is delivered, failed or expired. A failed or expired fax
is refunded: by card automatically, up to three refunds a day, and by
stablecoin through contact@getemboss.ai.
Two optional fields on fill-with-context
POST https://api.getemboss.ai/pay/fill-with-context takes two optional form
fields alongside the file, on both rails.
policy is safe or strict. safe is the default and writes any value the
engine reported as high or medium confidence; strict writes only high
confidence values and leaves the rest open.
mode is one_call or two_call. one_call is the default and returns the
finished PDF. two_call stops at the proposal, so a payer can read every
proposed answer before anything is written into the document. When the job
finishes within the paid call's wait, the 202 carries three additive keys
alongside the usual job_id, status_url, quote_id and status:
{
"job_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"status_url": "https://api.getemboss.ai/pay/jobs/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d?token=<token>",
"quote_id": "f28119f5-0f99-4226-b596-7df0cd7cb197",
"status": "ready",
"proposal_id": "3e714663-ff2c-45ac-ae11-3c7015facfef",
"commit_token": "<opaque token>",
"proposal_url": "https://api.getemboss.ai/proposals/3e714663-ff2c-45ac-ae11-3c7015facfef?token=<opaque token>"
}
Otherwise the paid call answers working, and these same three keys come in
the status check's answer once status is ready.
Read the proposal at proposal_url, then commit it with
POST https://api.getemboss.ai/proposals/{proposal_id}/commit, sending the token
in an X-Proposal-Token header. The token proves you own the proposal you paid
for and lives 45 minutes. Committing costs nothing more: the work was billed
when you paid for the prepare.
curl -X POST https://api.getemboss.ai/proposals/3e714663-ff2c-45ac-ae11-3c7015facfef/commit \
-H "X-Proposal-Token: <commit_token>" \
-H "Content-Type: application/json" \
-d '{"values": [{"field_id": 14, "value": "12-3456789"}]}'
See Prepare, commit and verify for the proposal
shape, the three results and the receipt. If two-call mode is not enabled on the
service, mode=two_call returns a 503; every other path is unaffected.
Python, with the official x402 SDK
(pip install "x402==2.21.0" eth-account httpx):
# copied from scripts/x402_live_e2e.py --http, which paid the first x402 job on the pay-per-call API
from x402.client import x402Client
from x402.http.clients.httpx import x402_httpx_transport
from x402.mechanisms.evm.exact.client import ExactEvmScheme
def http_client(account):
client = x402Client().register("eip155:8453", ExactEvmScheme(account))
return httpx.AsyncClient(transport=x402_httpx_transport(client))
async def run_http(account, form_dir, op):
files, data = multipart_body(form_dir, args.skill)
async with http_client(account) as client:
resp = await client.post(f"https://api.getemboss.ai/pay/{op}", files=files, data=data, timeout=600)
payment_response_hdr = resp.headers.get("PAYMENT-RESPONSE")
if payment_response_hdr:
from x402.http.utils import decode_payment_response_header
sr = decode_payment_response_header(payment_response_hdr)
tx = sr.transaction or None
# resp.status_code == 202; resp.json() has job_id, status_url, quote_id
x402_httpx_transport pays the 402 automatically: it sees the first 402,
signs a v2 payload for the requirement, and resends the same request with
PAYMENT-SIGNATURE set, so a single client.post(...) call is the whole
retry.
JavaScript, with the official x402 SDK
(npm i @x402/fetch@2.25.0 @x402/core@2.25.0 @x402/evm@2.25.0 viem@2.56.3):
// copied from scripts/x402_live_e2e.mjs --http, which paid an x402 job on the pay-per-call API
import { x402Client } from "@x402/core/client";
import { decodePaymentResponseHeader } from "@x402/core/http";
import { ExactEvmScheme } from "@x402/evm";
import { wrapFetchWithPayment } from "@x402/fetch";
const client = new x402Client().register("eip155:8453", new ExactEvmScheme(account));
const fetchWithPay = wrapFetchWithPayment(fetch, client);
const paidResp = await fetchWithPay(url, { method: "POST", body: multipartBodyFor(form, skill) });
const paymentResponseHeader = paidResp.headers.get("PAYMENT-RESPONSE");
if (paymentResponseHeader) {
const settle = decodePaymentResponseHeader(paymentResponseHeader);
// settle.success, settle.transaction, settle.network, settle.payer
}
// paidResp.status === 202; await paidResp.json() has job_id, status_url, quote_id
wrapFetchWithPayment does the same automatic retry as the Python
transport: the wrapped fetch sees the 402, signs, and resends with
PAYMENT-SIGNATURE set.
Rules a payer should know
- The price in the challenge is the price charged: it comes from the same engine as the account rail and the pay-per-call API's free quote.
- One payment authorization per quote on the A2A rail: a rejected or failed attempt uses up that quote's nonce, so ask for a new quote instead of signing again on the same task. The pay-per-call API works differently: a rejected or failed x402 attempt there gets a fresh 402, and a new signature (a new nonce) on the same quote is accepted.
- On the A2A rail, a message sent again on a task you paid for is answered
"This task is paid; check it for the result." Check the task with
tasks/get(GetTask) instead of starting a new one, which would mean a new price. - On a paid
send_fax, or a paid job cut off by an update to Emboss, if Emboss cannot confirm whether the payment went through, the task endsfailedwith "We could not confirm your payment. Contact contact@getemboss.ai with your task id." Contact us with the task id rather than paying again on a new task: the payment may already have been taken. On the other paid skills the job has already run, so the task completes with the result and the same sentence; see below. - A quote that is not paid expires an hour after it is made; once paid, its job and result stay available for 24 hours from the quote.
- On the pay-per-call API, a paid job answers one of five words at its
status_url:awaiting_payment,working,ready,failedorexpired. For a fax,readymeans the carrier accepted it; keep checking untilresult.statusisdelivered,failedorexpired. - Resending the identical paid request with the same
PAYMENT-SIGNATUREnever charges again: it returns the job, runs a failed job again at no charge (up to three runs in all for one payment), and answers410 Goneonce the result has expired. The same signature with a different document or setting is refused with409. - On the A2A rail the payer needs USDC on the Base network only; no ETH, the
facilitator pays the gas. On the pay-per-call API the 402's
acceptslist carries the Base entry first, then one entry for every chain Circle Gateway supports (extra.nameisGatewayWalletBatched); a payer with a Circle Gateway balance can pay any of those gaslessly, and a plain x402 wallet keeps paying the Base entry as before. - Anonymous quotes are limited per IP address and hour; a limited caller receives a rate-limit error and can retry in the next hour.
A resubmitted task can come back with an error instead of a receipt:
INSUFFICIENT_FUNDS: the payer's USDC balance is below the quoted amount.INVALID_SIGNATURE: the signature doesn't verify against the authorization, including a malformed payload or a facilitator rejection with no closer code.EXPIRED_PAYMENT: the authorization'svalidBeforefalls inside the job's runtime window.DUPLICATE_NONCE: that authorization's nonce was already used.
On make_fillable, fill_form and fill_from_context, Emboss runs the job
before it settles the payment on-chain. Nothing is charged when the payment
service clearly refused the payment: the task still completes with the
result, its final answer carries a receipt whose success is false, with an
errorReason, and a later check of the task carries the result with no
receipt. If Emboss cannot confirm whether the payment went through (a server
error, an answer that could not be read, none, or the payment step breaking
off with an error), the payment may have been taken: the task completes with
the result and "We could not confirm your payment. Contact
contact@getemboss.ai with your task id.", in its answer and when you check
the task later. Its receipt is marked not successful, unless Emboss recorded
the payment, when the receipt shows it. Contact contact@getemboss.ai with the
task id rather than paying again.
A paid send_fax settles first, just before the fax is sent: if the payment
service clearly refuses the payment, the task ends failed with "Payment did
not settle; nothing was sent." and nothing is charged; an unclear answer ends
it as the rule above says.
Billing and privacy
Pay-per-call operations are paid per request and do not use an Emboss account. If you use the regular API with an API key instead, the account billing applies: The first 5 form creations, 5 context fills, 5 standard fills, 5 verifications, 5 reads, and 5 packages each month are free when the job is 5 pages or fewer (a context fill counts the form and its documents together); a longer job is charged and does not use a free one. Only the uploaded PDF and any context you attach are processed for the request; see the privacy policy.