Cards

Virtual cards with limits and approvals, for agents that buy things.

A card is a virtual card number for an agent, or your own code, that has to pay for something: office supplies, a software seat, a flight. Each card has a spending limit, the kinds of merchant it may pay, and a threshold above which a person approves a purchase before anything is paid.

Cards are your company's commercial cards, for its business, in US dollars. They spend your company's own card balance, which Stripe Payments Company holds and you fund by bank transfer: what a card pays isn't charged to your NoClick account. A card number is only ever shown by the card issuer's own element on a NoClick page, so it never passes through NoClick, your code or a model.

Turn on card issuing

Before its first card, your company applies once for card issuing, in the developer console. The card issuer verifies the business: its legal name, tax ID and address, a representative, and anyone who owns more than 25% of it. Card issuing is open to US-registered businesses.

import noclick

program = noclick.Card.program()
print(program.status, program.requirements_due)
if program.status != "active":
    print("Apply at", program.onboarding_url)
statusWhat it means
not_startedNobody has applied yet.
pending_verificationApplied; the card issuer is verifying the company, or needs what requirements_due lists.
activeCards can be issued.
restrictedIt was active, and the card issuer needs something again before cards are issued.
rejectedThe card issuer declined the application.

Until it's active, issuing a card raises noclick.NotAvailable with the program's status.

Issue a card

concierge = noclick.Agent.from_name("concierge", user="acme")
card = noclick.Card.create(
    "Office supplies",
    limit_usd=1500,
    interval="month",
    merchants=["office_supplies"],
    agent=concierge,
    require_approval_above_usd=200,
    user="acme",
)
print(card.last4, card.brand, card.exp_month, card.exp_year)
OptionDefaultWhat it does
limit_usdrequiredThe most the card spends per interval.
interval"month""transaction" (per charge), "day", "month" or "all_time". Days and months are UTC.
merchantsany merchantMerchant categories it may pay: names like "office_supplies", "software", "travel", "advertising", or 4-digit merchant category codes.
agentnoneThe agent that pays with it. A card has one agent at a time, and an agent holds one card.
require_approval_above_usdneverA purchase above this is approved by a person before it's paid.
single_useFalseA fresh number for every checkout, each taking one charge.

noclick.Card.merchant_categories() lists every category name with the codes it covers. Whatever merchants allows, a card never pays for cash, money transfers, stored value, gambling or securities.

Names are unique among your open cards, per user; a name that's taken raises noclick.Conflict with the card's existing_id. A card issued inside as_user (or with user=) is for that user: only that user's agents can pay with it. It's still your company's card, paid from your company's balance, never one the user owns or funds.

Keep the card in your code

from_name with create_if_missing=True issues the card the first time it's used and, after that, keeps it in step with your code: a different limit, interval, merchant list, threshold, agent or single_use is updated to match.

card = noclick.Card.from_name(
    "Travel",
    create_if_missing=True,
    limit_usd=3000,
    interval="month",
    merchants=["travel"],
    agent=noclick.Agent.from_name("assistant", user="user_42"),
    user="user_42",
)

Approve before paying

A purchase is announced before anything is paid: its amount (tax and shipping included), the merchant and why. At or under the card's require_approval_above_usd it's ready at once; above it, a person approves it first. Either way it gets a number of its own that takes exactly one charge of at most that amount, then closes.

card = noclick.Card.from_name("Office supplies", user="acme")
purchase = card.purchase(640.00, "Herman Miller", reason="A chair for the new hire")
details = purchase.wait()   # once approved
print(details.amount_usd, details.last4, details.closes_at)

purchase returns a card.purchase operation: wait() returns the CardDetails to pay with, and raises noclick.PolicyDenied if the person declines, or noclick.Expired if nobody answers within 30 minutes. The person asked is the card's user, or your account's owner for an account card (the same people as in Who approves), by email, with the amount, merchant, reason and card; the approval also shows in the developer console, and the card.authorization.requested event goes to your webhooks. A purchase needs the key's cards:reveal scope, since it hands out a number.

