Physical Cards

Learn more about physical cards.

Encrypted PIN Block (Enterprise)

Physical cards can be provisioned for ATM/PIN debit access which requires a cardholder PIN. The Create card and Update card endpoints can set and update the cardholder PIN.

🚧

Due to the sensitive nature, PINs must be encrypted on the frontend immediately after user input.

An Encrypted PIN block is a JSON blob, encrypted with the Lithic API public key, base64 digest.

api.lithic.com.pub.pem

{
  "nonce": Integer,
  "pin": String
}
nonceA cryptographic nonce to create additional entropy and prevent replay attacks. This should be unique per request. Integer should be at least 8 digits in length.
pinCardholder PIN (between 4 and 12 numeric digits in length). String datatype to ensure leading zeros are not truncated.

Examples

import base64
import json
import random

# pip install pycryptodome
from Crypto.Cipher    import PKCS1_OAEP
from Crypto.PublicKey import RSA

pin_block =  {
    "nonce": random.randint(1e8, 1e12),
    "pin": "1234"
}

with open("path/to/api.lithic.com.pub.pem") as f:
    cipher = PKCS1_OAEP.new(RSA.importKey(f.read()))

ciphertext = cipher.encrypt(json.dumps(pin_block).encode('utf-8'))

# Use this for the "pin" field value when creating or updating cards
encrypted_pin_block = base64.b64encode(ciphertext)
const fs = require("fs");

// npm install node-forge
const forge = require("node-forge");

const pem = fs.readFileSync("path/to/api.lithic.com.pub.pem", "utf8");
const publicKey = forge.pki.publicKeyFromPem(pem);

function randomInt(low, high) {
  // Generate random integer between low and high, inclusive
  return Math.floor(Math.random() * (high - low + 1) + low)
}

const pinBlock =  {
  "nonce": randomInt(1e8, 1e12),
  "pin": "1234"
}

const ciphertext = publicKey.encrypt(JSON.stringify(pinBlock), "RSA-OAEP", {
  md: forge.md.sha1.create(),
  mgf1: {
    md: forge.md.sha1.create()
  }
});

// Use this for the "pin" field value when creating or updating cards
const encryptedPinBlock = forge.util.encode64(ciphertext);
console.log(encryptedPinBlock);
package main

import (
    crand "crypto/rand"
    "crypto/rsa"
    "crypto/sha1"
    "crypto/x509"
    "encoding/base64"
    "encoding/json"
    "encoding/pem"
    "fmt"
    "io/ioutil"
    rand "math/rand"
    "os"
    "time"
)

type PinBlock struct {
    Nonce int    `json:"nonce"`
    Pin   string `json:"pin"`
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

func loadKey(fileName string) (*rsa.PublicKey, error) {
    rawPem, err := ioutil.ReadFile(fileName)
    checkError(err)

    pemBlock, _ := pem.Decode(rawPem)

    return x509.ParsePKCS1PublicKey(pemBlock.Bytes)
}

func encrypt(msg []byte, publicKey *rsa.PublicKey) (string, error) {
    encryptedBytes, err := rsa.EncryptOAEP(
        sha1.New(),
        crand.Reader,
        publicKey,
        msg,
        nil,
    )
    if err != nil {
        return "", err
    }
    encodedData := base64.StdEncoding.EncodeToString(encryptedBytes)
    return encodedData, nil
}

func generateNonce() int {
    rand.Seed(time.Now().UnixNano())
    min := int(1e8)
    max := int(1e12)
    return min + rand.Intn(max-min+1)
}

func main() {
    pemFile := "path/to/api.lithic.com.pub.pem"

    publicKey, err := loadKey(pemFile)
    
    pinBlock := &PinBlock{Nonce: generateNonce(), Pin: "1234"}
    pinBlockBytes, err := json.Marshal(pinBlock)
    checkError(err)
    fmt.Println(string(pinBlockBytes))

    // Use this for the "pin" field value when creating or updating cards
    encoded, err := encrypt(pinBlockBytes, publicKey)
    checkError(err)

    fmt.Println(encoded)
}

Reissue Card (Enterprise)

API reference: Reissue Card

Initiate print and shipment of a duplicate card. Only applies to cards of type PHYSICAL.

POST https://api.lithic.com/v1/card/reissue

Sample Request

curl https://api.lithic.com/v1/card/reissue \
  -X POST \
  -H "Authorization: api-key YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"card_token":"589e1412-b132-48ac-ab5c-befd02535a57"}'

Sample Response

{
  "created": "2021-06-28T22:58:36Z",
  "cvv": "162",
  "exp_month": "06",
  "exp_year": "2027",
  "funding": {
    "account_name": "Sandbox",
    "created": "2020-07-08 17:57:36",
    "last_four": "5263",
    "nickname": "",
    "state": "ENABLED",
    "token": "b0f0d91a-3697-46d8-85f3-20f0a585cbea",
    "type": "DEPOSITORY_CHECKING"
  },
  "hostname": "",
  "last_four": "5205",
  "memo": "",
  "pan": "4111111540285205",
  "spend_limit": 0,
  "spend_limit_duration": "TRANSACTION",
  "state": "PENDING_FULFILLMENT",
  "token": "589e1412-b132-48ac-ab5c-befd02535a57",
  "type": "PHYSICAL"
}
card_tokenThe unique token of the card to update
shipping_address (optional)Shipping address. If omitted, the previous shipping address will be used
product_id (optional)Alphanumeric identifier to specify physical card manufacturing attributes. Only applies to cards of type PHYSICAL. This must be configured with Lithic before use.

Did this page help you?