Skip to main content

Update 3DS Enrollment

Overview

Use this endpoint to update the 3DS enrollment information of a card by re-associating it to another cardholder. This is useful when you need to change which cardholder's contact information (email and mobile) will be used for OTP delivery during 3DS transactions for a specific card.

NOTE: This API is offered on a client-by-client approval basis. Speak to your account manager to check eligibility.

Resource Access

Production (api.ahrvo.network)

POST https://api.ahrvo.network/card/issuance/api/issuing/3ds/v2/update

Staging (gateway.ahrvo.network)

POST https://gateway.ahrvo.network/card/issuance/api/issuing/3ds/v2/update

Request Headers

HeaderValueRequiredDescription
Acceptapplication/jsonYesContent type for the response
AuthorizationBearer {access_token}YesBearer token for authentication
x-api-keyAPI KeyYesAPI key for authentication
Content-Typeapplication/jsonYesRequest body content type

Request Body

The request body should contain a JSON object with the following structure:

{
"card_id": "card59202507231957437131",
"cardholder_id": "471041678120300546"
}

Request Fields

FieldTypeRequiredDescription
card_idstringYesThe unique ID of the card to be updated
cardholder_idstringYesThe unique ID of the new cardholder to be associated with the specified card

Response

Success Response (200 OK)

{
"code": "SUCCESS"
}

Response Fields

FieldTypeDescription
codestringStatus string indicating the result. "SUCCESS" refers to successful update

Error Responses

  • 400 Bad Request: Invalid card_id or cardholder_id provided
  • 401 Unauthorized: Invalid or missing authentication token
  • 403 Forbidden: 3DS feature not enabled for this account
  • 404 Not Found: Card or cardholder does not exist, or card is not enrolled for 3DS

Code Examples

cURL

curl -X POST \
'https://gateway.ahrvo.network/card/issuance/api/issuing/3ds/v2/update' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"card_id": "card59202507231957437131",
"cardholder_id": "471041678120300546"
}'

Python

import requests

url = "https://gateway.ahrvo.network/card/issuance/api/issuing/3ds/v2/update"
headers = {
"Accept": "application/json",
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"card_id": "card59202507231957437131",
"cardholder_id": "471041678120300546"
}

response = requests.post(url, headers=headers, json=data)
print(response.json())

JavaScript (Node.js)

const axios = require('axios');

const url = 'https://gateway.ahrvo.network/card/issuance/api/issuing/3ds/v2/update';
const headers = {
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'x-api-key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
};
const data = {
card_id: 'card59202507231957437131',
cardholder_id: '471041678120300546'
};

axios.post(url, data, { headers })
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error.response.data);
});

Usage Notes

  • Prerequisites: Both the card and the new cardholder must already exist
  • Card Must Be Enrolled: The card must already be enrolled for 3DS before updating
  • Immediate Effect: The change takes effect immediately for future 3DS transactions
  • Previous Association: The card will be disassociated from the previous cardholder
  • One Cardholder Per Card: Each card can only be associated with one cardholder at a time

Common Use Cases

Transfer Card to New Cardholder

When a card needs to be transferred to a different cardholder:

def transfer_card_to_new_cardholder(card_id, new_cardholder_id):
"""
Transfer a card's 3DS enrollment to a new cardholder.
This updates which contact information will receive OTPs.
"""
# Verify the new cardholder exists
cardholder = query_cardholder_details(new_cardholder_id)
if not cardholder:
raise ValueError(f"Cardholder {new_cardholder_id} not found")

# Update the enrollment
response = update_3ds_enrollment(card_id, new_cardholder_id)

if response['code'] == 'SUCCESS':
print(f"Card {card_id} now associated with cardholder {new_cardholder_id}")
print(f"OTP will be sent to: {cardholder['data']['email']}")
print(f"Mobile: {cardholder['data']['call_prefix']}{cardholder['data']['mobile']}")

return response

Update Contact Information for a Card

When you want to change the contact info for OTP delivery without updating the cardholder:

async function updateCardContactInfo(cardId, newEmail, newMobile) {
// Option 1: Update existing cardholder
const currentEnrollment = await queryEnrollmentDetails(cardId);
await updateCardholderContact(
currentEnrollment.data.cardholder_id,
{ email: newEmail, mobile: newMobile }
);

// Option 2: Create new cardholder and re-associate
const newCardholder = await createCardholder({
budget_id: currentEnrollment.data.budget_id,
email: newEmail,
mobile: newMobile,
// ... other required fields
});

await update3DSEnrollment({
card_id: cardId,
cardholder_id: newCardholder.data.cardholder_id
});
}