An agent does this itself (see Pay at a checkout): it announces the purchase with the total its browser found at the checkout, waits for the approval when one is needed, and NoClick fills in the number. Because the approval comes before the payment, no merchant ever sees a decline, so no fare or basket expires while the person decides.

The person has 30 minutes to answer, which suits someone at hand: the user who asked the agent to buy, or someone in the console. For an approval that takes hours, such as a finance team signing off, approve on the books side instead: the agent raises the purchase as a bill or a request in your accounting tools, or asks with Human.approve, and pays once it's approved, with an amount under the card's threshold.

A charge nobody announced

A charge can reach a card without a purchase first: your own code paying with a revealed number, or a merchant charging more than it said. Each one is judged as it happens:

  • Within the limit, at an allowed merchant and at or under the threshold, it's approved.
  • Over the limit, at another kind of merchant, on a frozen card or beyond your card balance, it's declined.
  • Above require_approval_above_usd, it's declined to the merchant at once and recorded pending_approval, because card networks give a charge only a few seconds, too short for a person. A person is asked, as for a purchase. Once they approve, the transaction becomes approved and the same merchant's next try, for up to that amount (plus 5% for tax or shipping) within 30 minutes, goes through. If they decline, or don't answer in 30 minutes, it becomes declined. The merchant's tries while the person decides are declined with awaiting_approval, without asking again.

The held charge's approval_id is the approval's operation, so your code can wait on it:

card = noclick.Card.from_name("Office supplies", user="acme")
for charge in card.transactions(status="pending_approval"):
    approved = noclick.Operation.from_id(charge.approval_id).wait()
    print(charge.merchant, charge.amount_usd, "approved" if approved else "declined")

A fresh number for every checkout

With single_use=True, the card is one set of limits behind many numbers: every purchase, and every reveal(), mints a number of its own that takes one charge and then closes. A merchant that keeps card numbers on file can never charge that one again, and a number that leaks is already dead. Together the numbers spend within the card's limit_usd per interval, and every charge lists under the one card with the last4 of the number it was on. A number nobody pays with closes after an hour.

buyer = noclick.Agent.from_name("buyer")
card = noclick.Card.create("Marketplace", limit_usd=500, interval="month", agent=buyer, single_use=True)

last4 and the expiry are the latest number's; a single-use card has none until its first checkout. Use a reusable card for subscriptions, which charge the same number every month. A merchant can still post a charge after a number closes (card network rules let it); it lists under the card like any other.

Give it to an agent

An agent pays with the card it holds: pass agent= when you issue the card, or card= on Agent.create, Agent.from_name or agent.update (see Agents). Either way, card.agent_id names the agent and agent.card_id the card. A user's agent holds one of that user's cards, an account agent one of your account's. A card has one agent at a time: giving an agent a card another agent holds raises noclick.Conflict naming that agent, and an agent that expires or is deleted gives its card back. Giving a card takes the key's cards scope.

buyer = noclick.Agent.from_name(
    "buyer",
    create_if_missing=True,
    instructions="You order office supplies for the team. Check what the card can spend before you buy.",
    tools=["browser", "search"],
    card=noclick.Card.from_name("Office supplies"),
)

The agent gets two tools, which your policy names card.status and card.purchase:

ToolWhat it does
card__statusWhat the card can spend now: its limit, what's left of it and when that resets, the approval threshold and the merchant categories.
card__purchase(amount_usd, merchant, reason)Approve before paying: at or under require_approval_above_usd a number for exactly that amount is ready at once; above it the turn waits, its operation awaiting_human, for the person the card answers to, and what came of it (approved, declined or expired) is the conversation's next turn.

The model never sees the number: it reads the card's last four digits, the amount and when the number closes. It pays at checkouts in its browser, where NoClick fills in the number from the card issuer itself, so the number never enters the model's context, a reply or a log, and payment fields are masked in every screenshot and page the model reads. An agent's purchase has no reveal page, so nobody else sees its number either; it's a card.purchase operation with the agent's agent_id, so noclick.Operation.list(kind="card.purchase", agent=buyer) lists what it bought.

