telnesstech

Payment processing

Handle recurring billing, payment methods, invoicing, and payment failures using the API

This guide covers payment processing for a telecom service from end to end. It takes you through the first collection, the stored payment methods, the billing, and a failed payment.

Prerequisites

You need all of these before you start:

  • Order management: Understanding of order creation and pricing flows
  • Customer management: Active customers with subscription services
  • Payment gateway integration: Access to payment processors (cards, bank transfers, digital wallets)
  • Billing system: Understanding of billing cycles and pricing models
  • Compliance: PCI DSS compliance for handling payment data

Overview

Payment processing encompasses:

  1. Payment session creation for secure payment collection
  2. Payment profile management for stored payment methods
  3. Payment processing and transaction handling
  4. Payment failure management and retry logic
  5. Billing and invoice management
  6. Promotional pricing and discount handling

Step-by-step implementation

Step 1: Create payment sessions for orders

Create secure payment sessions to collect payment for orders:

# Create payment session for order
curl -X POST "{BASE_URL}/payment-sessions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "123e4567-e89b-12d3-a456-426614174000",
    "paymentProvider": "STRIPE",
    "hosted": true,
    "returnUrl": "https://yourstore.com/payment/success",
    "cancelUrl": "https://yourstore.com/payment/cancel"
  }'

