Docs
Stripe Payment API

Stripe Payment API

Create payment intents and setup intents using your organization's Stripe configuration.

The Stripe Payment API allows you to create payment intents and setup intents using your organization's configured Stripe keys. All payment processing is done securely through Stripe with automatic audit logging.

Base URL

All API requests should be made to:

https://dev.api.evoncrm.com

Prerequisites

Before using these endpoints, you must:

  1. Configure your Stripe keys in Settings → Payments
  2. Obtain your organization API key from Settings → Developers

Setup Intent

Create a Setup Intent to collect payment method details without charging the customer. Useful for storing payment methods for future charges.

Endpoint

POST /api/stripe/setup-intent

Headers

X-API-Key: your_api_key_here
Content-Type: application/json

Request Body

{}

The endpoint requires no body parameters.

Success Response

{
  "success": true,
  "paymentSecret": "seti_1SOmkA9N4MgodpWpHi2XRjmW_secret_TLThFsMJbLGmpCUjynCG2LPb7egqvTF"
}

Response Fields

FieldTypeDescription
successbooleanIndicates if the request was successful
paymentSecretstringClient secret for confirming the setup intent on the client side

Example Request

curl -X POST "https://dev.api.evoncrm.com/api/stripe/setup-intent" \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{}'

Client-Side Usage

Use the paymentSecret with Stripe.js to collect payment method details:

const stripe = Stripe('your_publishable_key');
 
const { error, setupIntent } = await stripe.confirmCardSetup(paymentSecret, {
  payment_method: {
    card: cardElement,
    billing_details: {
      email: 'customer@example.com'
    }
  }
});
 
if (setupIntent.status === 'succeeded') {
  // Payment method saved successfully
  console.log('Setup Intent ID:', setupIntent.id);
}

Error Responses

400 Bad Request - Stripe Keys Not Configured

{
  "success": false,
  "error": "Stripe keys not configured for this organization"
}

401 Unauthorized - Invalid API Key

{
  "error": "Unauthorized. API key is missing or invalid."
}

500 Internal Server Error

{
  "success": false,
  "error": "Failed to create setup intent"
}

Payment Intent

Create a Payment Intent to accept payments including Buy Now Pay Later (BNPL) methods like Affirm and Klarna.

Endpoint

POST /api/stripe/payment-intent

Headers

X-API-Key: your_api_key_here
Content-Type: application/json

Request Body

{
  "amount": 149.99,
  "currency": "usd",
  "description": "Premium Subscription - Annual",
  "metadata": {
    "orderId": "ORD-12345",
    "customerId": "CUST-67890"
  },
  "payment_method_types": ["card", "affirm", "klarna"]
}

Parameters

FieldTypeRequiredDefaultDescription
amountnumberYes-Amount in dollars (will be converted to cents)
currencystringNo"usd"Three-letter ISO currency code
descriptionstringNo-Description of the payment
metadataobjectNo-Key-value pairs for additional information
payment_method_typesarrayNo["card", "affirm", "klarna"]Accepted payment methods

Supported Payment Methods

  • card - Credit/debit cards
  • affirm - Affirm (BNPL) - Minimum amount: $35.00 USD
  • klarna - Klarna (BNPL)
  • afterpay_clearpay - Afterpay/Clearpay (BNPL)
  • cashapp - Cash App Pay
  • link - Link by Stripe

Note: Affirm requires a minimum amount of $35.00 USD. If your payment amount is below this threshold, you'll receive an error message.

Supported Currencies

Common currencies include:

  • usd - US Dollar
  • eur - Euro
  • gbp - British Pound
  • cad - Canadian Dollar
  • aud - Australian Dollar
  • jpy - Japanese Yen

See Stripe's currency documentation for the complete list.

Success Response

{
  "success": true,
  "paymentSecret": "pi_3SOmkA9N4MgodpWp0DVEKK6L_secret_WZRbbD1fzTFjCdx54o25tsaKy",
  "paymentIntentId": "pi_3SOmkA9N4MgodpWp0DVEKK6L",
  "amount": 10050
}

Response Fields

FieldTypeDescription
successbooleanIndicates if the request was successful
paymentSecretstringClient secret for confirming the payment on the client side
paymentIntentIdstringUnique ID of the payment intent
amountnumberAmount in cents (smallest currency unit)

Example Request

