Issue cards
A card is issued in one call. A virtual card works as soon as it is created, wherever a card number is taken. It is your business’s own unless you give it to one of your agents. A physical card belongs to a person and is posted to them.
Every card spends from the same balance, within limits that Tuppence and its card processor enforce. Eligible spend earns cashback.
Before you start
Section titled “Before you start”- Switch cards on in the dashboard, under Cards. Your business is reviewed first.
- On the same page, the owner adds themselves as the primary cardholder. Cards are issued in their name, as the person who answers for them.
- Top up the card balance by bank transferNothing to fund: the cards spend your earnings once they’ve clearedTop up the card balance by SEPA transfer to its own IBAN (below).
You don’t need an agent or a spending policy. Creating and changing cards needs a key with
cards:write; reading them needs cards:read.
What an agent’s restricted key can do
Section titled “What an agent’s restricted key can do”A restricted key can make a card stricter, never looser. With cards:write it can lower a limit or
add one, freeze a card, cancel it, and change its metadata.
It can also get its agent a card for one purchase, when the request is signed as that agent and the card is:
- for that agent only;
- single-use (
usage: "single_use"with anamount); - locked to one merchant (
merchant); - on the agent’s default spending policy. A person sets that policy first, and
amountcan’t be more than its limits allow.
Anything else that lets a card spend more needs a person (the dashboard or the app) or a secret key:
- any other card, and replacing or activating one;
- raising or removing a limit;
- changing a card’s spending policy, or an agent’s default policy;
- unfreezing a card that a person, another key or Tuppence froze. A restricted key can unfreeze a card it froze itself.
Otherwise it gets 403 permission_denied, and nothing changes. A card an agent’s key issues is
recorded as that key’s, in the audit log and on its card.created event’s request.
Create a virtual card
Section titled “Create a virtual card”By default, a virtual card is your business’s own, issued to the primary cardholder, active at once, with a limit of £500.00$500.00€500.00 a day.
curl https://api.tuppence.ai/v1/cards \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: team-tools-card" \ -H "Content-Type: application/json" \ -d '{"limits": {"monthly": 50000}}'import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);// A virtual card, up to £500 a month.const card = await tuppence.cards.create({ limits: { monthly: 50000 } });import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);// A virtual card, up to $500 a month.const card = await tuppence.cards.create({ limits: { monthly: 50000 } });import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);// A virtual card, up to €500 a month.const card = await tuppence.cards.create({ limits: { monthly: 50000 } });import osfrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])card = tuppence.cards.create(limits={"monthly": 50000}){ "id": "card_…", "object": "card", "livemode": false, "agent": null, "cardholder": "ich_…", "type": "virtual", "usage": "standing", "status": "active", "currency": "gbp", "brand": "Visa", "last4": "4242", "limits": { "per_authorization": null, "daily": null, "weekly": null, "monthly": 50000, "all_time": null }, "spend_policy": null, "merchant_lock": null, "metadata": {}}{ "id": "card_…", "object": "card", "livemode": false, "agent": null, "cardholder": "ich_…", "type": "virtual", "usage": "standing", "status": "active", "currency": "usd", "brand": "Visa", "last4": "4242", "limits": { "per_authorization": null, "daily": null, "weekly": null, "monthly": 50000, "all_time": null }, "spend_policy": null, "merchant_lock": null, "metadata": {}}{ "id": "card_…", "object": "card", "livemode": false, "agent": null, "cardholder": "ich_…", "type": "virtual", "usage": "standing", "status": "active", "currency": "eur", "brand": "Visa", "last4": "4242", "limits": { "per_authorization": null, "daily": null, "weekly": null, "monthly": 50000, "all_time": null }, "spend_policy": null, "merchant_lock": null, "metadata": {}}The response never includes the card’s number or CVC. People see those in the dashboard or app.
Where the owner allows it, a key made with card_details:read can read them to pay at a checkout:
see See a card’s details.
Parameters you can set on create:
| Parameter | What it does |
|---|---|
limits | per_authorization, daily, weekly, monthly and all_time, in minor units. Without it, the card has 500 a day in your currency |
usage | standing (the default: it keeps working) or single_use: one purchase, up to amount |
amount | The most a single-use card can spend, in minor units |
merchant | The one merchant it can be used at: name, network_id, or both |
spend_policy | The rules it follows (pol_…) |
agent | The agent it is for (agt_…). Leave it out for your business’s own card |
cardholder | Who it is issued to (ich_…). Defaults to the primary cardholder |
status | active (the default) or inactive, to activate it later |
metadata | Your own keys and values |
A card is issued in your account’s currency: GBP in the UK, USD in the US.
A card for one purchase
Section titled “A card for one purchase”A single-use card locked to one merchant pays that merchant once, up to its amount. It’s the safest card for a one-off purchase.
const once = await tuppence.cards.create({ usage: "single_use", amount: 8000, // £80.00 merchant: { name: "Acme Cloud" },});const once = await tuppence.cards.create({ usage: "single_use", amount: 8000, // $80.00 merchant: { name: "Acme Cloud" },});const once = await tuppence.cards.create({ usage: "single_use", amount: 8000, // €80.00 merchant: { name: "Acme Cloud" },});A card for an agent
Section titled “A card for an agent”Name an agent to give it its own card. The card follows the agent’s default rules unless you set others, and each purchase is checked against what is usual for that agent.
const agent = await tuppence.agents.create({ name: "Research agent" });const card = await tuppence.cards.create({ agent: agent.id, limits: { monthly: 50000 } });A request signed by an agent (agent identity) that names no agent creates a card for that agent. An agent reads only its own cards’ numbers.
Order a physical card
Section titled “Order a physical card”A physical card is for one person (one each). It is posted to them and activated once it arrives.
const card = await tuppence.cards.create({ type: "physical", cardholder: "ich_…", limits: { monthly: 100000 },});// Once it has arrived:await tuppence.cards.activate(card.id);It goes to the cardholder’s billing address unless you set shipping. The card’s shipping
tracks each move, and card.shipped is sent when it is on its way.
Top up the cards
Section titled “Top up the cards”A purchase must fit within what the cards can spend: spendable on GET /v1/issuing/balance.
Where that money comes from depends on your account’s country (spends_from).
UK cards spend the card balance, kept apart from the money you take. Top it up by bank transfer from any business bank account to the card balance’s own sort code and account number. Topping up is free, and a transfer usually arrives within minutes in banking hours.
| Step | Call |
|---|---|
| 1. See what the cards can spend | GET /v1/issuing/balance: spendable, with spends_from: issuing_balance |
| 2. Get the bank details | GET /v1/issuing/funding_instructions: gb.account_holder_name, gb.sort_code, gb.account_number. They’re your account’s own: save them as a payee and reuse them |
| 3. Send the transfer | From your bank. spendable rises when it lands |
| In test mode | POST /v1/test_helpers/issuing/fund with amount: the transfer arrives, spendable at once, and issuing_funding.available is sent |
# 1. What the cards can spend now.curl -sS --fail-with-body https://api.tuppence.ai/v1/issuing/balance \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY"
# 2. The card balance's own sort code and account number.curl -sS --fail-with-body https://api.tuppence.ai/v1/issuing/funding_instructions \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY"
# 3. Test mode: a £100.00 transfer arrives.curl -sS --fail-with-body https://api.tuppence.ai/v1/test_helpers/issuing/fund \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"amount": 10000}'import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);
// 1. What the cards can spend now.const balance = await tuppence.request("GET", "/v1/issuing/balance");console.log(balance.spendable, balance.currency, balance.spends_from); // … "gbp" "issuing_balance"
// 2. The card balance's own sort code and account number.const instructions = await tuppence.request("GET", "/v1/issuing/funding_instructions");console.log(instructions.gb); // { account_holder_name, sort_code, account_number }
// 3. Test mode: a £100.00 transfer arrives.const funding = await tuppence.request("POST", "/v1/test_helpers/issuing/fund", { body: { amount: 10000 },});console.log(funding.status, funding.amount); // available 10000import osfrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
# 1. What the cards can spend now.balance = tuppence.request("GET", "/v1/issuing/balance")print(balance["spendable"], balance["currency"], balance["spends_from"]) # … gbp issuing_balance
# 2. The card balance's own sort code and account number.instructions = tuppence.request("GET", "/v1/issuing/funding_instructions")print(instructions["gb"]) # account_holder_name, sort_code, account_number
# 3. Test mode: a £100.00 transfer arrives.funding = tuppence.request("POST", "/v1/test_helpers/issuing/fund", body={"amount": 10000})print(funding["status"], funding["amount"]) # available 10000GET /v1/issuing/funding lists the test transfers, newest first. Money still clearing can also
move onto the cards at once, for the instant fee: POST /v1/issuing/topups with
method: "instant_from_balance", where instant_payouts.destinations.card_balance on
GET /v1/payout_settings says it’s available.
Earnings that have cleared can move onto the cards too, free: POST /v1/issuing/topups with
{"method": "balance", "amount": 5000}. It’s decided like a payout (cleared money only, holds and
limits apply), answers in_transit with expected_available_at set to Stripe’s date, usually the
next day, and becomes available then. In live mode it’s being switched on account by account:
until it’s on for yours, the answer is topup_unavailable with reason: not_enabled and a
fix_url to top up by bank transfer.
A Trusted account can also top up by card at speed: instant: the card rate plus 0.99% on top,
spendable within a minute of the payment. Below Trusted it’s refused with reason: tier and a
fix_url to ask for higher limits.
US cards spend your earnings directly, once they’ve cleared, so there’s nothing to move.
spendable is what you could pay out right now, and a purchase counts toward your daily payout
limit. Earnings aren’t spendable in your account’s first seven days. To add money, top up by bank
transfer: POST /v1/issuing/topups with method: bank_transfer gives your card balance’s own
account and routing number. What you send is spendable as soon as it arrives, usually by the
next business day, and can be paid out only once it has cleared. The fee is 0.5%, at most $5, on
top. A Trusted account can also top up by card at speed: instant: the card rate plus 0.99% on
top, spendable at once, and payable out only once the payment clears. method: balance is
refused with already_spendable: the cards already spend your earnings.
| Step | Call |
|---|---|
| 1. See what the cards can spend | GET /v1/issuing/balance: spendable, with spends_from: payments_balance. limited_by, available_from and message say what holds it and when more clears |
| 2. Bank details | POST /v1/issuing/topups with {method: bank_transfer, amount}: next_action.bank_transfer has the routing and account number and the reference; send the top-up’s total. (GET /v1/issuing/funding_instructions still answers available: false, reason: spends_earnings) |
| In test mode | Take a payment with pm_card_bypassPending, then move the test clock seven days on. The cards can spend it |
# 1. What the cards can spend now, and what holds it.curl https://api.tuppence.ai/v1/issuing/balance \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY"
# 2. No bank details: available is false, reason spends_earnings.curl https://api.tuppence.ai/v1/issuing/funding_instructions \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY"import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);
// 1. What the cards can spend now, and what holds it.const balance = await tuppence.request("GET", "/v1/issuing/balance");console.log(balance.spendable, balance.spends_from, balance.limited_by, balance.message);
// 2. No bank details: the cards spend your cleared earnings.const instructions = await tuppence.request("GET", "/v1/issuing/funding_instructions");console.log(instructions.available, instructions.reason); // false "spends_earnings"import osfrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
# 1. What the cards can spend now, and what holds it.balance = tuppence.request("GET", "/v1/issuing/balance")print(balance["spendable"], balance["spends_from"], balance["limited_by"], balance["message"])
# 2. No bank details: the cards spend your cleared earnings.instructions = tuppence.request("GET", "/v1/issuing/funding_instructions")print(instructions["available"], instructions["reason"]) # False spends_earningsEuropean cards spend the card balance, kept apart from the money you take, as in the UK. They
are Visa commercial cards in euros, for business use. Top the balance up by SEPA transfer from any
business bank account to the card balance’s own IBAN. Topping up is free, and a transfer usually
arrives the next banking day. A purchase the balance can’t cover is declined, so keep an eye on
spendable. Moving earnings onto the cards, and topping up by card, aren’t offered in Europe yet.
| Step | Call |
|---|---|
| 1. See what the cards can spend | GET /v1/issuing/balance: spendable, with spends_from: issuing_balance |
| 2. Bank details | GET /v1/issuing/funding_instructions: type: eu_bank_transfer, and eu with account_holder_name, iban, bic and the IBAN’s country. Or POST /v1/issuing/topups with {method: bank_transfer, amount} for the same IBAN and a reference |
| In test mode | POST /v1/test_helpers/issuing/fund with amount: the transfer arrives and issuing_funding.available is sent |
Every cardholder needs a mobile number (phone_number): the card’s bank sends its 3-D Secure code
by text. The 3-D Secure screens are in English, French, German, Italian or Spanish, by the
cardholder’s language, and in English otherwise.
Cards issued by Stripe. Cards are issued by Stripe Technology Europe Limited, an electronic money institution authorized by the Central Bank of Ireland (firm reference number: C187865). Cards are issued under the Visa card scheme pursuant to a license from Visa Europe Limited.
Reading the balance and the bank details needs issuing_funding:read; the test helper needs
test_helpers:write. Before cards are switched on, each call answers 403 issuing_not_enabled.
Limits
Section titled “Limits”A card always has at least one limit. Change them at any time; null removes one.
await tuppence.cards.setLimits(card.id, { monthly: 100000, per_authorization: 5000 });Your account also has limits: how many cards it can create and what they can spend together. A card’s limits can only be tighter. See Card limits.
A spending policy (/v1/spend_policies in the API reference) sets which kinds
of merchant and which countries a card may pay, and the amount above which a person must approve a
purchase first. Set one when you create the card or later. Without one, an agent’s card follows the
agent’s default, and any other card is stopped only by its limits.
await tuppence.cards.update(card.id, { spend_policy: "pol_…" });Freeze, unfreeze, replace or cancel
Section titled “Freeze, unfreeze, replace or cancel”await tuppence.cards.freeze(card.id); // every purchase is declined until it is unfrozenawait tuppence.cards.unfreeze(card.id);await tuppence.cards.cancel(card.id); // for goodReplace a lost or stolen standing card with POST /v1/cards/{id}/replace and a reason. The old
card stops, and the new one keeps the holder, agent, limits and rules. A single-use card can’t be
replaced: cancel it and create another. A physical card can also be replaced when damaged or
expired; the old one keeps working until the new one is activated.
Events
Section titled “Events”card.created is sent when a card is issued, and card.updated whenever its status, limits or
rules change. Each carries the card. See Webhooks.
Errors
Section titled “Errors”| Code | HTTP | Means |
|---|---|---|
parameter_invalid | 400 | A value isn’t one the API takes. param names it: a disabled agent, an agent on a physical card, a currency that isn’t your account’s |
parameter_missing | 400 | A single-use card without amount, or no primary cardholder yet to issue it to (add one on the dashboard’s Cards page, or pass cardholder) |
resource_missing | 404 | No such agent, cardholder or spending policy in this account and mode |
resource_limit_reached | 400 | The person already has a physical card |
issuing_card_limit | 403 | Over one of the account’s card limits |
permission_denied | 403 | A restricted key tried to loosen a card. See what an agent’s restricted key can do |