Third-party wallets (such as Apple Pay)

In addition to the Pismo digital wallet, cards you issue through Pismo can be used in payment systems such as Apple Pay and Google Pay that have their own digital wallets. This happens through the following process:

  1. Customer enters card information (PAN, CVV) into payment app (Apple Pay, Google Pay, and so on) for tokenization in a digital wallet (card on file). The token allows payments to be made without exposing sensitive data.

  2. The payment app verifies the card with the card network (Visa, Mastercard) and Pismo.

As the card issuer, you can, for this process:

  • Provide your own anti-fraud card verification via a webhook. See Issuer anti-fraud webhook below.

  • Assign a network profile to a card that, principally, determines the picture used to represent a card in a payment app. The payment network provides card issuers a management portal where card pictures are stored and assigned an identifier.

Note: The card tokenization process generates a number of events, for more information, see Card tokenization flow and events.

Issuer anti-fraud webhook

As a card issuer, you can provide your own anti-fraud card verification via a webhook, that Pismo calls during the tokenization process. If you don't provide one, Pismo uses a default anti-fraud method.

πŸ“˜

Pismo webhook security

For information on webhook security, see Verifying webhook requests

You can:

  • Approve a card
  • Not approve a card
  • Ask Pismo to prompt the cardholder for further identity and verification in the form of SMS, phone call, or app.

Anti-fraud webhook request

Sample webhook request payload from Pismo:

{
    "card":{
        "id": 134142,
        "type": "PLASTIC",
        "program_id": 1234,
        "bin": "5122"
    },
    "combo_card": { },
    "token_id": 432,
    "network_brand": "vts",
    "custom_codes": ["FRB", "BNB"],
    "original_network_data": {

    }
}
FieldTypeDescriptionRequired
custom_codesstring arrayCard status validation codes indicating Pismo's findings. Issuer can still approve.

- BNB - Other custom code
- FLE - Generic refusal
- PFT - Anti-fraud refusal
- UBT - Card blocked
- NPL - Card not plastic
- FRB - Card created
- VNM - Invalid card
- CED - Invalid expiration date
- EXPCRD - Expired card
- TNF - Token not found
- FR1 - Failed CVV2
- FR2 - Invalid CVV2
- CNM - Card not normal
- 998 - Card not found
- OP1 - Internal error
- HSE - Hardware security module (HSM) validation error
No
token_idnumberPismo-issued token ID. You can use this to call the Get token information endpoint.Yes
network_brandstring- vts - Visa
- mdes - Mastercard
Yes
cardobject
 idnumberPismo-assigned card IDYes
 typeenum stringCard type:

- PLASTIC
- VIRTUAL
- RECURRING
- TEMPORARY
Yes
Β Β Β Β  program_idnumberProgram IDYes
Β Β Β Β Β binstringBank identification numberYes
 statusenum stringCard status:

- created
- normal
- damaged
- warning
- lost
- unknown
No
 stageenum stringCard lifecycle status

- unknown
- blocked
- unblocked
No
combo_cardobjectNo
 statusenum stringCard status:

- created
- normal
- damaged
- warning
- lost
- unknown
original_network_dataobjectRefer to the VISA Token Service (VTS)
Issuer API Specifications (JSON) manual
or the Mastercard Customer Interface Specification manual for more information on the contents of this field.
No

Anti-fraud webhook response

Sample webhook response payload from issuer:

{
    "step_up_methods": [
        {
            "method": "OTPCALL",
            "value": "+551130039500"
        },
        {
            "method": "OTPSMS",
            "value": null
        },
        {
            "method": "OTPAPP",
            "value": "com.btg.pactual.banking://wallet"
        }
    ],
    "approve": true,
    "action_code": "85"
}
FieldTypeDescriptionRequired
approvebooleanCard approved flagYes
action_codestringDecision code:

- 00 - Approved
- 05 - Not approved
- 85 - Needs challenge
- 14 - Invalid PAN
- N7 - Invalid CVV2
- 54 - Invalid expiration date
- 96 - Issuer internal system error
Yes
step_up_methodsobject arrayNeeds challenge methods. At least one is required if used.Yes if action_code = 85.
 methodenum stringOne-time passcode (OTP) challenge method:

- OTPSMS - SMS
- OTPAPP - App
- OTPCALL - Call
 valuestringAdditional information for challenge - phone number, app, and so on.
 platformenum stringOnly used for Visa and only when the method is OPTAPP:

- IOS
- ANDROID
- WINDOWS
- WEB
 sourcestringOnly used for Visa and under certain conditions. This is the Source Address field as discussed in the VISA VTS manual and you should read that document for more information.

Anti-fraud code sample (Go)

package main

// Anti-fraud code sample in Go

