Skip to main content

Common Errors

This guide covers common errors you may encounter when working with the Consent Management API and how to resolve them.

400 Bad Request

Missing Required Consents

Error:
{
  "error": "Validation error",
  "details": [
    "Missing required consent: termsAndPrivacy for policy type: global"
  ]
}
Cause: Not all required consents for the policy type are present in the request. Required Consents by Policy:
  • US Policy: eSignAct (E-Sign Act compliance), termsAndPrivacy, marketingNotifications, smsNotifications, emailNotifications
  • Global Policy: termsAndPrivacy, marketingNotifications, smsNotifications, emailNotifications (excludes eSignAct)
Solution:
const requiredConsents = {
  global: [
    'eSignAct',
    'termsAndPrivacy',
    'marketingNotifications',
    'smsNotifications',
    'emailNotifications'
  ],
  US: [
    'termsAndPrivacy',
    'marketingNotifications',
    'smsNotifications',
    'emailNotifications'
  ]
};

function validateConsents(policyType: 'global' | 'US', consents: ConsentItem[]) {
  const required = requiredConsents[policyType];
  const providedTypes = consents.map(c => c.consentType);

  const missing = required.filter(type => !providedTypes.includes(type));

  if (missing.length > 0) {
    throw new Error(`Missing required consents: ${missing.join(', ')}`);
  }

  return true;
}

validateConsents('global', consents);

Error:
{
  "error": "Validation error",
  "details": [
    "Invalid consentType: 'pushNotifications'. Must be one of: eSignAct, termsAndPrivacy, marketingNotifications, smsNotifications, emailNotifications"
  ]
}
Cause: Using a consent type that’s not supported by the API. Valid Consent Types:
  • eSignAct
  • termsAndPrivacy
  • marketingNotifications
  • smsNotifications
  • emailNotifications
Solution:
const VALID_CONSENT_TYPES = [
  'eSignAct',
  'termsAndPrivacy',
  'marketingNotifications',
  'smsNotifications',
  'emailNotifications'
] as const;

type ConsentType = typeof VALID_CONSENT_TYPES[number];

function isValidConsentType(type: string): type is ConsentType {
  return VALID_CONSENT_TYPES.includes(type as ConsentType);
}

if (!isValidConsentType(consentType)) {
  throw new Error(`Invalid consent type: ${consentType}`);
}

Error:
{
  "error": "Validation error",
  "details": [
    "Invalid consentStatus: 'pending'. Must be one of: granted, denied"
  ]
}
Cause: Using an invalid status value during consent creation. Valid Status Values:
  • For creation: granted, denied
  • For revocation: revoked (set automatically)
Solution:
type ConsentStatus = 'granted' | 'denied';

function validateConsentStatus(status: string): status is ConsentStatus {
  return status === 'granted' || status === 'denied';
}

const consent = {
  consentType: 'marketingNotifications',
  consentStatus: validateConsentStatus(status) ? status : 'denied'
};

Empty Onboarding ID

Error:
{
  "error": "Validation error",
  "details": [
    "onboardingId is required and must not be empty"
  ]
}
Cause: Missing or empty onboardingId in request. Solution:
import { v4 as uuidv4 } from 'uuid';

function generateOnboardingId(): string {
  const timestamp = Date.now();
  const uuid = uuidv4();
  return `onboarding_${timestamp}_${uuid}`;
}

const onboardingId = generateOnboardingId();

Missing Tenant ID

Error:
{
  "error": "Validation error",
  "details": [
    "tenantId is required"
  ]
}
Cause: Missing tenantId field in request. Solution:
const requestBody = {
  onboardingId: onboardingId,
  tenantId: 'tenant_baanx_prod', // Required field
  policyType: 'global',
  consents: [...]
};
The tenantId value should be provided by Baanx during onboarding. Contact support if you don’t have your tenant ID.

409 Conflict

Duplicate Onboarding ID

Error:
{
  "error": "Conflict",
  "details": [
    "Consent set with onboardingId 'onboarding_abc123' already exists"
  ]
}
Cause: Attempting to create a consent set with an onboardingId that already exists. Solution: Generate a new unique onboardingId:
async function createConsentWithRetry(
  consents: ConsentItem[],
  maxRetries: number = 3
) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const onboardingId = generateOnboardingId();

    try {
      return await consentManager.createOnboardingConsent(
        onboardingId,
        consents,
        metadata
      );
    } catch (error) {
      if (error.status === 409 && attempt < maxRetries - 1) {
        console.log(`Duplicate onboardingId, retrying... (${attempt + 1}/${maxRetries})`);
        continue;
      }
      throw error;
    }
  }
}

User Already Linked

Error:
{
  "error": "Conflict",
  "details": [
    "This consent set is already linked to userId 'user_123abc456def'"
  ]
}
Cause: Attempting to link a userId to a consent set that’s already been linked. Solution: Check if consent set is already linked before attempting to link:
async function linkUserIfNeeded(consentSetId: string, userId: string) {
  try {
    const consentSet = await consentManager.getConsentSetById(consentSetId);

    if (consentSet.userId) {
      if (consentSet.userId === userId) {
        console.log('Consent set already linked to this user');
        return consentSet;
      } else {
        throw new Error(`Consent set is linked to different user: ${consentSet.userId}`);
      }
    }

    return await consentManager.linkUserToConsent(consentSetId, userId);
  } catch (error) {
    if (error.status === 409) {
      console.log('Consent set already linked, fetching current state...');
      return await consentManager.getConsentSetById(consentSetId);
    }
    throw error;
  }
}

