Skip to main content

Card Authentication Contacts

Overview​

A card authentication contact is a phone number, email address, or postal address bound directly to a payment card or a financial account. Authentication uses it instead of the account holder's identity record.

Bind a contact when the identity record cannot answer for the cardholder. An anonymous or lightly identified card has no identity record to fall back on, so without a bound contact it cannot receive a digital wallet one time passcode or answer an address check.

Two kinds of contact exist, and each is bound and revoked on its own:

KindEnum valueWhat it holdsWhat it answers
ChannelEMAIL_AND_PHONEAn email address, a phone number, or bothThe digital wallet provisioning one time passcode
AddressPOSTAL_ADDRESSA postal addressAddress verification during an authorization

At most one contact of each kind is in force at a time. A card can therefore carry one channel contact and one address contact at once, and revoking one leaves the other untouched.

What a contact is bound to​

A contact bound to a payment card applies to that card alone. A contact bound to a financial account applies to every card issued against it.

The two kinds do not offer the same choice:

  • A channel contact binds to a payment card only. bindCardAuthenticationContactChannel takes paymentCardId directly.
  • An address contact binds to either. bindCardAuthenticationContactAddress takes a scope with exactly one of paymentCardId or financialAccountId.

Which purpose reads which scope​

The two purposes do not read the same scopes, and this is the single easiest thing to get wrong.

PurposeReads the card contactReads the financial account contact
Wallet one time passcodeYesNo
Address verificationYesYes
binding a phone at the account level does nothing for wallet OTP

Wallet OTP reads the card scoped contact only. There is no fallback from the card to its financial account. A card with no channel contact of its own falls back to the account holder's identity record, never to a contact bound on the financial account. Bind the channel contact on every card that needs one.

Address verification reads both scopes and treats what it finds as supplemental candidates alongside the addresses already on the card, so binding an address does not remove the card's own addresses from consideration.

Before you bind​

Two things have to be in place on your card product before a bound contact changes anything.

card authentication contacts are not self-service

Highnote turns this feature on per card product, and the API cannot do it for you. Contact your Highnote representative to have the feature and its purposes enabled, or support@highnote.com once you are live.

The card product needs the card authentication contact feature. Without it, every bind is rejected, whatever the call sends. A contact that is already bound can still be read, but every purpose on it reads back as not enabled.

Each purpose has to be enabled on that feature. A bind is rejected if the resulting contact would carry a purpose the feature does not enable, even when the call never mentioned that purpose. Product configuration is not visible through the API, so if a bind is rejected on a purpose, send that purpose as false explicitly. This works only when the card product already has the feature:

# Bind the channel without the wallet one time passcode purpose.
walletOtpEnabled: false
# Bind the address without the address verification purpose.
avsEnabled: false

Read the contacts in force​

cardAuthenticationContacts returns the contacts in force, at most one of each kind. Pass type to narrow the result to one kind, or leave it out to get every kind.

A card's list does not include contacts bound to the card's financial account, and a financial account's list does not include contacts bound to individual cards. Read both to see everything that applies to a card.

an empty list and null mean different things

An empty list means the read succeeded and nothing is bound, so authentication uses the account holder's identity record. Null means the read could not be answered and tells you nothing about what is bound. Do not treat null as "nothing bound".

Read a card's contacts​