curl -X POST "https://dev.api.evoncrm.com/api/stripe/payment-intent" \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 299.00,
    "currency": "usd",
    "description": "Weight Management Program",
    "metadata": {
      "programId": "glp-1-semaglutide",
      "patientId": "PAT-98765"
    },
    "payment_method_types": ["card", "affirm"]
  }'

Client-Side Usage

Use the paymentSecret with Stripe.js to collect payment:

const stripe = Stripe('your_publishable_key');
 
// For card payments
const { error, paymentIntent } = await stripe.confirmCardPayment(
  paymentSecret,
  {
    payment_method: {
      card: cardElement,
      billing_details: {
        name: 'John Doe',
        email: 'john@example.com'
      }
    }
  }
);
 
// For BNPL methods (e.g., Affirm)
const { error, paymentIntent } = await stripe.confirmAffirmPayment(
  paymentSecret,
  {
    return_url: 'https://yoursite.com/checkout/complete'
  }
);
 
if (paymentIntent.status === 'requires_capture') {
  // Payment authorized, ready to capture
  console.log('Payment Intent ID:', paymentIntent.id);
}

Payment Capture

Payment intents are created with capture_method: 'manual', meaning:

  1. Authorization: Payment is authorized but not charged
  2. Capture: You must capture the payment separately to charge the customer

To capture the payment, use Stripe's API or dashboard:

# Capture via Stripe API
curl -X POST "https://api.stripe.com/v1/payment_intents/{paymentIntentId}/capture" \
  -u "your_stripe_secret_key:" \
  -d "amount_to_capture=14999"

Error Responses

400 Bad Request - Validation Error

{
  "success": false,
  "error": "Invalid request data",
  "details": [
    {
      "code": "too_small",
      "minimum": 0,
      "type": "number",
      "inclusive": false,
      "exact": false,
      "message": "Amount must be positive",
      "path": ["amount"]
    }
  ]
}

400 Bad Request - Minimum Amount for Affirm

{
  "success": false,
  "error": "Amount must be no less than $35.00 USD for the Affirm payment method."
}

400 Bad Request - Stripe Keys Not Configured

{
  "success": false,
  "error": "Stripe keys not configured for this organization"
}

401 Unauthorized - Invalid API Key

{
  "error": "Unauthorized. API key is missing or invalid."
}

500 Internal Server Error

{
  "success": false,
  "error": "Failed to create payment intent"
}

Create Intents (Combined)

Create both a Setup Intent and Payment Intent in a single request based on a product price ID. This endpoint automatically calculates discounts if a coupon code is provided.

Endpoint

POST /api/stripe/create-intents

Headers

X-API-Key: your_api_key_here
Content-Type: application/json

Request Body

{
  "priceId": "price_asdf98asdf98as98sdf",
  "couponCode": "SAVE20"
}

Parameters

FieldTypeRequiredDescription
priceIdstringYesThe ID of the product price from your database
couponCodestringNoStripe coupon code to apply discount

Success Response

{
  "success": true,
  "price": {
    "priceId": "price_asdf98asdf98as98sdf",
    "productId": "prod_cmg4x57yp0018r9p7q73eghxb",
    "productName": "Injectable Semaglutide",
    "productType": "SEMAGLUTIDE",
    "productDescription": "GLP-1 weight management medication",
    "originalAmount": 758,
    "discount": 151.6,
    "finalAmount": 606.4,
    "currency": "USD",
    "type": "RECURRING",
    "recurringInterval": "MONTH",
    "recurringIntervalCount": 1,
    "label": "Monthly Subscription",
    "coupon": {
      "code": "SAVE20",
      "percentOff": 20,
      "amountOff": null,
      "valid": true
    }
  },
  "paymentProviders": {
    "stripe": {
      "setupIntent": {
        "clientSecret": "seti_1ABC2def3GHI4jkl_secret_5MNO6pqr7STU8vwx",
        "id": "seti_1ABC2def3GHI4jkl"
      },
      "paymentIntent": {
        "clientSecret": "pi_1XYZ2abc3DEF4ghi_secret_5JKL6mno7PQR8stu",
        "id": "pi_1XYZ2abc3DEF4ghi",
        "amount": 60640,
        "currency": "usd"
      }
    }
  }
}

Response Fields

