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
| Header | Value | Required | Description |
|---|---|---|---|
Accept | application/json | Yes | Content type for the response |
Authorization | Bearer {access_token} | Yes | Bearer token for authentication |
x-api-key | API Key | Yes | API key for authentication |
Content-Type | application/json | Yes | Request 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
| Field | Type | Required | Description |
|---|---|---|---|
card_id | string | Yes | The unique ID of the card to be updated |
cardholder_id | string | Yes | The unique ID of the new cardholder to be associated with the specified card |
Response
Success Response (200 OK)
{
"code": "SUCCESS"
}
Response Fields
| Field | Type | Description |
|---|---|---|
code | string | Status string indicating the result. "SUCCESS" refers to successful update |
Error Responses
- 400 Bad Request: Invalid
card_idorcardholder_idprovided - 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
- Query Current Enrollment: Get current cardholder association
- Verify New Cardholder: Ensure new cardholder exists and has valid contact info
- Perform Update: Submit the enrollment update request
- Verify Update: Confirm the card is now associated with new cardholder
- Notify Users: Inform both old and new cardholders of the change
- 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
Related Endpoints
- 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_idor card doesn't exist - Solution: Verify the
card_idis correct and the card exists in your system
Cardholder Not Found (404)
- Cause: Invalid
cardholder_idor cardholder doesn't exist - Solution:
- Verify the
cardholder_idis correct - Create the cardholder first if they don't exist
- Verify the
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
- Ensure you're using a different
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