# Check payment session status
curl -X GET "{BASE_URL}/payment-sessions/{paymentSessionId}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json"
// Create payment session for order
const createOrderPaymentSession = async (orderId, paymentOptions = {}) => {
  const sessionData = {
    orderId: orderId,
    paymentProvider: paymentOptions.paymentProvider || 'STRIPE',
    hosted: true,
    returnUrl: paymentOptions.returnUrl || `${window.location.origin}/payment/success`,
    cancelUrl: paymentOptions.cancelUrl || `${window.location.origin}/payment/cancel`,
    metadata: {
      source: 'customer_portal',
      timestamp: new Date().toISOString(),
    },
  };

  const response = await fetch('{BASE_URL}/payment-sessions', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(sessionData),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Payment session creation failed: ${error.message}`);
  }

  return await response.json();
};

// Monitor payment session status
const checkPaymentSessionStatus = async (paymentSessionId) => {
  const response = await fetch(`{BASE_URL}/payment-sessions/${paymentSessionId}`, {
    method: 'GET',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
  });

  const session = await response.json();

  return {
    status: session.status,
    paymentSessionId: session.paymentSessionId,
    orderId: session.orderId,
    hostedUrl: session.hostedUrl,
    updatedAt: session.updatedAt,
  };
};

// Look up the customer attached to a session's order
const getOrderCustomer = async (orderId) => {
  const response = await fetch(`{BASE_URL}/orders/${orderId}`, {
    method: 'GET',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
  });

  const order = await response.json();

  return order.customer;
};

// Payment session component
const PaymentSessionHandler = ({ orderId, onSuccess, onFailure }) => {
  const [paymentSession, setPaymentSession] = useState(null);
  const [loading, setLoading] = useState(false);

  const initiatePayment = async () => {
    setLoading(true);
    try {
      const session = await createOrderPaymentSession(orderId, {
        returnUrl: `${window.location.origin}/orders/${orderId}/success`,
        cancelUrl: `${window.location.origin}/orders/${orderId}/cancel`,
      });

      setPaymentSession(session);

      // Redirect to payment URL
      window.location.href = session.hostedUrl;
    } catch (error) {
      console.error('Payment session creation failed:', error);
      onFailure(error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="payment-session">
      <button onClick={initiatePayment} disabled={loading}>
        {loading ? 'Creating Payment Session...' : 'Pay Now'}
      </button>
    </div>
  );
};

Step 2: Manage payment profiles

Set up stored payment methods for recurring billing:

# Create payment profile session
curl -X POST "{BASE_URL}/payment-profiles/sessions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "returnUrl": "https://yourstore.com/billing/payment-methods",
    "paymentMethods": ["card", "bank_account"]
  }'

# Get payment profiles
curl -X GET "{BASE_URL}/payment-profiles" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json"
// Create payment profile session for storing payment methods
const createPaymentProfileSession = async (returnUrl, customerContext) => {
  const sessionData = {
    returnUrl: returnUrl,
    paymentMethods: ['card', 'bank_account'],
    metadata: {
      customerId: customerContext.customerId,
      source: 'billing_setup',
    },
  };

  const response = await fetch('{BASE_URL}/payment-profiles/sessions', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(sessionData),
  });

  return await response.json();
};

// Get stored payment profiles
const getPaymentProfiles = async () => {
  const response = await fetch('{BASE_URL}/payment-profiles', {
    method: 'GET',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
  });

  return await response.json();
};

// Get specific payment profile
const getPaymentProfile = async (paymentProfileId) => {
  const response = await fetch(`{BASE_URL}/payment-profiles/${paymentProfileId}`, {
    method: 'GET',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
  });

  return await response.json();
};

Step 3: Process payments

Handle payment processing and transaction management:

# List payments
curl -X GET "{BASE_URL}/payments?customerId={customerId}&limit=50" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json"

# Get payment details
curl -X GET "{BASE_URL}/payments/{paymentId}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json"
// List payments with filtering
const getPayments = async (filters = {}) => {
  const params = new URLSearchParams();

  if (filters.customerId) params.append('customerId', filters.customerId);
  if (filters.status) params.append('status', filters.status);
  if (filters.dateFrom) params.append('dateFrom', filters.dateFrom);
  if (filters.dateTo) params.append('dateTo', filters.dateTo);
  if (filters.limit) params.append('limit', filters.limit);
  if (filters.offset) params.append('offset', filters.offset);

  const response = await fetch(`{BASE_URL}/payments?${params}`, {
    method: 'GET',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
  });

  return await response.json();
};

// Get specific payment details
const getPaymentDetails = async (paymentId) => {
  const response = await fetch(`{BASE_URL}/payments/${paymentId}`, {
    method: 'GET',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
  });

  return await response.json();
};

// Payment management component
const PaymentManager = ({ customerId }) => {
  const [payments, setPayments] = useState([]);
  const [paymentProfiles, setPaymentProfiles] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    Promise.all([getPayments({ customerId, limit: 50 }), getPaymentProfiles()]).then(
      ([paymentsData, profilesData]) => {
        setPayments(paymentsData.items || []);
        setPaymentProfiles(profilesData.items || []);
        setLoading(false);
      },
    );
  }, [customerId]);

  const handleAddPaymentMethod = async () => {
    try {
      const session = await createPaymentProfileSession(
        `${window.location.origin}/billing/payment-methods`,
        { customerId },
      );
      window.location.href = session.hostedUrl;
    } catch (error) {
      console.error('Failed to create payment profile session:', error);
    }
  };

  if (loading) return <div>Loading payment information...</div>;

  return (
    <div className="payment-manager">
      <h3>Payment Methods</h3>
      <div className="payment-profiles">
        {paymentProfiles.map((profile) => (
          <div key={profile.paymentProfileId} className="payment-profile">
            <span>
              {profile.brand} •••• {profile.lastFour}
            </span>
            <span>
              Expires {profile.expiryMonth}/{profile.expiryYear}
            </span>
          </div>
        ))}
        <button onClick={handleAddPaymentMethod}>Add Payment Method</button>
      </div>

      <h3>Payment History</h3>
      <div className="payment-history">
        {payments.map((payment) => (
          <div key={payment.paymentId} className="payment-item">
            <span>{payment.description}</span>
            <span>
              {new Intl.NumberFormat('en-US', {
                style: 'currency',
                currency: payment.currency,
              }).format(payment.amountMinor / 100)}
            </span>
            <span className={`status ${payment.status}`}>{payment.status}</span>
            <span>{new Date(payment.createdAt).toLocaleDateString()}</span>
          </div>
        ))}
      </div>
    </div>
  );
};

Handling payment failures and retries

Handle a failed payment with retry logic:

// Payment failure handler with exponential backoff
class PaymentRetryHandler {
  constructor(maxRetries = 3) {
    this.maxRetries = maxRetries;
    this.retryDelays = [24, 72, 168]; // Hours: 1 day, 3 days, 1 week
  }

  async handlePaymentFailure(paymentSessionId, failureReason) {
    console.log(`Payment failed for session ${paymentSessionId}: ${failureReason}`);

    // Get payment session details
    const session = await checkPaymentSessionStatus(paymentSessionId);
    const customer = await getOrderCustomer(session.orderId);

    // Categorize failure type
    const failureCategory = this.categorizeFailure(failureReason);

    switch (failureCategory) {
      case 'insufficient_funds':
        await this.scheduleRetry(paymentSessionId, 24); // Retry in 24 hours
        await this.notifyCustomer(customer.customerId, 'insufficient_funds');
        break;

      case 'expired_card':
        await this.requestPaymentMethodUpdate(customer.customerId);
        break;

      case 'fraud_suspected':
        await this.escalateToFraud(session);
        break;

      case 'technical_error':
        await this.scheduleRetry(paymentSessionId, 1); // Retry in 1 hour
        break;

      default:
        await this.escalateToSupport(session);
    }
  }

  categorizeFailure(reason) {
    const failureMap = {
      insufficient_funds: 'insufficient_funds',
      card_declined: 'insufficient_funds',
      expired_card: 'expired_card',
      invalid_cvc: 'expired_card',
      fraud_suspected: 'fraud_suspected',
      processing_error: 'technical_error',
      network_error: 'technical_error',
    };

    return failureMap[reason] || 'unknown';
  }

  async scheduleRetry(paymentSessionId, delayHours) {
    // In production, this would schedule a background job
    setTimeout(
      async () => {
        try {
          // Create new payment session with same order
          const originalSession = await checkPaymentSessionStatus(paymentSessionId);
          const customer = await getOrderCustomer(originalSession.orderId);
          const newSession = await createOrderPaymentSession(originalSession.orderId);

          // Notify customer of retry attempt
          await this.notifyCustomerRetry(customer.customerId, newSession.hostedUrl);
        } catch (error) {
          console.error('Payment retry failed:', error);
        }
      },
      delayHours * 60 * 60 * 1000,
    );
  }

  async notifyCustomer(customerId, failureType) {
    // Implement customer notification logic
    console.log(`Notifying customer ${customerId} about ${failureType}`);
  }

  async requestPaymentMethodUpdate(customerId) {
    // Create payment profile session for updating payment method
    const session = await createPaymentProfileSession(
      `${process.env.BASE_URL}/billing/update-payment`,
      { customerId },
    );

    // Send email with update link
    await this.notifyCustomer(customerId, 'payment_method_update_required');
  }
}

Promotional pricing and discounts

Handle promotional codes and discount applications:

// Get promotion by promo code
const getPromotionByCode = async (promoCode) => {
  const response = await fetch(`{BASE_URL}/discounts/promotions/promo-code/${promoCode}`, {
    method: 'GET',
    headers: {
      Authorization: 'Bearer YOUR_ACCESS_TOKEN',
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Invalid promo code: ${error.message}`);
  }

  return await response.json();
};