Bulk Update Card Enrollments

Update multiple cards to a new cardholder:

def bulk_update_card_enrollments(card_ids, new_cardholder_id):
"""
Update multiple cards to use a new cardholder's contact information.
Useful for enterprise scenarios or family accounts.
"""
results = {
'success': [],
'failed': []
}

# Verify cardholder exists first
cardholder = query_cardholder_details(new_cardholder_id)
if not cardholder:
return {'error': f"Cardholder {new_cardholder_id} not found"}

for card_id in card_ids:
try:
# Check if card is enrolled
try:
enrollment = query_enrollment_details(card_id)
except Exception as e:
results['failed'].append({
'card_id': card_id,
'error': 'Card not enrolled for 3DS'
})
continue

# Update enrollment
response = update_3ds_enrollment(card_id, new_cardholder_id)

if response['code'] == 'SUCCESS':
results['success'].append(card_id)
else:
results['failed'].append({
'card_id': card_id,
'error': response.get('message', 'Update failed')
})
except Exception as e:
results['failed'].append({
'card_id': card_id,
'error': str(e)
})

return results

Card Replacement Scenario

When issuing a replacement card to the same cardholder:

async function replaceCardWith3DS(oldCardId, newCardId) {
try {
// Get the current cardholder from old card
const oldEnrollment = await queryEnrollmentDetails(oldCardId);
const cardholderId = oldEnrollment.data.cardholder_id;

// Enroll new card with same cardholder
await enrollCardFor3DS(newCardId, cardholderId);

console.log(`New card ${newCardId} enrolled with same cardholder`);
console.log('Cardholder contact information preserved');

// Optionally unenroll old card if needed
// await unenrollCard(oldCardId);

return { success: true, cardholderId };
} catch (error) {
console.error('Card replacement failed:', error);
throw error;
}
}

Family Account Management

Manage cards for family members with shared or individual cardholder profiles:

class FamilyAccountManager:
def __init__(self):
self.primary_cardholder_id = None
self.family_cardholders = {}

def set_primary_cardholder(self, cardholder_id):
"""Set the primary cardholder for the family account"""
self.primary_cardholder_id = cardholder_id

def assign_card_to_family_member(self, card_id, member_name, cardholder_id):
"""Assign a card to a specific family member"""
try:
# Update the card's enrollment to the member's cardholder
response = update_3ds_enrollment(card_id, cardholder_id)

if response['code'] == 'SUCCESS':
self.family_cardholders[card_id] = {
'member_name': member_name,
'cardholder_id': cardholder_id
}
print(f"Card {card_id} assigned to {member_name}")

return response
except Exception as e:
print(f"Failed to assign card: {e}")
raise

def consolidate_to_primary(self, card_ids):
"""Consolidate multiple cards to use primary cardholder"""
if not self.primary_cardholder_id:
raise ValueError("Primary cardholder not set")

return bulk_update_card_enrollments(card_ids, self.primary_cardholder_id)

Audit and Verification Workflow

Verify enrollment updates with comprehensive logging:

async function verifiedEnrollmentUpdate(cardId, newCardholderId) {
console.log('=== Starting Enrollment Update ===');

try {
// Step 1: Get current enrollment
console.log('Step 1: Fetching current enrollment...');
const currentEnrollment = await queryEnrollmentDetails(cardId);
console.log(`Current cardholder: ${currentEnrollment.data.cardholder_id}`);
console.log(`Current email: ${currentEnrollment.data.email}`);

// Step 2: Verify new cardholder exists
console.log('Step 2: Verifying new cardholder...');
const newCardholder = await queryCardholderDetails(newCardholderId);
console.log(`New cardholder email: ${newCardholder.data.list[0].email}`);

// Step 3: Perform update
console.log('Step 3: Updating enrollment...');
const updateResponse = await update3DSEnrollment({
card_id: cardId,
cardholder_id: newCardholderId
});

if (updateResponse.code !== 'SUCCESS') {
throw new Error('Update failed');
}

// Step 4: Verify update
console.log('Step 4: Verifying update...');
const verifyEnrollment = await queryEnrollmentDetails(cardId);

if (verifyEnrollment.data.cardholder_id === newCardholderId) {
console.log('✓ Enrollment successfully updated and verified');
console.log(`New OTP delivery email: ${verifyEnrollment.data.email}`);
console.log(`New OTP delivery mobile: ${verifyEnrollment.data.call_prefix}${verifyEnrollment.data.mobile}`);
return { success: true, enrollment: verifyEnrollment.data };
} else {
throw new Error('Verification failed - enrollment not updated');
}
} catch (error) {
console.error('✗ Enrollment update failed:', error.message);
throw error;
}
}