FieldTypeDescription
successbooleanIndicates if the request was successful
price.priceIdstringID of the product price
price.productIdstringID of the product
price.productNamestringName of the product
price.productTypestringType of product (SEMAGLUTIDE, TIRZEPATIDE)
price.productDescriptionstring | nullProduct description
price.originalAmountnumberOriginal price in dollars
price.discountnumberDiscount amount in dollars (0 if no coupon)
price.finalAmountnumberFinal price after discount in dollars
price.currencystringCurrency code (uppercase)
price.typestringPrice type: ONE_TIME or RECURRING
price.recurringIntervalstring | nullFor recurring: DAY, WEEK, MONTH, or YEAR
price.recurringIntervalCountnumber | nullNumber of intervals between charges
price.labelstring | nullOptional price label/description
price.couponobjectCoupon details (only present if couponCode was provided and valid)
price.coupon.codestringThe coupon code used
price.coupon.percentOffnumber | nullPercentage discount (if percentage-based)
price.coupon.amountOffnumber | nullFixed amount discount in cents (if amount-based)
price.coupon.validbooleanWhether the coupon was successfully applied
paymentProviders.stripe.setupIntentobjectSetup Intent details for saving payment method
paymentProviders.stripe.setupIntent.clientSecretstringClient secret for confirming setup intent
paymentProviders.stripe.setupIntent.idstringSetup Intent ID
paymentProviders.stripe.paymentIntentobjectPayment Intent details for processing payment
paymentProviders.stripe.paymentIntent.clientSecretstringClient secret for confirming payment intent
paymentProviders.stripe.paymentIntent.idstringPayment Intent ID
paymentProviders.stripe.paymentIntent.amountnumberAmount in cents
paymentProviders.stripe.paymentIntent.currencystringCurrency code in lowercase

Price Type Description

The endpoint creates a description string based on the price type:

  • ONE_TIME: Returns "ONE_TIME"
  • RECURRING with MONTH interval:
    • Count = 1: "MONTHLY"
    • Count = 3: "QUARTERLY"
    • Count = 6: "BIANNUAL"
    • Count = 12: "ANNUAL"
  • RECURRING with YEAR interval: Returns "ANNUAL"
  • Other combinations: Returns "{INTERVAL}_{COUNT}"

This description is included in the Payment Intent metadata.

Payment Intent Metadata

The Payment Intent includes comprehensive metadata:

{
  "organizationId": "org_123",
  "productId": "prod_cmg4x57yp0018r9p7q73eghxb",
  "productName": "Injectable Semaglutide",
  "priceId": "price_asdf98asdf98as98sdf",
  "priceType": "RECURRING",
  "recurringInterval": "MONTH",
  "recurringIntervalCount": "1",
  "originalAmount": "758",
  "finalAmount": "606.40",
  "couponCode": "SAVE20"
}

Example Request

curl -X POST "https://dev.api.evoncrm.com/api/stripe/create-intents" \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "priceId": "price_asdf98asdf98as98sdf",
    "couponCode": "SAVE20"
  }'

Example Request (Without Coupon)

curl -X POST "https://dev.api.evoncrm.com/api/stripe/create-intents" \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "priceId": "price_asdf98asdf98as98sdf"
  }'

Client-Side Usage

const stripe = Stripe('your_publishable_key');
 
// Create intents
const response = await fetch('/api/stripe/create-intents', {
  method: 'POST',
  headers: {
    'X-API-Key': 'your_api_key_here',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    priceId: 'price_asdf98asdf98as98sdf',
    couponCode: 'SAVE20'
  })
});
 
const data = await response.json();
 
// Show pricing information to customer
console.log(`Original: $${data.price.originalAmount}`);
console.log(`Discount: -$${data.price.discount}`);
console.log(`Total: $${data.price.finalAmount}`);
 
// Display price type information
if (data.price.type === 'RECURRING') {
  console.log(
    `Billing: Every ${data.price.recurringIntervalCount} ${data.price.recurringInterval.toLowerCase()}(s)`
  );
} else {
  console.log('Billing: One-time payment');
}
 
// First, save the payment method
const setupResult = await stripe.confirmCardSetup(
  data.paymentProviders.stripe.setupIntent.clientSecret,
  {
    payment_method: {
      card: cardElement,
      billing_details: {
        name: 'John Doe',
        email: 'john@example.com'
      }
    }
  }
);
 
if (setupResult.error) {
  console.error('Setup failed:', setupResult.error);
  return;
}
 
// Then, process the payment
const paymentResult = await stripe.confirmCardPayment(
  data.paymentProviders.stripe.paymentIntent.clientSecret,
  {
    payment_method: setupResult.setupIntent.payment_method
  }
);
 