A turn answering someone outside the agent's scope (a stranger's email, a caller) isn't offered card__purchase, so what they write never spends your money, and a test run is offered neither tool. A child the agent spawns pays with the same card only when it's given "card" (or "card.status", "card.purchase") in its tools, as with any of its parent's tools.

Pay at a checkout

An agent holding a card pays in its browser:

  1. Its browser act reaches the checkout's payment step. With nothing announced yet it stops there, not done, and its result's payment_due says what the checkout charges (tax and shipping included) and who.
  2. The agent announces that purchase with card__purchase, which is ready at once or waits for a person as above.
  3. Its next act on that page pays with it. The model types the expiry, the cardholder's name and the billing address (your company's), and names the card number and CVC fields; NoClick fills those two from the card issuer's own element, through the browser itself, so the number never passes through NoClick, a model or a screenshot. A purchase pays a checkout charging at most its amount.

The card's number and CVC fields are masked (drawn as dots, and left out of the page the model reads) in every screenshot and page the model sees, and in what screenshot and content return; no script runs on a page while an agent has a card.

One-time codes

A merchant can ask the card issuer to confirm a payment with a one-time code (3-D Secure). Each card's codes go to an address NoClick holds for that card, never to a person's phone, so they reach the checkout that's paying:

  • At an agent's checkout NoClick types the code into the merchant's form for the model, which never reads it. If none arrives within two minutes, the act ends, not done, and nothing is paid.
  • For a person paying with a number from reveal() or purchase(), the code shows on that card's page while it's open.

A code lasts ten minutes and is used once. In the US a card issuer asks for codes only once your company's card program is enrolled in 3-D Secure; until then a merchant's 3-D Secure check passes without one.

See what it spent

card = noclick.Card.from_name("Office supplies", user="acme")
print(card.spent_usd, "of", card.limit_usd, "this", card.interval, "until", card.resets_at)

for charge in card.transactions(limit=20):
    print(charge.at, charge.merchant, charge.amount_usd, charge.status, charge.decline_reason)

