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:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow to obtain the corporation token
PermissionsCard visibility depends on the caller's permissions
Issuing a CardWhere {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:

ParameterDescription
employee_idReturn only the cards linked to this employee. Only honoured for card administrators
page_number1-indexed page number
page_sizePage size

Who Sees What

Caller holdsSeesemployee_id filter
Su or CardSuEvery card in the corporationHonoured. The employee must belong to the corporation
CardView onlyOnly the cards linked to their own employee recordSilently ignored

A CardView-only caller that sends employee_id gets their own cards back, with no error. The
filter is dropped, the response is 200, and it looks like the target employee holds exactly the
caller's cards. Do not build a team overview on CardView alone — it needs CardSu.

A card issued without employee_id is 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" }
      ]
    }
  ]
}
FieldDescription
idCard id — the {cardId} path parameter everywhere else
card_wallet_addressThe card's on-chain address
statusRequested, NotActivated, Active, Blocked, Closed
status_reasonShort reason for the current status, e.g. User
previous_statusStatus before the current one. Absent for a card that has never transitioned
card_data.name_on_cardName embossed on the card
card_data.payment_systemVisa or MasterCard
card_data.card_number_first_4First 4 digits. Present only while Requested or NotActivated
card_data.card_number_last_4Last 4 digits. Present once past Requested and NotActivated
card_data.expiry_dateMM/YYYY. Present once past Requested and NotActivated
card_data.formatPlastic, Virtual or Metal
card_data.card_nameDisplay name
limitCurrent limits and usage — see Managing a Card
delivery_addressShipping 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>
PlaceholderValue
<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
}
FieldTypeRequiredDescription
action_typestringYesGetCardDetails to read card data. Matched case-insensitively. 3dsChallenge is the only other accepted value and does not unlock card data
message_signaturestringYesSignature over the message from Step 1
nonceintegerYesThe same unix timestamp used to build the message

Response:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
FieldDescription
tokenAction token. Pass it as action_token on the next call

The nonce expires after 120 seconds. A nonce older than that is rejected with 400 ErrorInvalidField and Invalid nonce. Generate the nonce, sign and exchange in one interaction —
do not pre-generate signatures.

Use GetCardDetails for every card read. One GetCardDetails token is accepted by all three
endpoints — PAN and expiry, CVV, and PIN. You do not need a separate token per field.

3dsChallenge unlocks none of them. It is a different purpose, minted for the 3D Secure flow, and
all three card-detail endpoints refuse it with 400 and
"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/signature is 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"
}
FieldDescription
card_numberFull PAN
expiry_dateExpiry 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 ReasonError DetailsDescriptionResolution
ErrorMissingFieldfield: action_typeaction type is requiredSend GetCardDetails or 3dsChallenge
ErrorInvalidFieldfield: action_typeInvalid value for action typeOnly those two values are recognised
ErrorMissingFieldfield: message_signaturemessage signature is requiredSign the message and send the signature
ErrorMissingFieldfield: noncenonce is requiredSend the unix timestamp used in the message
ErrorInvalidFieldfield: nonceInvalid nonceThe nonce is more than 120 seconds old. Re-sign
ErrorInvalidFieldfield: signatureInvalid signatureThe signature does not verify against the message
ErrorInvalidStatusaction_type: GetDetails, value: <status>Card status does not allow details retrievalThe card is not Active
ErrorInvalidStatusaction_type: GetCvvCard status does not allow CVV retrievalThe card is not Active
ErrorInvalidStatusaction_type: GetPinCard status does not allow PIN retrievalThe card is not Active
ErrorInvalidFieldaction_token: invalid_purposeThe action token was minted for a different actionMint the token with action_type GetCardDetails

Server Errors (500)

Error ReasonDescriptionResolution
ErrorGeneralThis signature does not belong to youThe signature recovers to a different address than the token's employee_address
ErrorGeneralFailed to decode message signatureThe signature is not valid hex (EVM) or base64 (Stellar)
ErrorGeneralUnsupported chain typeThe employee address is neither EVM nor Stellar
ErrorGeneralFailed to issue action tokenThe confirmation provider rejected the request. Retry with a fresh nonce
ErrorGeneralFailed to get card CVVThe 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

Did this page help you?