Best Practices

  • Verify Before Update: Always verify that both the card and new cardholder exist before attempting to update
  • Check Current Enrollment: Query the current enrollment details to understand what's changing
  • Notify Stakeholders: Inform the cardholder when their contact information will be used for a card
  • Verify After Update: Query the enrollment details after update to confirm the change
  • Audit Logging: Log all enrollment updates for security and compliance
  • Handle Errors Gracefully: Implement proper error handling and rollback mechanisms

Update Workflow

Standard Update Process

  1. Query Current Enrollment: Get current cardholder association
  2. Verify New Cardholder: Ensure new cardholder exists and has valid contact info
  3. Perform Update: Submit the enrollment update request
  4. Verify Update: Confirm the card is now associated with new cardholder
  5. Notify Users: Inform both old and new cardholders of the change
  6. Test OTP Delivery: Optionally test that OTP delivery works with new contact info

Migration Scenario

When migrating from one cardholder system to another:

def migrate_card_enrollments(old_cardholder_id, new_cardholder_id):
"""
Migrate all cards from old cardholder to new cardholder.
Useful during system migrations or account consolidation.
"""
# Find all cards associated with old cardholder
cards = find_cards_by_cardholder(old_cardholder_id)

results = {
'total': len(cards),
'migrated': 0,
'failed': 0,
'details': []
}

for card in cards:
try:
# Update each card to new cardholder
response = update_3ds_enrollment(card['card_id'], new_cardholder_id)

if response['code'] == 'SUCCESS':
results['migrated'] += 1
results['details'].append({
'card_id': card['card_id'],
'status': 'success'
})
else:
results['failed'] += 1
results['details'].append({
'card_id': card['card_id'],
'status': 'failed',
'error': response.get('message')
})
except Exception as e:
results['failed'] += 1
results['details'].append({
'card_id': card['card_id'],
'status': 'error',
'error': str(e)
})

print(f"Migration complete: {results['migrated']}/{results['total']} successful")
return results
  • Create Cardholder 3DS Contact - Create new cardholder profiles
  • Enroll Card for 3DS - Initial enrollment of a card for 3DS
  • Query 3DS Enrollment Details - Check current enrollment status by card
  • Update 3DS Contact - Update cardholder contact information
  • Unenroll Card from 3DS - Remove 3DS enrollment from a card

Troubleshooting

Card Not Found (404)

  • Cause: Invalid card_id or card doesn't exist
  • Solution: Verify the card_id is correct and the card exists in your system

Cardholder Not Found (404)

  • Cause: Invalid cardholder_id or cardholder doesn't exist
  • Solution:
    • Verify the cardholder_id is correct
    • Create the cardholder first if they don't exist

Card Not Enrolled (404)

  • Cause: Card is not currently enrolled for 3DS
  • Solution:
    • Use the Enroll endpoint first to enroll the card
    • Verify the card was previously enrolled successfully

Update Has No Effect

  • Cause: Possible caching or the same cardholder ID was provided
  • Solution:
    • Ensure you're using a different cardholder_id
    • Query enrollment details to verify the current state
    • Wait a moment and query again to check if update propagated

Forbidden Access (403)

  • Cause: 3DS feature not enabled for your account
  • Solution: Contact your account manager to enable this feature

Security Considerations

  • Authorization: Only authorized users should be able to update card enrollments
  • Validation: Validate both card and cardholder ownership before allowing updates
  • Audit Trail: Maintain detailed logs of all enrollment changes
  • Notification: Notify relevant parties when enrollment is updated
  • Rate Limiting: Implement rate limiting to prevent abuse
  • Two-Factor Authentication: Consider requiring 2FA for enrollment updates

Impact and Implications

OTP Delivery Changes

After updating enrollment:

  • OTPs will be sent to the new cardholder's email
  • OTPs will be sent to the new cardholder's mobile number
  • Previous cardholder will no longer receive OTPs for this card

Transaction Authorization

  • All future 3DS transactions will use the new cardholder's contact information
  • Ensure the new cardholder is aware and can access their email/mobile
  • Test OTP delivery after update to confirm functionality

Interactive API Explorer