FindCardAuthenticationContacts​
Query
query FindCardAuthenticationContacts($id: ID!) {
node(id: $id) {
... on PaymentCard {
__typename
id
cardAuthenticationContacts {
id
type
idempotencyKey
email
phone
streetAddress
extendedAddress
locality
region
postalCode
postalCodeExtension
countryCodeAlpha3
active
version
effectiveFrom
effectiveThrough
walletOtp {
enabled
effectiveFrom
effectiveThrough
}
avs {
enabled
effectiveFrom
effectiveThrough
}
}
}
}
}
Variables
{
  "id": "<PAYMENT_CARD_ID>"
}
⚠️ Please login to execute queries. Visit the dashboard to authenticate.
Result
{
"data": {
"node": {
"__typename": "PaymentCard",
"id": "<PAYMENT_CARD_ID>",
"cardAuthenticationContacts": [
{
"id": "<CARD_AUTHENTICATION_CONTACT_ID>",
"type": "EMAIL_AND_PHONE",
"idempotencyKey": "example-card-9f31-channel-1",
"email": "cardholder@example.com",
"phone": "+14155550142",
"streetAddress": null,
"extendedAddress": null,
"locality": null,
"region": null,
"postalCode": null,
"postalCodeExtension": null,
"countryCodeAlpha3": null,
"active": true,
"version": 1,
"effectiveFrom": "2026-09-19T14:02:11.408Z",
"effectiveThrough": null,
"walletOtp": {
"enabled": true,
"effectiveFrom": "2026-09-19T14:02:11.408Z",
"effectiveThrough": null
},
"avs": {
"enabled": false,
"effectiveFrom": null,
"effectiveThrough": null
}
}
]
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}

Read a financial account's contacts​

FindFinancialAccountAuthenticationContacts​
Query
query FindFinancialAccountAuthenticationContacts($id: ID!) {
node(id: $id) {
... on FinancialAccount {
__typename
id
cardAuthenticationContacts(type: POSTAL_ADDRESS) {
id
type
idempotencyKey
streetAddress
extendedAddress
locality
region
postalCode
postalCodeExtension
countryCodeAlpha3
active
version
effectiveFrom
effectiveThrough
avs {
enabled
effectiveFrom
effectiveThrough
}
}
}
}
}
Variables
{
  "id": "<FINANCIAL_ACCOUNT_ID>"
}
⚠️ Please login to execute queries. Visit the dashboard to authenticate.
Result
{
"data": {
"node": {
"__typename": "FinancialAccount",
"id": "<FINANCIAL_ACCOUNT_ID>",
"cardAuthenticationContacts": [
{
"id": "<CARD_AUTHENTICATION_CONTACT_ID>",
"type": "POSTAL_ADDRESS",
"idempotencyKey": "example-acct-4c08-address-1",
"streetAddress": "100 Main Street",
"extendedAddress": "Suite 400",
"locality": "San Francisco",
"region": "CA",
"postalCode": "94105",
"postalCodeExtension": "1234",
"countryCodeAlpha3": "USA",
"active": true,
"version": 1,
"effectiveFrom": "2026-09-19T14:06:53.771Z",
"effectiveThrough": null,
"avs": {
"enabled": true,
"effectiveFrom": "2026-09-19T14:06:53.771Z",
"effectiveThrough": null
}
}
]
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}
the read is the truth about a purpose

A purpose reads back enabled: false, with both of its dates null, whenever the card product does not enable it, whatever was requested when the contact was bound. Render what the read returns, not what you submitted.

Bind an email address, a phone number, or both​

bindCardAuthenticationContactChannel binds a channel contact to a payment card. Supply an email address, a phone number, or both.

The phone number is stored and returned exactly as you send it, and it is not validated against any format. Two spellings of the same number are two different contacts, so pick one format and use it every time. International format with a leading plus sign and digits only, such as +14155550142, works well. An email address is validated as an email address and may be up to 254 characters.

BindCardAuthenticationContactChannel​
Query
mutation BindCardAuthenticationContactChannel(
$input: BindCardAuthenticationContactChannelInput!
) {
bindCardAuthenticationContactChannel(input: $input) {
__typename
... on CardAuthenticationContact {
id
type
idempotencyKey
email
phone
active
version
effectiveFrom
walletOtp {
enabled
effectiveFrom
effectiveThrough
}
}
... on UserError {
errors {
errorPath
code
description
}
}
... on AccessDeniedError {
message
}
}
}
Variables
{
  "input": {
    "paymentCardId": "<PAYMENT_CARD_ID>",
    "idempotencyKey": "example-card-9f31-channel-1",
    "email": "cardholder@example.com",
    "phone": "+14155550142",
    "walletOtpEnabled": true
  }
}
⚠️ Please login to execute queries. Visit the dashboard to authenticate.
Result
{
"data": {
"bindCardAuthenticationContactChannel": {
"__typename": "CardAuthenticationContact",
"id": "<CARD_AUTHENTICATION_CONTACT_ID>",
"type": "EMAIL_AND_PHONE",
"idempotencyKey": "example-card-9f31-channel-1",
"email": "cardholder@example.com",
"phone": "+14155550142",
"active": true,
"version": 1,
"effectiveFrom": "2026-09-19T14:02:11.408Z",
"walletOtp": {
"enabled": true,
"effectiveFrom": "2026-09-19T14:02:11.408Z",
"effectiveThrough": null
}
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}

Bind a postal address​

bindCardAuthenticationContactAddress binds an address contact to a payment card or a financial account. Set exactly one of paymentCardId or financialAccountId on scope.

streetAddress, postalCode, and countryCodeAlpha3 are required. The last two are required because an address check compares them directly. countryCodeAlpha3 is an ISO 3166-1 alpha-3 code and must be three uppercase letters, for example USA; usa is rejected.

BindCardAuthenticationContactAddress​
Query
mutation BindCardAuthenticationContactAddress(
$input: BindCardAuthenticationContactAddressInput!
) {
bindCardAuthenticationContactAddress(input: $input) {
__typename
... on CardAuthenticationContact {
id
type
idempotencyKey
streetAddress
extendedAddress
locality
region
postalCode
postalCodeExtension
countryCodeAlpha3
active
version
effectiveFrom
avs {
enabled
effectiveFrom
effectiveThrough
}
}
... on UserError {
errors {
errorPath
code
description
}
}
... on AccessDeniedError {
message
}
}
}
Variables
{
  "input": {
    "scope": {
      "paymentCardId": "<PAYMENT_CARD_ID>"
    },
    "idempotencyKey": "example-card-9f31-address-1",
    "streetAddress": "100 Main Street",
    "extendedAddress": "Suite 400",
    "locality": "San Francisco",
    "region": "CA",
    "postalCode": "94105",
    "postalCodeExtension": "1234",
    "countryCodeAlpha3": "USA",
    "avsEnabled": true
  }
}
⚠️ Please login to execute queries. Visit the dashboard to authenticate.
Result
{
"data": {
"bindCardAuthenticationContactAddress": {
"__typename": "CardAuthenticationContact",
"id": "<CARD_AUTHENTICATION_CONTACT_ID>",
"type": "POSTAL_ADDRESS",
"idempotencyKey": "example-card-9f31-address-1",
"streetAddress": "100 Main Street",
"extendedAddress": "Suite 400",
"locality": "San Francisco",
"region": "CA",
"postalCode": "94105",
"postalCodeExtension": "1234",
"countryCodeAlpha3": "USA",
"active": true,
"version": 1,
"effectiveFrom": "2026-09-19T14:06:53.771Z",
"avs": {
"enabled": true,
"effectiveFrom": "2026-09-19T14:06:53.771Z",
"effectiveThrough": null
}
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}

Rebinding replaces the contact​

There is no update operation. Binding again supersedes the contact of that kind in full.

The contact values and the purpose flags behave differently on a rebind, and an integration that assumes one rule for both will withdraw a channel it meant to keep.

What you sendContact values (email, phone, the address fields)Purpose flags (walletOtpEnabled, avsEnabled)
A valueReplaces the previous valueSets the purpose to that value
NothingWithdraws the previous valueKeeps what the previous version said

So a rebind that sends only phone withdraws the email address bound before it. Send every channel you want in force, every time. Leave a purpose flag out only when you mean to keep whatever it was.

a rebind can be rejected over a purpose you did not mention

Because an omitted purpose carries forward, a rebind can produce a contact carrying a purpose the card product no longer enables, and that bind is rejected even though the call never named the purpose. Send the purpose as false to withdraw it and let the bind through.

Channel contacts and address contacts are independent. Rebinding one does not touch the other.

Choose an idempotency key​

idempotencyKey is not a retry key. It does the opposite of one.

On a bind it must be a value you have not used before. Reusing one is rejected and binds nothing, and the earlier result is not replayed. A key stays used after its contact is revoked, so a later bind needs a new one. A key has to be unique across your whole organization for that kind of contact, not merely unique for the card, so reusing a key on a different card is rejected too.

Its real job is to be the handle you pass to revokeCardAuthenticationContact later. The read returns it on every contact.

Revoke a contact​

revokeCardAuthenticationContact retires the contact of one kind and returns it as it stood when it was retired. Pass scope and type to identify which contact, and the idempotencyKey of the contact you mean to retire, as returned by the read.

The call fails rather than retire a contact someone else bound in the meantime. If it fails, read the contacts again and retry with the key the read returns.

Retiring one kind leaves the other in force. Any purpose still open is closed at the same time, and authentication falls back to the account holder's identity record from then on. A later bind starts from no contact in force, so it has to supply every purpose it expects again.

RevokeCardAuthenticationContact​
Query
mutation RevokeCardAuthenticationContact(
$input: RevokeCardAuthenticationContactInput!
) {
revokeCardAuthenticationContact(input: $input) {
__typename
... on CardAuthenticationContact {
id
type
idempotencyKey
email
phone
active
version
effectiveFrom
effectiveThrough
walletOtp {
enabled
effectiveFrom
effectiveThrough
}
}
... on UserError {
errors {
errorPath
code
description
}
}
... on AccessDeniedError {
message
}
}
}
Variables
{
  "input": {
    "scope": {
      "paymentCardId": "<PAYMENT_CARD_ID>"
    },
    "type": "EMAIL_AND_PHONE",
    "idempotencyKey": "example-card-9f31-channel-1"
  }
}
⚠️ Please login to execute queries. Visit the dashboard to authenticate.
Result
{
"data": {
"revokeCardAuthenticationContact": {
"__typename": "CardAuthenticationContact",
"id": "<CARD_AUTHENTICATION_CONTACT_ID>",
"type": "EMAIL_AND_PHONE",
"idempotencyKey": "example-card-9f31-channel-1",
"email": "cardholder@example.com",
"phone": "+14155550142",
"active": false,
"version": 1,
"effectiveFrom": "2026-09-19T14:02:11.408Z",
"effectiveThrough": "2026-09-19T15:41:07.220Z",
"walletOtp": {
"enabled": true,
"effectiveFrom": "2026-09-19T14:02:11.408Z",
"effectiveThrough": "2026-09-19T15:41:07.220Z"
}
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}

Versions and history​

Binding is append-only. Each bind writes a new version that supersedes the one before it, so a contact has a history rather than a current value that gets overwritten.

  • id is the identifier of one version. A rebind produces a new id.
  • version counts the binds of that kind on that card or financial account. The first is 1, and it keeps counting across a revoke, so a bind after a revoke is the next number up rather than 1 again.
  • active reports whether a version is the one in force. A version superseded by a rebind or closed by a revoke reports false.
  • effectiveFrom and effectiveThrough bound the period a version was in force. effectiveThrough is null while it still is.

Each purpose carries its own effectiveFrom and effectiveThrough, so a purpose switched on later than the contact was bound reports its own start.