Card Details
List cards, and retrieve PAN, expiry, CVV and PIN behind a wallet-signature confirmation.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How to obtain the corporation token |
| Permissions | Card visibility depends on the caller's permissions |
| Issuing a Card | Where {cardId} comes from |
Overview
Card data splits in two. The card record — status, format, limits, delivery address, masked digits — is
returned by the card list. The sensitive values — PAN, expiry, CVV and PIN — are behind a second gate:
each read requires a one-time action token, obtained by signing a message with the employee's wallet.
List Cards
GET /api/v1/cards
Requires CardSu or CardView.
Query parameters:
| Parameter | Description |
|---|---|
employee_id | Return only the cards linked to this employee. Only honoured for card administrators |
page_number | 1-indexed page number |
page_size | Page size |
Who Sees What
| Caller holds | Sees | employee_id filter |
|---|---|---|
Su or CardSu | Every card in the corporation | Honoured. The employee must belong to the corporation |
CardView only | Only the cards linked to their own employee record | Silently ignored |
A
CardView-only caller that sendsemployee_idgets their own cards back, with no error. The
filter is dropped, the response is200, and it looks like the target employee holds exactly the
caller's cards. Do not build a team overview onCardViewalone — it needsCardSu.
A card issued without
employee_idis linked to nobody. It is invisible to every non-administrator
and cannot be linked afterwards — there is no link endpoint.
Response:
{
"data": [
{
"id": "64120850-73a1-4df5-a074-d463258c9deb",
"card_wallet_address": "0xAAFF0821A09A1Aac28B72dD3Ff410A7ea5FEb874",
"status": "Active",
"status_reason": "",
"previous_status": "NotActivated",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:30:00Z",
"card_data": {
"name_on_card": "Alex Grey",
"payment_system": "Visa",
"card_number_last_4": "5678",
"expiry_date": "01/2028",
"format": "Virtual",
"card_name": "Marketing team card"
},
"limit": {
"daily_limit": 1000.0,
"daily_usage": 200.0,
"monthly_limit": 10000.0,
"monthly_usage": 2000.0,
"transaction_limit": 500.0,
"currency": "USD"
},
"delivery_address": {
"line1": "10 Downing Street",
"line2": "Flat 2",
"city": "London",
"state": "",
"zip_code": "SW1A 2AA",
"country": "GB"
},
"allowed_actions": [
{ "type": "Block", "relative_path": "/api/v1/cards/:cardId/block" },
{ "type": "SetLimit", "relative_path": "/api/v1/cards/:cardId/limit" },
{ "type": "GetDetails", "relative_path": "/api/v1/cards/:cardId/details" },
{ "type": "GetCvv", "relative_path": "/api/v1/cards/:cardId/cvv" },
{ "type": "GetPin", "relative_path": "/api/v1/cards/:cardId/pin" },
{ "type": "Close", "relative_path": "/api/v1/cards/:cardId/close" }
]
}
]
}| Field | Description |
|---|---|
id | Card id — the {cardId} path parameter everywhere else |
card_wallet_address | The card's on-chain address |
status | Requested, NotActivated, Active, Blocked, Closed |
status_reason | Short reason for the current status, e.g. User |
previous_status | Status before the current one. Absent for a card that has never transitioned |
card_data.name_on_card | Name embossed on the card |
card_data.payment_system | Visa or MasterCard |
card_data.card_number_first_4 | First 4 digits. Present only while Requested or NotActivated |
card_data.card_number_last_4 | Last 4 digits. Present once past Requested and NotActivated |
card_data.expiry_date | MM/YYYY. Present once past Requested and NotActivated |
card_data.format | Plastic, Virtual or Metal |
card_data.card_name | Display name |
limit | Current limits and usage — see Managing a Card |
delivery_address | Shipping address for a plastic card. Note zip_code, not postal_code |
allowed_actions[] | Actions permitted in the current status, with the endpoint for each |
allowed_actions reflects card status only. It does not account for the caller's permissions: a
CardView-only employee still sees Close listed on an Active card and gets 403 when they call it.
Code
const response = await fetch(`${baseUrl}/api/v1/cards?page_number=1&page_size=25`, {
headers: { 'Authorization': `Bearer ${corporationToken}` }
});
const { data: cards } = await response.json();response = requests.get(
f"{base_url}/api/v1/cards",
headers={"Authorization": f"Bearer {corporation_token}"},
params={"page_number": 1, "page_size": 25},
)
cards = response.json()["data"]req, _ := http.NewRequest("GET", baseURL+"/api/v1/cards?page_number=1&page_size=25", nil)
req.Header.Set("Authorization", "Bearer "+corporationToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var cardsResp struct {
Data []CardResponse `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&cardsResp)There is no endpoint that returns a single card by id. Read the list and filter on id.
Reading Sensitive Card Data
PAN, expiry, CVV and PIN each require an action token: a short-lived confirmation that the employee
holding the corporation token also controls the wallet behind it. The flow is sign, exchange, read.
Build message → sign with employee wallet → POST /confirmation/signature/verify → action_token
↓
POST /api/v1/cards/{cardId}/details | /cvv | /pin
Step 1: Build and Sign the Message
The message is a fixed template:
By signing this I confirm that I am executing action <action_type> at <nonce>
| Placeholder | Value |
|---|---|
<action_type> | GetCardDetails for any card read. 3dsChallenge exists for the 3D Secure flow and is not accepted by the card-detail endpoints |
<nonce> | Unix timestamp in seconds, at the moment the message is formed |
Sign it with the employee's wallet key — the address in the corporation token's employee_address
claim. EVM addresses use a personal-sign (EIP-191 text hash) signature submitted as a 0x hex string;
Stellar addresses sign the SHA-256 hash of the message and submit a base64 signature.
Step 2: Exchange the Signature
POST /api/v1/confirmation/signature/verify
Requires CardSu or CardViewDetails.
Request body:
{
"action_type": "GetCardDetails",
"message_signature": "0x1234567890abcdeffedcba0987654321...",
"nonce": 1704106800
}| Field | Type | Required | Description |
|---|---|---|---|
action_type | string | Yes | GetCardDetails to read card data. Matched case-insensitively. 3dsChallenge is the only other accepted value and does not unlock card data |
message_signature | string | Yes | Signature over the message from Step 1 |
nonce | integer | Yes | The same unix timestamp used to build the message |
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}| Field | Description |
|---|---|
token | Action token. Pass it as action_token on the next call |
The nonce expires after 120 seconds. A
nonceolder than that is rejected with400 ErrorInvalidFieldandInvalid nonce. Generate the nonce, sign and exchange in one interaction —
do not pre-generate signatures.
Use
GetCardDetailsfor every card read. OneGetCardDetailstoken is accepted by all three
endpoints — PAN and expiry, CVV, and PIN. You do not need a separate token per field.
3dsChallengeunlocks none of them. It is a different purpose, minted for the 3D Secure flow, and
all three card-detail endpoints refuse it with400and
"error_details": [{"key": "action_token", "details": "invalid_purpose"}]. Sending it to the PIN
endpoint fails exactly like sending it to the PAN endpoint.
POST /api/v1/confirmation/signatureis the obsolete form of the same exchange. Use
/signature/verify.
Step 3: Read the Data
Each endpoint requires CardSu or CardViewDetails, and the card must be Active.
Get PAN and Expiry
POST /api/v1/cards/{cardId}/details
Request body:
{
"action_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Response:
{
"card_number": "4111111111111111",
"expiry_date": "01/2028"
}| Field | Description |
|---|---|
card_number | Full PAN |
expiry_date | Expiry date, MM/YYYY |
Get CVV
POST /api/v1/cards/{cardId}/cvv
Response:
{
"cvv": "123"
}Get PIN
POST /api/v1/cards/{cardId}/pin
Response:
{
"pin": "1234"
}Code
const nonce = Math.floor(Date.now() / 1000);
const message = `By signing this I confirm that I am executing action GetCardDetails at ${nonce}`;
const signature = await walletClient.signMessage({ account, message });
const tokenResponse = await fetch(`${baseUrl}/api/v1/confirmation/signature/verify`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ action_type: 'GetCardDetails', message_signature: signature, nonce })
});
const { token: actionToken } = await tokenResponse.json();
const detailsResponse = await fetch(`${baseUrl}/api/v1/cards/${cardId}/details`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ action_token: actionToken })
});
const details = await detailsResponse.json();nonce = int(time.time())
message = f"By signing this I confirm that I am executing action GetCardDetails at {nonce}"
signature = sign_message(message) # employee wallet key
token_response = requests.post(
f"{base_url}/api/v1/confirmation/signature/verify",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={"action_type": "GetCardDetails", "message_signature": signature, "nonce": nonce},
)
action_token = token_response.json()["token"]
details_response = requests.post(
f"{base_url}/api/v1/cards/{card_id}/details",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={"action_token": action_token},
)
details = details_response.json()nonce := time.Now().Unix()
message := fmt.Sprintf("By signing this I confirm that I am executing action GetCardDetails at %d", nonce)
signature := signMessage(message) // employee wallet key
tokenBody, _ := json.Marshal(map[string]interface{}{
"action_type": "GetCardDetails",
"message_signature": signature,
"nonce": nonce,
})
tokenReq, _ := http.NewRequest("POST", baseURL+"/api/v1/confirmation/signature/verify", bytes.NewBuffer(tokenBody))
tokenReq.Header.Set("Authorization", "Bearer "+corporationToken)
tokenReq.Header.Set("Content-Type", "application/json")
tokenResp, _ := http.DefaultClient.Do(tokenReq)
defer tokenResp.Body.Close()
var actionTokenResp struct {
Token string `json:"token"`
}
json.NewDecoder(tokenResp.Body).Decode(&actionTokenResp)
detailsBody, _ := json.Marshal(map[string]string{"action_token": actionTokenResp.Token})
detailsReq, _ := http.NewRequest("POST", baseURL+"/api/v1/cards/"+cardId+"/details", bytes.NewBuffer(detailsBody))
detailsReq.Header.Set("Authorization", "Bearer "+corporationToken)
detailsReq.Header.Set("Content-Type", "application/json")
detailsResp, _ := http.DefaultClient.Do(detailsReq)
defer detailsResp.Body.Close()Your Responsibilities
- PAN, CVV and PIN cross your systems in clear text. Do not log, cache or persist them.
- The action token authorises a read. Treat it as the data it unlocks.
- The signing key belongs to the employee. An integration that signs on the employee's behalf is
holding their credential — scope and store it accordingly.
Error Handling
{
"error_reason": "ErrorInvalidStatus",
"error_description": "Action is not allowed",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "action_type", "details": "GetDetails" },
{ "key": "value", "details": "Blocked" }
]
}Validation Errors (400)
| Error Reason | Error Details | Description | Resolution |
|---|---|---|---|
ErrorMissingField | field: action_type | action type is required | Send GetCardDetails or 3dsChallenge |
ErrorInvalidField | field: action_type | Invalid value for action type | Only those two values are recognised |
ErrorMissingField | field: message_signature | message signature is required | Sign the message and send the signature |
ErrorMissingField | field: nonce | nonce is required | Send the unix timestamp used in the message |
ErrorInvalidField | field: nonce | Invalid nonce | The nonce is more than 120 seconds old. Re-sign |
ErrorInvalidField | field: signature | Invalid signature | The signature does not verify against the message |
ErrorInvalidStatus | action_type: GetDetails, value: <status> | Card status does not allow details retrieval | The card is not Active |
ErrorInvalidStatus | action_type: GetCvv | Card status does not allow CVV retrieval | The card is not Active |
ErrorInvalidStatus | action_type: GetPin | Card status does not allow PIN retrieval | The card is not Active |
ErrorInvalidField | action_token: invalid_purpose | The action token was minted for a different action | Mint the token with action_type GetCardDetails |
Server Errors (500)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorGeneral | This signature does not belong to you | The signature recovers to a different address than the token's employee_address |
ErrorGeneral | Failed to decode message signature | The signature is not valid hex (EVM) or base64 (Stellar) |
ErrorGeneral | Unsupported chain type | The employee address is neither EVM nor Stellar |
ErrorGeneral | Failed to issue action token | The confirmation provider rejected the request. Retry with a fresh nonce |
ErrorGeneral | Failed to get card CVV | The card provider rejected the read. A mis-scoped token is refused earlier, as a 400 — this is the provider declining an otherwise valid request. Retry with a fresh token, then contact Wirex |
Updated 20 days ago