404 Not Found

Error:
{
  "error": "Not found",
  "details": [
    "Consent set with ID '550e8400-e29b-41d4-a716-446655440001' not found"
  ]
}
Cause: Attempting to access a consent set that doesn’t exist or was deleted. Possible Reasons:
  • ❌ Incorrect consentSetId
  • ❌ Consent set created in different environment (sandbox vs production)
  • ❌ Using wrong tenant context
Solution:
async function getConsentSetSafely(consentSetId: string) {
  try {
    return await consentManager.getConsentSetById(consentSetId);
  } catch (error) {
    if (error.status === 404) {
      console.error(`Consent set not found: ${consentSetId}`);

      return null;
    }
    throw error;
  }
}

const consentSet = await getConsentSetSafely(consentSetId);
if (!consentSet) {
  console.log('Need to create new consent set');
}

User Not Found

Error:
{
  "error": "Not found",
  "details": [
    "No consent sets found for userId 'user_123abc456def'"
  ]
}
Cause: Attempting to retrieve consents for a user who has no consent sets. Solution: Handle missing consent gracefully:
async function getUserConsentOrDefault(userId: string) {
  try {
    const { consentStatus } = await consentManager.getUserConsentStatus(userId);
    return consentStatus;
  } catch (error) {
    if (error.status === 404) {
      console.log(`No consents found for user ${userId}`);
      return 'none';
    }
    throw error;
  }
}

const status = await getUserConsentOrDefault(userId);

if (status === 'none') {
  console.log('User needs to complete onboarding consent');
}

Error:
{
  "error": "Not found",
  "details": [
    "Consent with ID 'consent_123' not found in consent set"
  ]
}
Cause: Attempting to revoke a consent that doesn’t exist or has already been revoked. Solution:
async function revokeConsentSafely(
  consentSetId: string,
  consentId: string
) {
  try {
    return await consentManager.revokeConsent(consentSetId, consentId);
  } catch (error) {
    if (error.status === 404) {
      const consentSet = await consentManager.getConsentSetById(consentSetId);
      const consent = consentSet.consents.find(c => c.consentId === consentId);

      if (!consent) {
        console.log('Consent does not exist');
      } else if (consent.consentStatus === 'revoked') {
        console.log('Consent already revoked');
        return consent;
      }
    }
    throw error;
  }
}

498 Invalid Client Key

Error:
{
  "error": "Invalid client key",
  "details": [
    "The provided x-client-key is invalid or expired"
  ]
}
Cause: Missing, incorrect, or expired x-client-key header. Solution:
if (!clientKey || clientKey.trim() === '') {
  throw new Error('Client key is required');
}

const headers = {
  'x-client-key': clientKey,
  'x-secret-key': secretKey
};

await fetch('https://api.baanx.com/v2/consent/onboarding', {
  method: 'POST',
  headers,
  body: JSON.stringify(requestBody)
});
If your client key is expired or invalid, contact Baanx support to obtain new credentials.

499 Missing Client Key

Error:
{
  "error": "Missing client key",
  "details": [
    "x-client-key header is required for all requests"
  ]
}
Cause: x-client-key header not included in request. Solution: Ensure all requests include the required header:
const headers: Record<string, string> = {
  'Content-Type': 'application/json',
  'x-client-key': process.env.BAANX_CLIENT_KEY!
};

if (endpoint.requiresAuth) {
  headers['x-secret-key'] = process.env.BAANX_SECRET_KEY!;
}

await fetch(url, { method, headers, body });

500 Internal Server Error

Error:
{
  "error": "Internal server error",
  "details": [
    "An unexpected error occurred while creating the consent set"
  ]
}
Cause: Unexpected server-side error. Solution: Implement retry logic with exponential backoff:
async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error.status === 500 && attempt < maxRetries - 1) {
        const delay = baseDelay * Math.pow(2, attempt);
        console.log(`Server error, retrying in ${delay}ms... (${attempt + 1}/${maxRetries})`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      throw error;
    }
  }

  throw new Error('Max retries exceeded');
}

const result = await retryWithBackoff(() =>
  consentManager.createOnboardingConsent(onboardingId, consents, metadata)
);
If 500 errors persist, contact Baanx support with your request details and timestamp for investigation.

Common Integration Issues

Symptoms:
  • Created consent set with all required consents
  • Status returns incomplete instead of complete
Possible Causes:
  1. Using denied status for required consents:
    // ❌ This will result in incomplete status
    { consentType: 'termsAndPrivacy', consentStatus: 'denied' }
    
    Fix: Required consents must be granted:
    // ✅ Correct
    { consentType: 'termsAndPrivacy', consentStatus: 'granted' }
    
  2. Revoked consents: If any required consent has been revoked, status becomes incomplete. Check:
    const { consentSets } = await consentManager.getUserConsentStatus(userId, true);
    const revokedConsents = consentSets[0].consents.filter(
      c => c.consentStatus === 'revoked'
    );
    
    console.log('Revoked consents:', revokedConsents);
    