if (paymentResult.paymentIntent.status === 'requires_capture') {
  console.log('Payment authorized successfully');
  // Notify your backend to capture the payment
}

Coupon Validation

  • If couponCode is provided, it will be validated with Stripe
  • If the coupon is invalid or expired, a warning is logged but the request continues without applying the discount
  • The response will still include price.coupon with valid: false if validation failed
  • Only active and valid coupons will apply discounts

Discount Calculation

The endpoint supports two types of Stripe coupons:

  1. Percentage-based: Discount = (originalAmount × percentOff) / 100
  2. Amount-based: Discount = amountOff (converted from cents to dollars)

Final amount is calculated as: finalAmount = originalAmount - discount

Payment Capture

Both intents are created with manual capture:

  • Setup Intent: Saves the payment method for future use
  • Payment Intent: Authorizes but doesn't charge (requires manual capture)

To capture the authorized payment:

curl -X POST "https://api.stripe.com/v1/payment_intents/{paymentIntentId}/capture" \
  -u "your_stripe_secret_key:"

Error Responses

400 Bad Request - Validation Error

{
  "success": false,
  "error": "Invalid request data",
  "details": [
    {
      "code": "invalid_type",
      "message": "Invalid priceId format",
      "path": ["priceId"]
    }
  ]
}

404 Not Found - Price Not Found

{
  "success": false,
  "error": "Price not found"
}

400 Bad Request - Price Not Active

{
  "success": false,
  "error": "Price is not active"
}

404 Not Found - Product Not Found

{
  "success": false,
  "error": "Product not found"
}

400 Bad Request - Stripe Not Initialized

{
  "success": false,
  "error": "Failed to initialize Stripe"
}

500 Internal Server Error

{
  "success": false,
  "error": "Internal server error",
  "message": "Database connection failed"
}

Use Cases

  • Subscription Checkout: Collect payment method and first payment in one flow
  • Trial to Paid: Save payment method during trial, authorize first charge
  • Product Checkout: Process product purchase with saved payment method
  • Coupon Campaigns: Apply promotional discounts at checkout
  • Flexible Billing: Support various recurring intervals (daily, weekly, monthly, yearly)

Provider-Agnostic Architecture

The response structure uses paymentProviders object to allow multiple payment providers:

{
  "paymentProviders": {
    "stripe": {
      /* Stripe-specific data */
    },
    "paypal": {
      /* Future: PayPal data */
    },
    "square": {
      /* Future: Square data */
    }
  }
}

This design allows you to:

  • Add multiple payment providers without breaking existing integrations
  • Let customers choose their preferred payment method
  • A/B test different payment providers
  • Implement payment provider fallbacks

Security Features

All Stripe API endpoints include:

  • Encrypted Keys: Stripe keys are encrypted with AES-256 in the database
  • Audit Logging: Every key access is automatically logged with IP, user agent, and timestamp
  • API Key Authentication: Requires valid organization API key
  • Organization Isolation: Each organization uses their own Stripe account

Best Practices

  1. Key Configuration: Always configure Stripe keys in test mode first
  2. Error Handling: Implement comprehensive error handling for payment failures
  3. Webhook Integration: Set up Stripe webhooks to handle async payment events
  4. Manual Capture: Review payments before capturing for fraud prevention
  5. Metadata Usage: Store relevant order/customer IDs in metadata for reconciliation
  6. Client-Side Security: Never expose secret keys on the client side
  7. Amount Validation: Validate amounts on both client and server side
  8. Currency Consistency: Ensure currency matches customer's region
  9. Testing: Use Stripe test cards in development environment
  10. Compliance: Follow PCI compliance guidelines when handling payment data

Testing

Use Stripe test keys and test cards:

Test Cards

Card NumberScenario
4242 4242 4242 4242Successful payment
4000 0000 0000 0002Card declined
4000 0000 0000 9995Insufficient funds
4000 0025 0000 3155Requires authentication (3D Secure)

Use any future expiry date, any 3-digit CVC, and any postal code.

Notes on Price Schema

The API now uses a flexible pricing model:

  • Old schema: Used a single frequency field (ONCE, MONTHLY, QUARTERLY, BIANNUAL, ANNUAL)
  • New schema: Uses type (ONE_TIME/RECURRING) with recurringInterval and recurringIntervalCount
  • This allows for more flexible billing periods (e.g., every 2 weeks, every 3 days, etc.)
  • The careValidateBundleId field has moved from Product to ProductPrice level