The preview
Access is by invitation while PayCarter is pre-launch. A company gets three kinds of token:
| Token | Who holds it | What it can do |
|---|---|---|
| Agent | One AI agent, tied to one named employee | Ask to buy, collect payment details for an approved purchase, report the outcome |
| Approver | The person named in the spending rules | See what is waiting for them, approve or decline |
| Admin | Whoever runs PayCarter for the company | Set up people, rules, cards and agents, issue and revoke tokens, read every purchase |
Send the token on every call as Authorization: Bearer <token>. Tokens are shown once when they are made, and only a hash is stored. A token can only ever act as what it was issued for: an agent cannot approve its own purchase.
How a purchase runs
- The agent asksMerchant, amount, currency and a reason, before it reaches any checkout.
- The rules are checkedCurrency, blocked and approved merchants, the limit per purchase, the monthly budget. The reply lists every rule and how it came out.
- A person approves, if neededAnything above the approval threshold waits for the named approver, who gets a link to a one-screen decision.
- The agent collects payment detailsFor that purchase only, and only once.
- The agent reports what happenedCompleted or failed, the amount charged and the order reference. All of it lands in one audit trail.
Every purchase request has a summary that is always one of three words. It is the only field an agent needs to branch on.
| Summary | Meaning | What the agent should do |
|---|---|---|
approved | Within the rules, or a person has said yes | Go to the checkout, then collect payment details |
pending | Waiting for the approver | Check back later. Do not send the request again |
denied | A rule stopped it, the approver declined, or it lapsed | Do not attempt the purchase. Say which rule stopped it |
An approved or pending request holds its amount against the monthly budget, so two requests cannot both squeeze under the limit. Permission lapses if it is not used in time (24 hours unless the company sets otherwise), and a lapsed, declined or failed purchase gives its budget back.
Quick start
Four calls take a purchase from request to receipt. The examples use the demo rules: a 250.00 limit per purchase, 2,000.00 a month, and approval needed above 100.00.
1. Ask to buy
curl https://api.paycarter.com/v1/intents \
-H "Authorization: Bearer $PAYCARTER_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"merchant": { "name": "Fernhill Software", "url": "https://fernhill.example", "countryCode": "GB" },
"amount": "240.00",
"currency": "GBP",
"reason": "Annual plan, design team"
}'
240.00 is above the approval threshold, so the answer is pending:
{
"intent": {
"id": "int_76c3efbc6ba3406e9fcb5b9d2fdd21ff",
"summary": "pending",
"status": "pending",
"merchant": { "name": "Fernhill Software", "url": "https://fernhill.example", "countryCode": "GB" },
"amount": "240.00",
"currency": "GBP",
"reason": "Annual plan, design team",
"checks": [
{ "rule": "currency", "passed": true, "detail": "Purchase is in GBP." },
{ "rule": "blocked_merchant", "passed": true, "detail": "Fernhill Software is not blocked." },
{ "rule": "per_purchase_limit", "passed": true, "detail": "Within the 250.00 GBP limit per purchase." },
{ "rule": "monthly_budget", "passed": true, "detail": "1760.00 GBP of the 2000.00 GBP monthly budget left after this purchase." },
{ "rule": "approval_threshold", "passed": false, "detail": "Above 100.00 GBP, so the approver decides." }
],
"flags": [],
"createdAt": "2026-10-12T14:09:20.711Z",
"expiresAt": "2026-10-13T14:09:20.711Z",
"events": [ /* the audit trail so far */ ]
}
}
2. The approver decides
Usually on the approval screen, from a link. With the approver's token it is one call:
curl https://api.paycarter.com/v1/intents/$INTENT_ID/decision \
-H "Authorization: Bearer $PAYCARTER_APPROVER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "decision": "approve", "reason": "Renewal agreed in the budget" }'
3. Collect payment details
curl -X POST https://api.paycarter.com/v1/intents/$INTENT_ID/credential \
-H "Authorization: Bearer $PAYCARTER_AGENT_TOKEN"
{
"intent": { "id": "int_76c3…", "summary": "approved", "status": "credential_issued" /* … */ },
"credential": {
"cardNumber": "4111111111111111",
"expirationDate": "12/2030",
"securityCode": "123"
}
}
This works once per purchase. A second call answers 409 credential_already_issued. PayCarter does not store or log these details.
4. Report what happened
curl https://api.paycarter.com/v1/intents/$INTENT_ID/confirmation \
-H "Authorization: Bearer $PAYCARTER_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "completed", "amount": "240.00", "orderId": "FH-10042" }'
If the amount reported is more than was approved, the purchase is still recorded and is flagged amount_over_approved for the company to see.
Connect an agent with MCP
The same flow is available as tools over the Model Context Protocol, so an agent that speaks MCP needs no custom code.
| Server address | https://api.paycarter.com/mcp |
| Transport | Streamable HTTP |
| Header | Authorization: Bearer <agent token> |
Many MCP clients take a block like this. The key names vary, so check your client's own documentation.
{
"mcpServers": {
"paycarter": {
"type": "http",
"url": "https://api.paycarter.com/mcp",
"headers": { "Authorization": "Bearer YOUR_AGENT_TOKEN" }
}
}
}
The agent is identified by the token on the connection, never by anything it passes to a tool, so one agent cannot act as another. Approver and admin tokens are refused.
Tools
| Tool | Takes | Gives back |
|---|---|---|
request_purchase | merchant_name, amount, currency, reason. Optional: merchant_url, merchant_country, merchant_category_code | The request, its summary, each rule checked, and a next_step written for the agent |
get_purchase | intent_id | The current state, for example whether a pending request has been approved |
get_payment_credential | intent_id | Card number, expiry and security code for that purchase. Works once |
confirm_purchase | intent_id, status (completed or failed). Optional: amount, order_id | The finished record |
{
"intent_id": "int_662cca55acf7451a9c3fadddb08490ff",
"summary": "pending",
"status": "pending",
"merchant": "Kettle & Co",
"amount": "129.00",
"currency": "GBP",
"reason": "Office kettle",
"checks": [ /* one entry per rule */ ],
"expires_at": "2026-10-13T14:09:20.883Z",
"flags": [],
"next_step": "A person has to approve this first. Check back with get_purchase. Do not send the request again."
}
get_payment_credential returns them as a tool result, so they pass through the model and whatever logs its provider keeps. They are for one purchase and work once, but treat that conversation as sensitive.Purchases API
| Call | Token | What it does |
|---|---|---|
POST/v1/intents | Agent | Ask to make a purchase |
GET/v1/intents/{id} | Agent or approver | Read a request and its audit trail |
POST/v1/intents/{id}/decision | Approver | Approve or decline |
POST/v1/intents/{id}/credential | Agent | One-time payment details |
POST/v1/intents/{id}/confirmation | Agent | Report the outcome |
Asking to buy
| Field | Required | Notes |
|---|---|---|
merchant.name | Yes | 2 to 100 characters |
merchant.url | No | A full https address. Rules about merchants match on the name or on the website, including its subdomains |
merchant.countryCode | No | Two capital letters, such as GB |
merchant.categoryCode | No | Four-digit merchant category code |
amount | Yes | A decimal string such as "240.00", never a number, so nothing is lost to rounding |
currency | Yes | GBP, EUR or USD, and it must match the company's rules |
reason | Yes | 3 to 200 characters. The approver reads it |
Status
summary is for agents. status says exactly where a purchase is.
| Status | Summary | Meaning |
|---|---|---|
pending | pending | Waiting for the approver |
approved | approved | Payment details can be collected |
issuing | approved | Payment details are being fetched right now |
credential_issued | approved | The agent has the payment details |
completed | approved | The purchase happened |
failed | denied | The agent reported that it did not go through |
denied | denied | Stopped by a rule |
declined | denied | The approver said no |
expired | denied | Not used in time |
Rules a company can set
- A limit per purchase, and a monthly budget per agent (calendar month, UTC).
- An approval threshold. Set it to zero and every purchase waits for a person.
- Approved merchants, blocked merchants, or both.
- How long permission lasts before it lapses.
- The named person who approves.
Approvals
When a purchase needs a person, the approver is sent a link, by a webhook into a chat channel for now, with email on the way. The link opens one screen: merchant, amount, reason, who is asking, the card, and what is left of the budget, with Approve and Decline.
| Call | Token | What it does |
|---|---|---|
GET/v1/approvals | Approver | Everything waiting for them |
GET/v1/approvals/{id} | Approver, or the link | The purchase as the approver sees it, with the budget |
POST/v1/approvals/{id}/decision | Approver, or the link | { "decision": "approve" } or "decline", with an optional reason |
A webhook receives a JSON POST with a readable text line and the same facts as separate fields:
{
"text": "Jamie's agent (for Jamie) wants to spend 240.00 GBP at Fernhill Software: Annual plan, design team\nApprove or decline: https://api.paycarter.com/approve/int_…#…",
"event": "approval_requested",
"intentId": "int_76c3efbc6ba3406e9fcb5b9d2fdd21ff",
"merchant": "Fernhill Software",
"amount": "240.00",
"currency": "GBP",
"reason": "Annual plan, design team",
"requestedBy": "Jamie's agent",
"onBehalfOf": "Jamie",
"approver": "Priya, Finance",
"expiresAt": "2026-10-13T14:09:20.711Z",
"approvalUrl": "https://api.paycarter.com/approve/int_…#…"
}
Admin API
With an admin token. Everything is confined to the admin's own company, and anything belonging to another company is reported as not found.
| Call | Body |
|---|---|
GETPOST/v1/admin/employees | name, email |
GETPOST/v1/admin/approversPUT /v1/admin/approvers/{id} | name, and how to tell them: email, webhookUrl (https only) |
GETPOST/v1/admin/policiesPUT /v1/admin/policies/{id} | currency, perPurchaseLimit, monthlyBudget, approvalThreshold, approverId. Optional: allowedMerchants, blockedMerchants, expiryHours. Amounts are strings such as "250.00" |
GETPOST/v1/admin/cards | employeeId, last4. Adds a test card. Real cards will be added by the cardholder, with the card network |
GETPOST/v1/admin/agentsPUT /v1/admin/agents/{id} | name, employeeId, policyId, cardId. The card must belong to that employee |
GETPOST/v1/admin/tokensDELETE /v1/admin/tokens/{tokenId} | type (agent, approver or admin), subjectId, optional label. The token is in the reply to the POST and cannot be read again. Revoking takes effect at once |
GET/v1/admin/intents | Every purchase, newest first. Filter with ?status=pending,approved and ?limit= (up to 200) |
Errors
Errors have one shape, with a stable code to branch on and a message written for a person:
{ "error": { "code": "invalid_currency", "message": "Currency must be one of GBP, EUR, USD." } }
A purchase the rules refuse is not an error. It comes back 201 with the summary denied, so the refusal is on the record.
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_amount, invalid_currency, invalid_merchant, invalid_reason, invalid_body, invalid_decision, invalid_status, invalid_query | Something in the request is wrong. The message says what |
| 401 | unauthorised | No token, or one that is unknown or revoked |
| 403 | forbidden | The token is the wrong kind for this call |
| 404 | intent_not_found, not_found | No such record, or it belongs to someone else |
| 409 | not_approved, not_pending, no_credential, credential_already_issued, credential_in_progress | The purchase is not at the right stage for this call |
| 409 | conflict, busy | Two requests collided. Read the purchase again and retry |
| 502, 503 | rail_error, rail_unavailable | The payment network did not answer. Nothing was issued, and the purchase stays approved |
Not there yet
What this preview does not do, so nobody finds out the hard way:
- Real cards. It is waiting on the card networks. Until then every purchase runs on the test network.
- Sign-in for approvers and admins. Both rest on tokens and links for now.
- Safe retries. There are no idempotency keys yet. If a request to buy times out, read the purchase before sending it again.
- Signed webhooks, and events beyond approval requests.
- Usage limits and versioning. The address will gain a version policy before launch.