Generate a token to view a physical card's PIN code

A POST request sent to generate a token to view a physical card's PIN code.

This endpoint is not available for virtual cards.

WARNING: The card's PIN code is a PCI sensitive field that should only be viewed on the client
device and never logged or stored in any form.

The response includes a callback URL and a token. The token is good for one use only.
Using the response object, the user should perform a POST request to the callback URL with a body of

{
  tokenId: tokenId
}

to view the card's PIN.

Once the token is used in the POST request to the callback URL, a response similar to the following will be returned:

"4444"

A wallet signature must be supplied in the request body to prove ownership of the
wallet that owns the card.

Example with Ethersjs:

const wallet = ethers.Wallet.createRandom();
const types = {
  Authenticate: [
    { name: 'id', type: 'string' },
    { name: 'queriedAt', type: 'uint256' },
  ],
};
const domain = {
  name: 'Kulipa Wallet Verification',
  version: '1',
  chainId: 42161, // the actual chain ID of the wallet network (e.g. 42161 for Arbitrum One)
  verifyingContract: '0x...', // the user's withdrawal address
};
const message = {
  id: 'crd-e259ee22-f857-444d-9c38-e0bf2ee669e3', // the card ID
  queriedAt: Date.now(), // unix timestamp in milliseconds, validated within a short tolerance window of server time
};

const signature = await wallet.signTypedData(domain, types, message);

Example with Solana kit:

const keys = await generateKeyPair();
const address = await getAddressFromPublicKey(keys.publicKey);
const validTypedData = {
  id: 'crd-e259ee22-f857-444d-9c38-e0bf2ee669e3', // the card ID
  queriedAt: Date.now(), // unix timestamp in milliseconds, validated within a short tolerance window of server time
  purpose: 'Card Sensitive Field Token',
  organisation: 'Kulipa',
  wallet: address,
};
const structCodec = getStructCodec([
  ['organisation', addCodecSizePrefix(getUtf8Codec(), getU32Codec())],
  ['purpose', addCodecSizePrefix(getUtf8Codec(), getU32Codec())],
  ['wallet', addCodecSizePrefix(getUtf8Codec(), getU32Codec())],
  ['id', addCodecSizePrefix(getUtf8Codec(), getU32Codec())],
  ['queriedAt', getU64Codec()],
]);
const encodedData = structCodec.encode(validTypedData);
const signedBytes = await signBytes(keys.privateKey, encodedData);
const signature = getBase58Decoder().decode(signedBytes);
Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…
Path Params
string
required
length between 40 and 40

A Card identifier. Begins with 'crd-' followed by a v4 UUID

Body Params

Wallet signature accompanying the request, used to prove ownership of the wallet that owns the card.

A wallet signature over a typed-data payload, used to prove ownership of the wallet that
owns the resource. The exact shape of typedData depends on the wallet's blockchain
(EIP-712 for EVM, Starknet typed data, or Solana payload) and on what is being signed.

signatures
array of strings
required

The wallet signature(s) over the typed data.

signatures*
typedData
object
required

The typed structured data of the wallet authentication.

Responses

400

The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).

401

Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.

403

The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.

429

The user has sent too many requests in a given amount of time ("rate limiting").

500

The server has encountered a situation it does not know how to handle.

Language
LoadingLoading…
Response
Choose an example:
application/json