spent_usd is what the card spent in its current interval (this calendar month's spend for a per-transaction limit), and resets_at is when it starts over: midnight UTC for a day, the 1st of the month UTC for a month. transactions lists charges and refunds newest first, 20 unless you pass limit (None for all of them). A charge is approved while the merchant holds the amount, settled once it's taken (settled_amount_usd, which can differ for tips or shipping), reversed if it's released untaken, or declined. A refund has kind refund and an amount_usd below zero. A declined charge says why:

decline_reasonWhy
over_limitIt would take the card past limit_usd for the interval.
over_budgetThe card is an app's, and the charge is more than what's left of the app's budget this month.
merchant_not_allowedThe merchant isn't in the card's merchants.
amount_mismatchIt's more than the purchase its number was minted for.
frozenThe card is frozen.
insufficient_fundsYour card balance can't cover it.
not_approvedThe person asked declined it.
approval_expiredNobody answered within 30 minutes.
awaiting_approvalThe same merchant's earlier charge is still waiting on a person.
card_closedThe card is closed, or the number already took its one charge.
card_expiredThe number is past its expiry.
invalid_cvc, invalid_expiryThe checkout was given the wrong CVC or expiry.
3ds_failedThe merchant's 3-D Secure check failed.
issuer_declinedThe card issuer declined it (suspected fraud, or a network refusal).

Show a number to a person

When a person, not an agent, has to pay, reveal gives them a page where the card issuer's own element shows the number, CVC and expiry:

card = noclick.Card.from_name("Software")
details = card.reveal()
print(details)   # the url stays out of logs
send_to_finance(details.url)

The page shows the number for 10 minutes to whoever opens it, so treat url like the number itself and send it only to the person who pays. On a single-use card each reveal is a fresh number. CardDetails also carries the cardholder name and billing_address a checkout asks for: your company's. Revealing needs a key with the cards:reveal scope, which no session and no other scope includes, and every reveal is recorded with the key that made it.

Refunds and disputes

A merchant's refund arrives as a refund transaction and goes back to your card balance, and the card.transaction.refunded event goes to your webhooks. When a settled charge is wrong, dispute it with the card network:

card = noclick.Card.from_name("Office supplies", user="acme")
[charge] = card.transactions(status="settled", limit=1)
dispute = card.dispute(charge, reason="not_received", explanation="Ordered Sep 2, never delivered.")
print(dispute.status)   # submitted

reason is fraudulent (nobody at your company made the charge), canceled, duplicate (name the original in duplicate_of), not_received, not_as_described (say whether it was merchandise or a service in product) or incorrect_amount (with the amount_usd in dispute). A purchase your agent made by mistake isn't fraud: dispute it with the reason that fits, after asking the merchant for a refund. The network decides within 30 to 90 days; card.dispute.updated events follow it to won, lost or expired, and a dispute won puts the money back on your card balance.

Change, freeze or close a card

card = noclick.Card.from_name("Office supplies", user="acme")
card.update(limit_usd=2000, require_approval_above_usd=300)
card.update(merchants=None)   # any merchant from now on

card.freeze()     # declines every charge on every number; keeps its numbers and limits
card.unfreeze()
card.close()      # for good: its numbers close and are never reissued

In update, None clears a field: merchants=None allows any merchant, require_approval_above_usd=None never asks, agent=None takes the card away from its agent. Changes apply from the next charge. Turning single_use on closes the card's reusable number. A closed card keeps its transactions and is listed with status="closed"; purchases on it still waiting for a person are canceled.

Fund your cards

Every card, your account's and every user's, spends from your company's card balance, which Stripe Payments Company holds. You add to it by bank transfer to the account details the console shows:

funding = noclick.Card.funding()
print(funding.balance_usd, funding.pending_usd, funding.available_usd)
bank = funding.bank_transfer
print(bank.bank_name, bank.routing_number, bank.account_number, bank.networks)

A wire lands the same day; an ACH transfer takes a few business days. pending_usd is held by charges that haven't settled, and available_usd is what cards can still spend. A charge the balance can't cover is declined with insufficient_funds.

The balance can drop below zero when a merchant takes a charge without asking (a late tip, a charge posted after a number closed). Then NoClick freezes every card until it's funded again:

  • Each card reads status frozen with status_reason negative_balance, and declines every charge with insufficient_funds. unfreeze() raises noclick.Conflict meanwhile, and a card issued meanwhile starts frozen.
  • funding.frozen_since says since when. The card.balance.negative event goes to your webhooks, and your account's owner gets an email.
  • Once a transfer brings the balance back to zero or above, card.balance.funded follows and the cards pay again. A card you froze yourself stays frozen.

Limits and billing

  • Each card number costs $0.10, charged to your account when it's issued: a reusable card's once, a single-use card's per checkout. What a card pays comes out of your card balance, not your account, so it doesn't count toward spend caps: the card's limit_usd is its cap.
  • An account has up to 100 open cards; issuing more raises noclick.QuotaExceeded. Single-use numbers don't count.
  • A charge above $10,000 is declined by the card issuer unless your company has asked for a higher ceiling.
  • Cards are issued in the US, in dollars, whatever region your account uses; a charge in another currency is converted, with the card network's fees.
  • Managing cards needs the key's cards scope; purchases and reveals need cards:reveal.

Errors

ErrorWhen
noclick.NotAvailableCards aren't enabled on this deployment (no program_status), or your company's card issuing isn't active (the error's program_status says where it stands, and requirements_due what's still needed).
noclick.ConflictAn open card of the same user already has that name (existing_id names it), the agent already holds a card, the card is frozen or closed (or unfreeze() while your card balance is below zero), or a dispute the network won't take.
noclick.PermissionDeniedThe key lacks cards or cards:reveal, or the card belongs to another user.
noclick.InvalidRequestA limit at or below zero, an amount past what the card can spend now, or a merchant category that doesn't exist.
noclick.QuotaExceededThe account already has 100 open cards.

Every method is in the Card reference, from Card.create to Card.funding.