Skip to main content

Intents

Intents are pre-transaction declarations that create an audit trail and enable transaction matching. They answer the question: “What did the agent claim it was going to do?”
Intents are only available for multi-use cards. Single-use cards capture intent at creation time via the purpose field.

Why Intents?

Without IntentsWith Intents
Transaction happens, no contextAgent declares intent before spending
Hard to audit what agent intendedClear audit trail of claimed purpose
Can’t detect unexpected purchasesTransactions matched against expectations

Intent Lifecycle

pending → matched    (transaction arrived within tolerance)
pending → mismatched (transaction arrived, but outside tolerance)
pending → expired    (TTL reached, no transaction)
pending → canceled   (manually canceled)

Create an Intent

Before making a purchase, declare what you’re about to do:
curl -X POST https://api.ledger.so/v1/cards/$CARD_ID/intents \
  -H "Api-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "intentId": "order-123",
    "summary": "Order lunch on DoorDash",
    "expectedAmount": 2500,
    "expectedMerchant": "DoorDash",
    "tolerance": 500,
    "ttlMinutes": 30
  }'
{
  "id": "int_abc123",
  "cardId": "card_xyz789",
  "intentId": "order-123",
  "summary": "Order lunch on DoorDash",
  "expectedAmount": 2500,
  "expectedMerchant": "DoorDash",
  "tolerance": 500,
  "status": "pending",
  "expiresAt": 1703521800000,
  "createdAt": 1703520000000
}

Intent Fields

FieldRequiredDescription
intentIdYesYour unique ID (for idempotency)
summaryYesHuman-readable description
expectedAmountNoExpected amount in cents
expectedMerchantNoExpected merchant name
toleranceNoAllowed variance in cents (e.g., 500 = ±$5)
ttlMinutesNoTime to live (default 30, max 1440)
metadataNoAdditional context (taskId, orderId, etc.)

Transaction Matching

When a transaction arrives, it’s matched against pending intents: Matched - Transaction is within tolerance:
{
  "status": "matched",
  "transactionId": "txn_def456",
  "matchResult": {
    "amountMatch": true,
    "merchantMatch": true,
    "actualAmount": 2450,
    "actualMerchant": "DOORDASH"
  }
}
Mismatched - Transaction outside tolerance:
{
  "status": "mismatched",
  "transactionId": "txn_def456",
  "matchResult": {
    "amountMatch": false,
    "merchantMatch": true,
    "actualAmount": 5000,
    "actualMerchant": "DOORDASH"
  }
}

List Intents

curl https://api.ledger.so/v1/cards/$CARD_ID/intents \
  -H "Api-Key: $API_KEY"
{
  "cardId": "card_xyz789",
  "intents": [
    {
      "id": "int_abc123",
      "intentId": "order-123",
      "summary": "Order lunch on DoorDash",
      "status": "matched",
      "createdAt": 1703520000000,
      "matchedAt": 1703520600000
    },
    {
      "id": "int_def456",
      "intentId": "order-124",
      "summary": "Order dinner on DoorDash",
      "status": "pending",
      "expiresAt": 1703525400000,
      "createdAt": 1703523600000
    }
  ]
}

Best Practices

The intent creates an audit trail. Even if you don’t use matching, it documents what the agent claimed.
Tips, taxes, and fees can vary. Set tolerance to account for this:
{
  "expectedAmount": 2500,
  "tolerance": 500
}
Add context that helps with debugging:
{
  "metadata": {
    "taskId": "task_123",
    "orderId": "order_456",
    "cartItems": ["burger", "fries"]
  }
}
Default is 30 minutes. Adjust based on your use case:
  • Quick purchases: 10-15 minutes
  • Complex checkouts: 30-60 minutes
  • Async workflows: up to 1440 (24 hours)

Intent vs Credential Access

Both create audit trails, but serve different purposes:
IntentCredential Access
Declared before any actionRequired to get PAN/CVV
Optional (but recommended)Required for card use
Matches against transactionsCorrelates with transactions
Has tolerance for varianceNo tolerance, just audit
For maximum accountability, use both:
  1. Create intent (declare what you’re about to do)
  2. Request credentials (get PAN/CVV with attestation)
  3. Make purchase
  4. Transaction matched to intent