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:
| Kind | Enum value | What it holds | What it answers |
|---|---|---|---|
| Channel | EMAIL_AND_PHONE | An email address, a phone number, or both | The digital wallet provisioning one time passcode |
| Address | POSTAL_ADDRESS | A postal address | Address 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.
bindCardAuthenticationContactChanneltakespaymentCardIddirectly. - An address contact binds to either.
bindCardAuthenticationContactAddresstakes ascopewith exactly one ofpaymentCardIdorfinancialAccountId.
Which purpose reads which scope
The two purposes do not read the same scopes, and this is the single easiest thing to get wrong.
| Purpose | Reads the card contact | Reads the financial account contact |
|---|---|---|
| Wallet one time passcode | Yes | No |
| Address verification | Yes | Yes |
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.
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 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>" }
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>" }
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>"
}
}
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 } }
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 } }
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 send | Contact values (email, phone, the address fields) | Purpose flags (walletOtpEnabled, avsEnabled) |
|---|---|---|
| A value | Replaces the previous value | Sets the purpose to that value |
| Nothing | Withdraws the previous value | Keeps 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.
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" } }
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.
idis the identifier of one version. A rebind produces a newid.versioncounts 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.activereports whether a version is the one in force. A version superseded by a rebind or closed by a revoke reportsfalse.effectiveFromandeffectiveThroughbound the period a version was in force.effectiveThroughis 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.
Related
- Add Cards to Digital Wallets covers provisioning and the token states a passcode resolves.
- Configure Spend Rules covers the address and postal code spend rules that act on an address verification result.
- PaymentCard and FinancialAccount in the API reference.