// Apply promo code to an order and re-price it
const applyPromoCodeToOrder = async (orderId, promoCode) => {
  try {
    // Validate promo code first
    const promotion = await getPromotionByCode(promoCode);

    // The promo code lives on the order itself
    const updateResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
      method: 'PUT',
      headers: {
        Authorization: 'Bearer YOUR_ACCESS_TOKEN',
        'X-API-Key': 'YOUR_API_KEY',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ promoCode }),
    });

    if (!updateResponse.ok) {
      const error = await updateResponse.json();
      throw new Error(`Failed to apply promo code: ${error.message}`);
    }

    // Read the order back to see the promotion applied
    const pricingResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
      headers: {
        Authorization: 'Bearer YOUR_ACCESS_TOKEN',
        'X-API-Key': 'YOUR_API_KEY',
      },
    });

    if (!pricingResponse.ok) {
      const error = await pricingResponse.json();
      throw new Error(`Reading the order failed: ${error.message}`);
    }

    const { pricing } = await pricingResponse.json();
    const discountsMinor = (pricing.lineItems ?? []).reduce(
      (sum, item) => sum + (item.totalDiscountsMinor ?? 0),
      0,
    );

    // Amounts are integers in the minor units of pricing.currency: 2749 is $27.49
    return {
      subtotalMinor: pricing.subtotalMinor,
      discountsMinor,
      totalMinor: pricing.totalMinor,
      currency: pricing.currency,
      promotion,
    };
  } catch (error) {
    console.error('Failed to apply promo code:', error);
    throw error;
  }
};

// Promo code component
const PromoCodeInput = ({ orderId, onApplied, onError }) => {
  const [promoCode, setPromoCode] = useState('');
  const [loading, setLoading] = useState(false);
  const [applied, setApplied] = useState(null);

  const handleApply = async () => {
    if (!promoCode.trim()) return;

    setLoading(true);
    try {
      const result = await applyPromoCodeToOrder(orderId, promoCode);
      setApplied(result);
      onApplied(result);
    } catch (error) {
      onError(error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="promo-code-input">
      <input
        type="text"
        placeholder="Enter promo code"
        value={promoCode}
        onChange={(e) => setPromoCode(e.target.value.toUpperCase())}
        disabled={loading || applied}
      />
      <button onClick={handleApply} disabled={loading || applied}>
        {loading ? 'Applying...' : 'Apply'}
      </button>

      {applied && (
        <div className="promo-applied">
          <p>✓ {applied.promotion.discount.description} applied</p>
          <p>
            Discount: -
            {new Intl.NumberFormat('en-US', {
              style: 'currency',
              currency: applied.currency,
            }).format(applied.discountsMinor / 100)}
          </p>
        </div>
      )}
    </div>
  );
};

Next steps

After implementing payment processing:

Best practices

Security

  • Never store raw payment card data - use tokenized payment profiles
  • Implement PCI DSS compliance for card processing
  • Use HTTPS for all payment-related communications
  • Validate all payment webhooks and callbacks

User experience

  • Provide clear payment status updates to customers
  • Implement user-friendly error messages for payment failures
  • Offer multiple payment methods when possible
  • Save successful payment methods for future use

Reliability

  • Retry a failed payment on a schedule that you control.
  • Handle payment processor downtime gracefully
  • Monitor payment success rates and failure patterns
  • Set up alerts for payment processing issues

Common questions

Q: How do I handle different currencies? A: The API supports more than one currency. Name the currency in the payment session. Your payment processor must support that currency.

Q: Can I process refunds through the API? A: A refund normally goes through your payment processor’s dashboard or API, then reflected in the API payment records.

Q: How do I implement recurring billing? A: Use stored payment profiles with scheduled payment sessions. The billing system can automatically create payment sessions for recurring charges.