//curl --location --request POST 'http://antifraudhost' \
//--header 'x-pismo-sign: eyJhbGciOiJSUzI1NiIsImtpZCI6IjEzY2VlZWViMWEwNjU1Nzg4Y2UyMDg0ZmZmY2FkMDJmNTFmYzMxNDAiLCJ0eXAiOiJKV1QifQ.eyJhY2NvdW50LWlkIjo5ODY2NTM4NiwiYm9keS1oYXNoIjoiUm0yNWNpTy9RRjNMdmIwL1JZbmVWMFZRUkMzd3pNbzZjemdlR1NHUE1KWT0iLCJleHAiOjE2NTI1MzY2MzAsImlhdCI6MTY1MTY3MjYzMCwiaXNzIjoiYXBpLnBpc21vLmlvIiwia2lkIjoiMTNjZWVlZWIxYTA2NTU3ODhjZTIwODRmZmZjYWQwMmY1MWZjMzE0MCIsInN1YiI6ImFwaS1jYXJkcy10b2tlbml6YXRpb24ifQ.bE7l7KQHRj_0J7zcXtLXgefQuGVQjdUr2BtetTQzTf25_tbHf6iUXHIsGB8iCVujo2e5aUrglxTQScduo3sUuJgzLNUHEyBOgMUc9b6v9K_-reFLpJa9Co0Ct8PmU_os-AZr6jqrYgzGk343I_FYDDw48BmqSMRgwtaJPzHQesaSdd8pjAw_CdSttobstplJjQiwUetufmSvZkOCQHeU-JHTwgdcTwgmtJwVMnFFERIdWQMzoz1UYtP0oMJivw3Zt-MjutqvodkaymOIx9IjJTywp0Oq61rmPtWlKW0pIsI6HCYDR8pASgantt7sXGpNb8cdynUIyvxlixpy77kdfg' \
//--header 'Content-Type: application/json' \
//--data-raw '{
//    "card":{
//        "id": 134142,
//        "type": "PLASTIC"
//    },
//    "combo_card": null,
//    "token_id": 432,
//    "network_brand": "vts",
//    "custom_codes": ["FRB", "BNB"],
//    "original_network_data": {}
//}'

import (
    "context"
    "crypto/x509"
    "encoding/json"
    "encoding/pem"
    "fmt"
    "github.com/labstack/echo/v4"
    "github.com/square/go-jose"
    "io/ioutil"
    "net/http"
)

type clientTokenDecision struct {
    Approve       bool                 `json:"approve"`
    ActionCode    string               `json:"action_code"`
    StepUpMethods []clientStepUpMethod `json:"step_up_methods"`
}

type clientStepUpMethod struct {
    Method   string `json:"method"`
    Value    string `json:"value,omitempty"`
    Source   string `json:"source"`
    Platform string `json:"platform"`
}

func main() {
    getPismoCertificates()
    e := echo.New()
    e.POST("/", handler)
    e.Logger.Fatal(e.Start(":1323"))
}

var mapCertificates map[string]*x509.Certificate

func getPismoCertificates() {
    url := "https://sandbox.pismolabs.io/robot/v1/metadata/x509/[email protected]"
    httpReq, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
    if err != nil {
        panic(err)
    }

    client := http.Client{}
    httpRes, err := client.Do(httpReq)
    if err != nil {
        panic(err)
    }

    if httpRes.StatusCode != http.StatusOK {
        panic(err)
    }

    bRes, err := ioutil.ReadAll(httpRes.Body)
    if err != nil {
        panic(err)
    }

    tmpMap := make(map[string]string)
    err = json.Unmarshal(bRes, &tmpMap)
    if err != nil {
        panic(err)
    }

    mapCertificates = make(map[string]*x509.Certificate)

    for key, val := range tmpMap {
        block, _ := pem.Decode([]byte(val))
        if block == nil {
            panic("cannot decode block")
        }

        cer, err := x509.ParseCertificate(block.Bytes)
        if err != nil {
            panic(err)
        }
        mapCertificates[key] = cer
    }
}

func handler(c echo.Context) error {
    tokenString := c.Request().Header.Get("X-Pismo-Sign")
    valid := isTokenValid(tokenString)
    if !valid {
        return fmt.Errorf("certificate not valid")
    }

    k := clientTokenDecision{

        Approve:    true,
        ActionCode: "85",
        StepUpMethods: []clientStepUpMethod{
            {

                Method: "OTPAPP",
                Value:  "app.to.app",
            },
            {
                Method: "OTPSMS",
            },
        },
    }
    return c.JSON(http.StatusOK, k)
}

func isTokenValid(tokenString string) bool {
    jwsSignature, err := jose.ParseSigned(tokenString)
    if err != nil {
        return false

    }

    for _, sign := range jwsSignature.Signatures {
        kid := sign.Header.KeyID
        cert := mapCertificates[kid]
        _, err := jwsSignature.Verify(cert.PublicKey)

        if err != nil {
            return false
        }
        break
    }
    return true
}