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)status | What it means |
|---|---|
not_started | Nobody has applied yet. |
pending_verification | Applied; the card issuer is verifying the company, or needs what requirements_due lists. |
active | Cards can be issued. |
restricted | It was active, and the card issuer needs something again before cards are issued. |
rejected | The 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)| Option | Default | What it does |
|---|---|---|
limit_usd | required | The most the card spends per interval. |
interval | "month" | "transaction" (per charge), "day", "month" or "all_time". Days and months are UTC. |
merchants | any merchant | Merchant categories it may pay: names like "office_supplies", "software", "travel", "advertising", or 4-digit merchant category codes. |
agent | none | The agent that pays with it. A card has one agent at a time, and an agent holds one card. |
require_approval_above_usd | never | A purchase above this is approved by a person before it's paid. |
single_use | False | A 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 recordedpending_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 becomesapprovedand 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 becomesdeclined. The merchant's tries while the person decides are declined withawaiting_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:
| Tool | What it does |
|---|---|
card__status | What 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:
- Its browser
actreaches the checkout's payment step. With nothing announced yet it stops there, not done, and its result'spayment_duesays what the checkout charges (tax and shipping included) and who. - The agent announces that purchase with
card__purchase, which is ready at once or waits for a person as above. - Its next
acton 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
actends, not done, and nothing is paid. - For a person paying with a number from
reveal()orpurchase(), 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_reason | Why |
|---|---|
over_limit | It would take the card past limit_usd for the interval. |
over_budget | The card is an app's, and the charge is more than what's left of the app's budget this month. |
merchant_not_allowed | The merchant isn't in the card's merchants. |
amount_mismatch | It's more than the purchase its number was minted for. |
frozen | The card is frozen. |
insufficient_funds | Your card balance can't cover it. |
not_approved | The person asked declined it. |
approval_expired | Nobody answered within 30 minutes. |
awaiting_approval | The same merchant's earlier charge is still waiting on a person. |
card_closed | The card is closed, or the number already took its one charge. |
card_expired | The number is past its expiry. |
invalid_cvc, invalid_expiry | The checkout was given the wrong CVC or expiry. |
3ds_failed | The merchant's 3-D Secure check failed. |
issuer_declined | The 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) # submittedreason 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 reissuedIn 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
statusfrozenwithstatus_reasonnegative_balance, and declines every charge withinsufficient_funds.unfreeze()raisesnoclick.Conflictmeanwhile, and a card issued meanwhile starts frozen. funding.frozen_sincesays since when. Thecard.balance.negativeevent 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.fundedfollows 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_usdis 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
cardsscope; purchases and reveals needcards:reveal.
Errors
| Error | When |
|---|---|
noclick.NotAvailable | Cards 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.Conflict | An 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.PermissionDenied | The key lacks cards or cards:reveal, or the card belongs to another user. |
noclick.InvalidRequest | A limit at or below zero, an amount past what the card can spend now, or a merchant category that doesn't exist. |
noclick.QuotaExceeded | The account already has 100 open cards. |
Every method is in the Card reference, from Card.create to Card.funding.