Symptoms:
  • Created consent set successfully
  • Can’t retrieve it using GET /v2/consent/user/{userId}
Cause: Consent set hasn’t been linked to a userId yet. Solution:
  1. After creating consent set, store the consentSetId:
    const { consentSetId } = await consentManager.createOnboardingConsent(...);
    await storage.set(`consent_${onboardingId}`, consentSetId);
    
  2. After user creation, link the consent set:
    const consentSetId = await storage.get(`consent_${onboardingId}`);
    await consentManager.linkUserToConsent(consentSetId, userId);
    
  3. Now you can retrieve by userId:
    const status = await consentManager.getUserConsentStatus(userId);
    

Issue: Metadata Not Appearing in Audit Trail

Symptoms:
  • Provided metadata in consent creation
  • Metadata missing or incomplete in audit records
Cause: Metadata field was not properly structured or contained invalid values. Solution: Ensure metadata is a valid JSON object:
const metadata = {
  ipAddress: req.ip || 'unknown',
  userAgent: req.headers['user-agent'] || 'unknown',
  timestamp: new Date().toISOString(),
  clientId: 'web-app-v1.2.0'
};

if (req.session?.id) {
  metadata.sessionId = req.session.id;
}

await consentManager.createOnboardingConsent(onboardingId, consents, metadata);
All metadata values must be serializable to JSON. Avoid passing functions, circular references, or undefined values.

Symptoms:
  • Received _links object in response
  • Following links results in 404 errors
Cause: Links are environment-specific and may point to incorrect environment. Solution: Parse and use the href from _links directly:
const response = await consentManager.getUserConsentStatus(userId);

if (response._links.audit) {
  const auditUrl = response._links.audit.href;
  const auditResponse = await fetch(auditUrl, {
    headers: { 'x-client-key': clientKey }
  });

  const auditData = await auditResponse.json();
}
Don’t construct URLs manually - use the provided links.

Issue: Rate Limiting

Symptoms:
  • Requests failing intermittently
  • 429 Too Many Requests errors
Solution: Implement rate limiting and request queuing:
class RateLimitedConsentManager {
  private queue: Array<() => Promise<any>> = [];
  private processing = false;
  private requestsPerSecond = 10;

  async enqueue<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          const result = await fn();
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });

      this.processQueue();
    });
  }

  private async processQueue() {
    if (this.processing || this.queue.length === 0) return;

    this.processing = true;

    while (this.queue.length > 0) {
      const fn = this.queue.shift()!;
      await fn();
      await new Promise(resolve => setTimeout(resolve, 1000 / this.requestsPerSecond));
    }

    this.processing = false;
  }
}

const rateLimitedManager = new RateLimitedConsentManager();

await rateLimitedManager.enqueue(() =>
  consentManager.createOnboardingConsent(...)
);

Debugging Tips

1. Enable Request Logging

async function loggedFetch(url: string, options: RequestInit) {
  console.log(`[REQUEST] ${options.method} ${url}`);
  console.log('[HEADERS]', options.headers);
  console.log('[BODY]', options.body);

  const startTime = Date.now();
  const response = await fetch(url, options);
  const duration = Date.now() - startTime;

  console.log(`[RESPONSE] ${response.status} (${duration}ms)`);

  const responseBody = await response.json();
  console.log('[RESPONSE BODY]', JSON.stringify(responseBody, null, 2));

  return { response, body: responseBody };
}

2. Validate Before Sending

function validateConsentRequest(request: any) {
  const errors: string[] = [];

  if (!request.onboardingId) errors.push('onboardingId is required');
  if (!request.tenantId) errors.push('tenantId is required');
  if (!request.policyType) errors.push('policyType is required');
  if (!request.consents || request.consents.length === 0) {
    errors.push('consents array is required');
  }

  const requiredConsents = request.policyType === 'US'
    ? 5
    : 4;

  if (request.consents.length < requiredConsents) {
    errors.push(`Missing required consents for ${request.policyType} policy`);
  }

  if (errors.length > 0) {
    throw new Error(`Validation failed: ${errors.join(', ')}`);
  }
}

validateConsentRequest(requestBody);

3. Test in Sandbox First

Always test consent flows in sandbox environment before production:
const environment = process.env.NODE_ENV === 'production' ? 'prod' : 'sandbox';

const baseUrl = environment === 'prod'
  ? 'https://api.baanx.com'
  : 'https://sandbox-api.baanx.com';

console.log(`[ENVIRONMENT] Using ${environment} environment`);

Getting Help

If you continue experiencing issues:
  1. Check API Status: Verify the Baanx API is operational
  2. Review Request/Response: Use logging to inspect full request and response
  3. Contact Support: Email [email protected] with:
    • Request timestamp
    • Request/response details (redact sensitive data)
    • x-request-id from response headers
    • Steps to reproduce

Next Steps