Skip to main content

Spending Intents

Intents are pre-transaction declarations that create an audit trail and enable automatic transaction matching. They answer the question: “What did the agent claim it was going to do?”
Terminology note: In LedgerOS, “Intent” refers to a spending intent — a declaration of what your agent plans to purchase. This is different from “intent” in LLM/NLU contexts (user intent classification) or Stripe’s PaymentIntent (a payment lifecycle object).Think of it as: “Before spending, declare your intent.”
Intents work with all card types. When a transaction arrives, it’s automatically matched against pending intents for that card.

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.)

Automatic Matching

When a transaction webhook arrives from the payment network, the system automatically:
  1. Finds any pending intent for the card
  2. Compares the transaction amount against expectedAmount (within tolerance)
  3. Compares the merchant name against expectedMerchant (case-insensitive partial match)
  4. Updates the intent status to matched or mismatched
  5. Emits a webhook event (intent.matched or intent.mismatched)

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