Skip to main content
Follow these best practices to build robust, secure, and reliable integrations with the Chariow API, especially when handling checkout flows.

Checkout API Best Practices

Always Handle All Response States

The checkout API returns different states that require different handling. Never assume a checkout will always require payment.
async function handleCheckout(checkoutData) {
  const response = await fetch('https://api.chariow.com/v1/checkout', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(checkoutData)
  });

  const result = await response.json();

  switch (result.data.step) {
    case 'awaiting_payment':
      // Redirect to payment - most common case
      return { action: 'redirect', url: result.data.payment.checkout_url };

    case 'completed':
      // Free product - sale completed immediately
      return { action: 'success', saleId: result.data.purchase.id };

    case 'already_purchased':
      // Customer already owns this product
      return { action: 'already_owned', message: result.data.message };

    default:
      // Handle unexpected states gracefully
      console.error('Unexpected checkout state:', result.data.step);
      return { action: 'error', message: 'Unexpected response from payment system' };
  }
}

Validate Data Before Submitting

Reduce failed checkouts by validating customer data on your end before calling the API.
function validateCheckoutData(data) {
  const errors = [];

  // Email validation
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!data.email || !emailRegex.test(data.email)) {
    errors.push('Valid email is required');
  }

  // Name validation
  if (!data.first_name || data.first_name.length > 50) {
    errors.push('First name is required (max 50 characters)');
  }
  if (!data.last_name || data.last_name.length > 50) {
    errors.push('Last name is required (max 50 characters)');
  }

  // Phone validation - numeric only
  const phoneNumber = data.phone?.number?.replace(/\D/g, '');
  if (!phoneNumber || phoneNumber.length < 6) {
    errors.push('Valid phone number is required');
  }

  // Country code validation
  const validCountryCodes = ['US', 'CA', 'GB', 'FR', 'DE', 'ES', 'IT', /* ... */];
  if (!data.phone?.country_code || !validCountryCodes.includes(data.phone.country_code)) {
    errors.push('Valid country code is required');
  }

  return {
    valid: errors.length === 0,
    errors
  };
}

Store Sale IDs Immediately

Always persist the sale ID as soon as you receive it. This is critical for customer support, refunds, and order tracking.
async function processCheckout(checkoutData, orderId) {
  const result = await initiateCheckout(checkoutData);

  if (result.data.purchase?.id) {
    // Store immediately - before any redirect
    await database.orders.update(orderId, {
      chariow_sale_id: result.data.purchase.id,
      checkout_initiated_at: new Date()
    });
  }

  return result;
}

Use Pulses for Reliable Sale Confirmation

Never rely solely on redirect URLs to confirm purchases. Payment pages can be closed, redirects can fail, and customers may not return to your site.
The redirect_url is for user experience only. Always use Pulses (webhooks) for reliable sale confirmation.
// Set up a Pulse endpoint for sale confirmation
app.post('/webhooks/chariow', async (req, res) => {
  const signature = req.headers['x-chariow-signature'];

  // Verify webhook signature
  if (!verifySignature(req.body, signature)) {
    return res.status(401).send('Invalid signature');
  }

  const { event, data } = req.body;

  switch (event) {
    case 'sale.completed':
      await fulfillOrder(data.id);
      await sendConfirmationEmail(data.customer.email);
      break;

    case 'sale.refunded':
      await revokeAccess(data.id);
      await sendRefundEmail(data.customer.email);
      break;
  }

  res.status(200).send('OK');
});

Security Best Practices

Never Expose API Keys in Client Code

API keys should only be used server-side. Never include them in JavaScript bundles, mobile apps, or any client-facing code.
// Server-side (Node.js)
const API_KEY = process.env.CHARIOW_API_KEY;

app.post('/api/checkout', async (req, res) => {
  const response = await fetch('https://api.chariow.com/v1/checkout', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(req.body)
  });

  res.json(await response.json());
});

Validate Webhook Signatures

Always verify that incoming webhooks are genuinely from Chariow by checking the signature.
const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

Use Environment Variables

Store all sensitive configuration in environment variables, never in code.
# .env file (never commit this)
CHARIOW_API_KEY=sk_live_your_api_key
CHARIOW_WEBHOOK_SECRET=whsec_your_webhook_secret
// Load from environment
const config = {
  apiKey: process.env.CHARIOW_API_KEY,
  webhookSecret: process.env.CHARIOW_WEBHOOK_SECRET
};

Error Handling Best Practices

Implement Comprehensive Error Handling

Handle all possible error scenarios gracefully to provide a good user experience.
async function safeCheckout(checkoutData) {
  try {
    const response = await fetch('https://api.chariow.com/v1/checkout', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(checkoutData)
    });

    // Handle HTTP errors
    if (!response.ok) {
      const error = await response.json();

      switch (response.status) {
        case 401:
          console.error('API key invalid');
          return { error: 'Payment system configuration error' };

        case 404:
          return { error: 'Product not found or unavailable' };

        case 422:
          // Validation errors - return field-specific messages
          return { error: 'Validation failed', fields: error.errors };

        case 429:
          return { error: 'Too many requests. Please try again shortly.' };

        default:
          return { error: 'Payment system unavailable. Please try again.' };
      }
    }

    return await response.json();

  } catch (networkError) {
    // Handle network failures
    console.error('Network error:', networkError);
    return { error: 'Unable to connect to payment system. Check your connection.' };
  }
}

