Payment infrastructure for AI agents. Mandate requests + JIT ephemeral card issuance.
Full docs: attesso.com/docs
Go from zero to first mandate in 5 minutes.
| Concept | Description |
|---|---|
mandate_request |
Authorization request sent to a user. Contains amount and validity window. Returns approval URL for user consent via passkey. |
ephemeral_card |
Short-lived virtual Visa card issued on demand. Default 5-minute TTL, single-use, auto-destroyed after authorization. |
passkey |
Hardware-backed user authentication. FaceID, TouchID, or Windows Hello. Cryptographically bound to attesso.com. |
fee_modes |
Two modes: markup (default) adds fees on top, inclusive deducts fees from amount. |
npm install @attesso/sdk
# or
pnpm add @attesso/sdkimport { AttessoClient } from '@attesso/sdk';
const attesso = new AttessoClient({
apiKey: process.env.ATTESSO_API_KEY // rk_test_...
});Your agent creates a mandate request. The user approves via passkey (FaceID/TouchID). Their payment method is charged immediately.
const request = await attesso.createMandateRequest({
externalUserId: 'user_123', // your user identifier
amount: 50000, // $500 in cents
validityWindow: '24h', // request expires in 24 hours
});
// Send the approval URL to your user
console.log('Approve at:', request.approvalUrl);
// User authenticates with FaceID/TouchID (WebAuthn passkey)const status = await attesso.getMandateRequest(request.id);
if (status.status === 'approved') {
console.log('Mandate ID:', status.mandate.id);
console.log('Spending limit:', status.spendingLimit); // amount minus fees
}When your agent is ready to make a purchase, issue an ephemeral virtual card.
const card = await attesso.issueCard(status.mandate.id, {
amount: 34700, // $347.00
ttlSeconds: 300, // 5 min TTL
});
console.log(card.number); // '4242...'
console.log(card.cvc); // '123'
console.log(card.expMonth); // 3
console.log(card.expYear); // 2026
// Use at merchant checkout. Auto-captured. Card destroyed after use.1% platform fee + Stripe processing at cost. Optional developer fee (0-5%). Pricing
The fastest way to add payment capabilities to your AI agent. One import, four tools, instant card issuance.
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { attesso } from '@attesso/sdk/vercel';
const result = await generateText({
model: openai('gpt-4o'),
tools: attesso.tools({ mandateId }),
prompt: 'Book me a flight to NYC under $500',
});npm install @attesso/sdk ai zod| Tool | Type | Description |
|---|---|---|
attesso_get_mandate |
read | Get mandate details including spendingLimit, remainingLimit, allowed merchants, and status. |
attesso_issue_card |
write | Issue an ephemeral virtual card with exact amount and ttlSeconds. Returns card number, cvc, expMonth, expYear. |
attesso_get_card |
read | Get card status and details. Check if the card has been used, expired, or cancelled. |
attesso_revoke_mandate |
write | Revoke a mandate. No further cards can be issued against it. |
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { attesso } from '@attesso/sdk/vercel';
async function bookFlight(mandateId: string, request: string) {
const result = await generateText({
model: openai('gpt-4o'),
tools: {
...attesso.tools({ mandateId }),
search_flights: {
description: 'Search for available flights',
parameters: z.object({
from: z.string(),
to: z.string(),
date: z.string(),
}),
execute: async ({ from, to, date }) => {
return searchFlightsAPI(from, to, date);
},
},
},
system: `You are a travel booking agent.
1. Search for flights matching the user request
2. Check the mandate with attesso_get_mandate
3. Find the best deal within the spendingLimit
4. Use attesso_issue_card to get a card for the purchase
5. Confirm the booking details`,
prompt: request,
maxSteps: 10,
});
return result.text;
}
const response = await bookFlight(
'mandate_abc123',
'Book me the cheapest flight from SFO to NYC next Friday'
);Note: The Vercel AI SDK integration requires
aiandzodas peer dependencies.
Use Attesso payment tools in any MCP-compatible AI assistant: Claude Desktop, Cursor, Windsurf, Continue, Zed, Cline, and more.
Add Attesso to your MCP config file:
- Claude Desktop (macOS):
~/Library/Application Support/Claude/claude_desktop_config.json - Claude Desktop (Windows):
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"attesso": {
"command": "npx",
"args": ["-y", "@attesso/mcp"],
"env": {
"ATTESSO_API_KEY": "rk_test_..."
}
}
}
}Restart your AI assistant after updating the configuration.
Same 4 tools as the Vercel AI SDK: attesso_get_mandate, attesso_issue_card, attesso_get_card, attesso_revoke_mandate.
| Variable | Description |
|---|---|
ATTESSO_API_KEY |
API key (rk_test_* or rk_live_*) |
ATTESSO_BASE_URL |
Custom API URL (default: https://api.attesso.com) |
ATTESSO_MANDATE_ID |
Default mandate ID for all operations |
ATTESSO_READ_ONLY |
Set to "true" to disable write operations |
Security Note: Mandate creation requires user passkey authentication (FaceID/TouchID) and cannot be done through MCP. MCP tools only operate within existing, user-authorized mandates.
The @attesso/sdk package provides a TypeScript/JavaScript client for the Attesso API. The SDK uses camelCase; the REST API uses snake_case.
Create a mandate request that the user can approve.
const request = await attesso.createMandateRequest({
amount: 50000, // $500.00 in cents
externalUserId: 'user_abc', // your user identifier
validityWindow: '7d', // expires in 7 days
category: 'travel', // optional category
metadata: { // optional key-value pairs
agentId: 'travel-bot-001',
purpose: 'Flight booking',
},
callbackUrl: 'https://...', // optional webhook URL
});
console.log(request.id); // 'req_abc123'
console.log(request.approvalUrl); // 'https://attesso.com/approve/req_abc123'
console.log(request.spendingLimit); // 47000 (amount minus fees)
console.log(request.totalCharged); // 50000
console.log(request.status); // 'pending'
console.log(request.callbackSecret); // only if callbackUrl providedconst request = await attesso.getMandateRequest('req_abc123');
console.log(request.status); // 'pending' | 'approved' | 'rejected' | 'expired' | 'cancelled'
console.log(request.mandate); // { id: '...' } when status is 'approved'await attesso.cancelMandateRequest('req_abc123');const mandate = await attesso.getMandate('mandate_xyz123');
console.log(mandate.status); // 'active' | 'exhausted' | 'expired' | 'revoked'
console.log(mandate.spendingLimit); // 47000 (cents)
console.log(mandate.totalCharged); // 50000 (what user was charged)
console.log(mandate.expiresAt); // '2026-03-01T00:00:00Z'Issue an ephemeral virtual card against a mandate. Returns full card details (PAN, CVC, expiry).
const card = await attesso.issueCard('mandate_xyz123', {
amount: 34700, // $347.00 in cents
ttlSeconds: 300, // 5 minute TTL (default)
allowedMccs: ['3000', '3001', '3299'], // optional
// OR blockedMccs: ['7995'] // mutually exclusive
});
console.log(card.cardId); // 'card_abc456'
console.log(card.number); // '4242424242424242'
console.log(card.cvc); // '123'
console.log(card.expMonth); // 3
console.log(card.expYear); // 2026
console.log(card.expiresAt); // '2026-02-07T12:05:00Z'Important: Card PAN, CVC, and expiry are only returned on issuance.
getCard()does not return these fields.
const card = await attesso.getCard('card_abc456');
console.log(card.status); // 'active' | 'used' | 'expired' | 'cancelled'const payment = await attesso.getPayment('pay_xyz789');
console.log(payment.status); // 'authorized' | 'captured' | 'reversed' | 'declined'
console.log(payment.amount); // 34700
console.log(payment.cardId); // 'card_abc456'
console.log(payment.mandateId); // 'mandate_xyz123'
console.log(payment.merchantName); // 'United Airlines'
console.log(payment.merchantMcc); // '3000'import { AttessoClient, AttessoError } from '@attesso/sdk';
try {
const card = await attesso.issueCard('mandate_xyz', {
amount: 100000,
ttlSeconds: 300,
});
} catch (error) {
if (error instanceof AttessoError) {
console.log('Status:', error.statusCode); // 400
console.log('Code:', error.code); // 'AMOUNT_EXCEEDS_LIMIT'
console.log('Message:', error.message); // 'Amount exceeds mandate spending limit'
}
}Base URL: https://api.attesso.com/v1
The REST API uses snake_case for all field names.
curl https://api.attesso.com/v1/mandates/mandate_xyz \
-H "Authorization: Bearer rk_test_your_restricted_key" \
-H "Content-Type: application/json"| Method | Endpoint | Description |
|---|---|---|
POST |
/v1/mandate-requests |
Create a new mandate request |
GET |
/v1/mandate-requests/:id |
Get mandate request status |
DELETE |
/v1/mandate-requests/:id |
Cancel a pending mandate request |
GET |
/v1/mandates/:id |
Get mandate details |
POST |
/v1/mandates/:id/cards |
Issue an ephemeral virtual card |
DELETE |
/v1/mandates/:id |
Revoke a mandate |
GET |
/v1/cards/:id |
Get card status (no PAN/CVC) |
GET |
/v1/payments/:id |
Get payment details |
POST |
/v1/mandate-requests/preview-fees |
Preview fee calculation |
For safe retries, include an Idempotency-Key header with POST requests.
curl -X POST https://api.attesso.com/v1/mandates/mandate_xyz/cards \
-H "Authorization: Bearer rk_test_..." \
-H "Idempotency-Key: unique-request-id-12345" \
-H "Content-Type: application/json" \
-d '{"amount": 34700, "ttl_seconds": 300}'Keys expire after 24 hours. Concurrent duplicates return 409 Conflict.
Two fee modes:
| Mode | Description | Example ($500 amount) |
|---|---|---|
| Markup (default) | Fees added on top. Input amount = agent spending limit. | spendingLimit: $500.00, totalCharged: $519.06 |
| Inclusive | Fees deducted from amount. Input amount = what user is charged. | totalCharged: $500.00, spendingLimit: $480.94 |
| Component | Rate | Description |
|---|---|---|
| Platform fee | 1% | Attesso platform fee |
| Processing | at cost | Stripe processing (varies by region) |
| Developer fee | 0-5% | Optional, routed to your Stripe Connect account |
| Code | Description |
|---|---|
MANDATE_NOT_FOUND |
Mandate ID doesn't exist |
MANDATE_REVOKED |
Mandate has been revoked |
MANDATE_EXPIRED |
Mandate has passed its expiration date |
MANDATE_EXHAUSTED |
Mandate remaining limit is zero |
AMOUNT_EXCEEDS_LIMIT |
Card amount exceeds mandate remaining limit |
MERCHANT_NOT_ALLOWED |
Merchant MCC not in card's allowedMccs or is in blockedMccs |
CARD_NOT_FOUND |
Card ID doesn't exist |
CARD_EXPIRED |
Card TTL has expired |
INVALID_AMOUNT |
Amount must be a positive integer (in cents) |
AUTHORIZATION_FAILED |
Card declined or insufficient funds |
WEBAUTHN_VERIFICATION_FAILED |
Passkey signature verification failed |
Error response format:
{
"error": {
"code": "AMOUNT_EXCEEDS_LIMIT",
"message": "Requested amount $600.00 exceeds mandate remaining limit of $500.00",
"statusCode": 400
}
}Webhooks notify your application when events happen. Configure a callbackUrl when creating a mandate request to receive events for that mandate's lifecycle.
Webhook Security: Verify webhook authenticity using the
X-Attesso-Signatureheader with HMAC-SHA256. ThecallbackSecretis returned once when creating the mandate request. Store it securely.
| Event | Description |
|---|---|
mandate_request.approved |
User approved the mandate request via passkey |
mandate_request.rejected |
User rejected the mandate request |
mandate_request.expired |
Request expired (validity window elapsed) |
mandate.exhausted |
Mandate spending limit fully consumed |
mandate.expired |
Mandate reached its expiration date |
payment.captured |
Payment captured at merchant |
payment.reversed |
Payment was reversed (refund or chargeback) |
{
"event": "mandate_request.approved",
"timestamp": "2026-02-08T12:00:00Z",
"data": {
"request_id": "req_abc123",
"mandate_id": "mandate_xyz",
"spending_limit": 47000,
"external_user_id": "user_abc",
"status": "approved",
"category": "travel",
"metadata": { "agent_id": "travel-bot" }
}
}import express from 'express';
import crypto from 'crypto';
const app = express();
app.post('/webhooks/attesso', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-attesso-signature'] as string;
const expectedSignature = crypto
.createHmac('sha256', process.env.CALLBACK_SECRET!)
.update(req.body)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
switch (event.event) {
case 'mandate_request.approved':
console.log('Mandate approved:', event.data.mandate_id);
break;
case 'payment.captured':
console.log('Payment captured:', event.data.payment_id);
break;
case 'mandate.exhausted':
console.log('Mandate exhausted:', event.data.mandate_id);
break;
}
res.json({ received: true });
});Test keys start with rk_test_. Cards issued with test keys are simulated and don't interact with the Visa network.
- WebAuthn verification is bypassed in test mode
- Cards are simulated and don't hit the Visa network
- Webhooks fire normally (configure a callbackUrl)
- Fee calculations work identically to production
- Rate limits are relaxed for easier development
- Complete your profile:Fill in business details in the dashboard settings
- Connect your bank account:Click "Go Live" in dashboard, follow Stripe Connect onboarding
- Switch to live keys:Replace
rk_test_...withrk_live_...in production - Start issuing:Mandates authorize real cards, funds settle to your bank via Stripe
Payouts are sent to your bank account on a 2-day rolling basis.
Attesso uses WebAuthn passkeys to ensure every mandate is authorized by a real user on a real device. Hardware-backed keys prevent bot farms, emulators, and phishing at the hardware level.
No app required. Users authorize via WebAuthn passkeys directly in the browser.
| Device | Hardware | Biometric | Key Storage |
|---|---|---|---|
| iPhone | Secure Enclave | FaceID / TouchID | SE P256 |
| Mac (TouchID) | Secure Enclave (T2/M-series) | TouchID | SE P256 |
| Windows | TPM 2.0 | Windows Hello | TPM RSA/ECC |
| Android | StrongBox / TEE | Fingerprint / Face | TEE P256 |
- Passkey attestation: Signature from TPM/Secure Enclave chip, not browser. Cannot be faked with JavaScript.
- Phishing resistant: Passkey is cryptographically bound to attesso.com. Fake domains don't trigger the prompt.
- Key isolation: Private key never leaves device hardware. Even if phone is cloned, key cannot be extracted.
| Endpoint | Limit |
|---|---|
| Auth | 5/min |
| Card issuance | 30/min |
| General | 100/min |
Sliding window. Check X-RateLimit-* response headers.
- Cards have a short TTL (30s-3600s, default 300s)
- Single-use: destroyed after first merchant authorization
- Amount-limited: card cannot be charged more than issued amount
- MCC-restricted: allowedMccs or blockedMccs per card
- Auto-destroyed on expiry even if unused
- PAN/CVC only returned on issuance, not on subsequent GET
Built for the agent economy. Hardware-secured. No app required.