See a card's details
People see a card’s number, expiry and CVC on screen, inside secure card fields. Your code or an agent can read an active virtual card’s details through the API, but only if the account’s owner allows it and only with a key made for it.
For people
Section titled “For people”In the dashboard, open a card and choose Show details (number, expiry, CVC) or, for a physical card, Show PIN. In the app, open the card and confirm with Face ID.
| Rule | |
|---|---|
| Fresh second factor | Confirm it’s you if you haven’t in the last 10 minutes: authenticator code in the dashboard, Face ID in the app |
| Who sees which card | An owner or admin: any of the account’s cards. Anyone else: only a physical card issued to them |
| Active cards only | A physical card shows details once it has arrived and been activated. Only a physical card has a PIN |
| Inside secure frames | The card processor’s secure elements draw the details in frames on the page. They never pass through Tuppence’s servers or the page’s own code, so they can’t reach a log. Copy buttons copy each field |
| Hides itself | After a minute, or as soon as you leave the page or app. Hide hides them sooner |
| Audited | Each view is logged as card.details_viewed: who, which card, when. Never the details |
| Rate | Five looks a minute, then wait a moment and try again |
Through the API
Section titled “Through the API”To pay at a web checkout, code or an agent needs the number, CVC and expiry for the merchant’s
payment form. GET /v1/cards/{id}/details returns them for an active virtual card: your
business’s own, or an agent’s. A request an agent signs reads only that agent’s own cards. This is
the one place card numbers leave the card processor through Tuppence, so every condition below must hold.
1. The owner switches it on
Section titled “1. The owner switches it on”Only the account’s owner can do this. In the dashboard, go to Settings → Cards and switch on Card numbers through the API. It asks for a fresh second factor and is recorded in the audit log. Switching it off stops every read at once.
2. Create a restricted key with card_details:read
Section titled “2. Create a restricted key with card_details:read”Create a restricted key (rk_…, in Developers → API keys → Restricted) with the
card_details:read scope. Give it to the code or agent that pays, with no other scopes it doesn’t
need.
card_details:read is never implied. A secret key doesn’t have it, no team role has it, and a
key with every other scope still doesn’t. A key has it only when made with it by name.
3. Read the card’s details
Section titled “3. Read the card’s details”The safest card to read is a single-use card locked to the merchant
(create_agent_card over MCP, or POST /v1/cards with a
merchant). It pays that merchant once, up to that amount, and nothing else.
curl https://api.tuppence.ai/v1/cards/card_…/details \ -H "Authorization: Bearer $TUPPENCE_CARD_DETAILS_KEY"import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_CARD_DETAILS_KEY); // rk_…const card = await tuppence.cards.retrieveDetails("card_…");// Type card.number, card.cvc, card.exp_month and card.exp_year into the merchant's// payment form, and nowhere else. Don't keep them.import osfrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_CARD_DETAILS_KEY"]) # rk_…card = tuppence.cards.retrieve_details("card_…")# Type card.number, card.cvc, card.exp_month and card.exp_year into the merchant's# payment form, and nowhere else. Don't keep them.{ "object": "card_details", "card": "card_…", "number": "4000 00…", "cvc": "•••", "exp_month": 12, "exp_year": 2029, "livemode": false}numberis the whole card number, with no spaces.- The response has
Cache-Control: no-store. The SDKs send no idempotency key with it, cache nothing and log nothing. - Over MCP, the same read is the
get_card_detailstool.
Never keep the number
Section titled “Never keep the number”The number and CVC are all anyone needs to spend on the card. Use them for one checkout, then let them go.
- Never store, print or log them: not in a database, file, cache, prompt, transcript, error report or debug line. Read them again for the next checkout.
- Send them only to the merchant’s payment form. Never to another tool, another agent, or a person in chat.
- 3-D Secure goes to the cardholder. If the checkout asks for a code sent to a phone, that code goes to the person the card is issued to. Stop and ask a person; don’t try to get past it. See 3-D Secure.
Guardrails
Section titled “Guardrails”| Guardrail | |
|---|---|
Restricted key with card_details:read | Never a secret key, a dashboard or app session, or a team role |
| Owner opt-in | Per account, by its owner, with a fresh second factor; audited |
| Platform flag | agent_card_details, off in production until PCI DSS service-provider compliance is confirmed |
| A virtual card, active | Your business’s own or an agent’s. A physical card is refused (its person sees it in the dashboard or app), as is a frozen, cancelled or not yet activated card |
| One agent, its own cards | A request signed by an agent (agent identity) reads only that agent’s cards: not another agent’s, and not your business’s own |
| Rate limited | Ten reads a minute per key: one checkout at a time |
| Audited and evented, without the number | Every read is in the audit log (card.details_retrieved) and sent as a webhook event. Neither carries the number or CVC |
| Never in a log | no-store; not kept in your request logs or with an idempotency key; the number is stored nowhere |
The event
Section titled “The event”card.details_retrieved is sent on every read, so the business sees each one and can alert on any
it doesn’t expect:
{ "object": "card_details_retrieval", "card": "card_…", "agent": "agt_…", "api_key": "rk_…", "retrieved_at": 1791374400, "livemode": false}agent is the card’s agent, or null for your business’s own card. api_key is the key’s id, not
its secret. See Webhooks.
In test mode
Section titled “In test mode”| Where | Answer |
|---|---|
| Local development and CI (the simulated gateway) | A generated test number: card-shaped, passes the Luhn check, can’t pay anywhere |
| Test mode on Tuppence’s servers | No number exists for test cards: 409 card_details_unavailable. Build and test against your local setup |
Errors
Section titled “Errors”| Code | HTTP | Means |
|---|---|---|
permission_denied | 403 | Not a restricted key with card_details:read (a secret key never has it), or the card is physical, or an agent signed the request and the card isn’t its own |
insufficient_role | 403 | A dashboard or app session. People see card details in the secure frames, not through the API |
feature_not_enabled | 403 | The owner hasn’t switched it on (Settings → Cards), or the platform flag is off here (production, for now) |
invalid_state | 400 | The card isn’t active: frozen, cancelled or not yet activated |
resource_missing | 404 | No such card in this account and mode |
card_details_unavailable | 409 | No number exists for this card: a test card outside local development |
rate_limited | 429 | More than ten reads a minute with this key. Wait for Retry-After |