Display User-Friendly Error Messages

Map API errors to helpful messages that guide users to fix issues.
const errorMessages = {
  'email': 'Please enter a valid email address',
  'phone.number': 'Please enter a valid phone number (digits only)',
  'phone.country_code': 'Please select your country',
  'address': 'Please enter your shipping address',
  'city': 'Please enter your city',
  'state': 'Please enter your state or region',
  'country': 'Please select your country',
  'zip': 'Please enter your postal code',
  'discount_code': 'This discount code is invalid or has expired'
};

function getFieldError(fieldName, apiErrors) {
  if (apiErrors[fieldName]) {
    return errorMessages[fieldName] || apiErrors[fieldName][0];
  }
  return null;
}

Log Errors for Debugging

Maintain detailed logs for troubleshooting while keeping sensitive data secure.
function logCheckoutError(error, checkoutData) {
  // Remove sensitive data before logging
  const safeData = {
    product_id: checkoutData.product_id,
    email: maskEmail(checkoutData.email),
    timestamp: new Date().toISOString(),
    error: error.message || error
  };

  console.error('Checkout failed:', JSON.stringify(safeData));

  // Send to monitoring service
  monitoring.captureError(error, { context: safeData });
}

function maskEmail(email) {
  const [local, domain] = email.split('@');
  return `${local.slice(0, 2)}***@${domain}`;
}

Performance Best Practices

Implement Request Timeouts

Don’t let API calls hang indefinitely. Set reasonable timeouts.
async function checkoutWithTimeout(checkoutData, timeoutMs = 30000) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch('https://api.chariow.com/v1/checkout', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(checkoutData),
      signal: controller.signal
    });

    return await response.json();
  } finally {
    clearTimeout(timeout);
  }
}

Cache Product Data

Reduce API calls by caching product information that doesn’t change frequently.
const productCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getProduct(productId) {
  const cached = productCache.get(productId);

  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }

  const response = await fetch(`https://api.chariow.com/v1/products/${productId}`, {
    headers: { 'Authorization': `Bearer ${API_KEY}` }
  });

  const data = await response.json();

  productCache.set(productId, {
    data: data.data,
    timestamp: Date.now()
  });

  return data.data;
}

Respect Rate Limits

Handle rate limiting gracefully with exponential backoff.
async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After') || Math.pow(2, attempt);
      console.log(`Rate limited. Retrying after ${retryAfter}s`);
      await sleep(retryAfter * 1000);
      continue;
    }

    return response;
  }

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

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Testing Best Practices

Test All Checkout Scenarios

Create comprehensive tests for every possible checkout outcome.
describe('Checkout Integration', () => {
  test('handles paid product checkout', async () => {
    const result = await handleCheckout({
      product_id: 'prd_paid_product',
      email: '[email protected]',
      // ...
    });

    expect(result.action).toBe('redirect');
    expect(result.url).toContain('payment.chariow.com');
  });

  test('handles free product checkout', async () => {
    const result = await handleCheckout({
      product_id: 'prd_free_product',
      email: '[email protected]',
      // ...
    });

    expect(result.action).toBe('success');
    expect(result.saleId).toBeDefined();
  });

  test('handles already purchased product', async () => {
    const result = await handleCheckout({
      product_id: 'prd_already_owned',
      email: '[email protected]',
      // ...
    });

    expect(result.action).toBe('already_owned');
  });

  test('handles invalid product gracefully', async () => {
    const result = await handleCheckout({
      product_id: 'prd_nonexistent',
      email: '[email protected]',
      // ...
    });

    expect(result.error).toBeDefined();
  });

  test('handles shipping required products', async () => {
    const result = await handleCheckout({
      product_id: 'prd_physical',
      email: '[email protected]',
      address: '123 Main St',
      city: 'New York',
      state: 'NY',
      country: 'US',
      zip: '10001',
      // ...
    });

    expect(result.action).toBe('redirect');
  });
});

Use Staging Environment for Integration Tests

Always test against a staging environment before deploying to production.
const config = {
  development: {
    apiUrl: 'https://api.staging.chariow.com/v1',
    apiKey: process.env.CHARIOW_STAGING_KEY
  },
  production: {
    apiUrl: 'https://api.chariow.com/v1',
    apiKey: process.env.CHARIOW_LIVE_KEY
  }
};

const env = process.env.NODE_ENV || 'development';
const apiConfig = config[env];

Checklist for Production

Before going live, ensure you’ve completed this checklist:
  • API keys stored in environment variables
  • API calls made server-side only
  • Webhook signatures verified
  • HTTPS used for all endpoints
  • Sensitive data not logged
  • All HTTP status codes handled
  • Network errors caught
  • User-friendly error messages displayed
  • Errors logged for debugging
  • Rate limiting handled gracefully
  • All checkout states handled (awaiting_payment, completed, already_purchased)
  • Sale IDs stored immediately
  • Pulses configured for sale confirmation
  • Customer data validated before submission
  • Shipping fields included when required
  • Unit tests for checkout logic
  • Integration tests with staging API
  • All product types tested
  • Error scenarios tested
  • Load testing completed

Next Steps