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:
- Configure your Stripe keys in Settings → Payments
- 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
| Field | Type | Description |
|---|---|---|
success | boolean | Indicates if the request was successful |
paymentSecret | string | Client 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
amount | number | Yes | - | Amount in dollars (will be converted to cents) |
currency | string | No | "usd" | Three-letter ISO currency code |
description | string | No | - | Description of the payment |
metadata | object | No | - | Key-value pairs for additional information |
payment_method_types | array | No | ["card", "affirm", "klarna"] | Accepted payment methods |
Supported Payment Methods
card- Credit/debit cardsaffirm- Affirm (BNPL) - Minimum amount: $35.00 USDklarna- Klarna (BNPL)afterpay_clearpay- Afterpay/Clearpay (BNPL)cashapp- Cash App Paylink- 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 Dollareur- Eurogbp- British Poundcad- Canadian Dollaraud- Australian Dollarjpy- 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
| Field | Type | Description |
|---|---|---|
success | boolean | Indicates if the request was successful |
paymentSecret | string | Client secret for confirming the payment on the client side |
paymentIntentId | string | Unique ID of the payment intent |
amount | number | Amount 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:
- Authorization: Payment is authorized but not charged
- 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
| Field | Type | Required | Description |
|---|---|---|---|
priceId | string | Yes | The ID of the product price from your database |
couponCode | string | No | Stripe 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
| Field | Type | Description |
|---|---|---|
success | boolean | Indicates if the request was successful |
price.priceId | string | ID of the product price |
price.productId | string | ID of the product |
price.productName | string | Name of the product |
price.productType | string | Type of product (SEMAGLUTIDE, TIRZEPATIDE) |
price.productDescription | string | null | Product description |
price.originalAmount | number | Original price in dollars |
price.discount | number | Discount amount in dollars (0 if no coupon) |
price.finalAmount | number | Final price after discount in dollars |
price.currency | string | Currency code (uppercase) |
price.type | string | Price type: ONE_TIME or RECURRING |
price.recurringInterval | string | null | For recurring: DAY, WEEK, MONTH, or YEAR |
price.recurringIntervalCount | number | null | Number of intervals between charges |
price.label | string | null | Optional price label/description |
price.coupon | object | Coupon details (only present if couponCode was provided and valid) |
price.coupon.code | string | The coupon code used |
price.coupon.percentOff | number | null | Percentage discount (if percentage-based) |
price.coupon.amountOff | number | null | Fixed amount discount in cents (if amount-based) |
price.coupon.valid | boolean | Whether the coupon was successfully applied |
paymentProviders.stripe.setupIntent | object | Setup Intent details for saving payment method |
paymentProviders.stripe.setupIntent.clientSecret | string | Client secret for confirming setup intent |
paymentProviders.stripe.setupIntent.id | string | Setup Intent ID |
paymentProviders.stripe.paymentIntent | object | Payment Intent details for processing payment |
paymentProviders.stripe.paymentIntent.clientSecret | string | Client secret for confirming payment intent |
paymentProviders.stripe.paymentIntent.id | string | Payment Intent ID |
paymentProviders.stripe.paymentIntent.amount | number | Amount in cents |
paymentProviders.stripe.paymentIntent.currency | string | Currency 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"
- Count = 1:
- 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
couponCodeis 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.couponwithvalid: falseif validation failed - Only active and valid coupons will apply discounts
Discount Calculation
The endpoint supports two types of Stripe coupons:
- Percentage-based: Discount = (originalAmount × percentOff) / 100
- 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
- Key Configuration: Always configure Stripe keys in test mode first
- Error Handling: Implement comprehensive error handling for payment failures
- Webhook Integration: Set up Stripe webhooks to handle async payment events
- Manual Capture: Review payments before capturing for fraud prevention
- Metadata Usage: Store relevant order/customer IDs in metadata for reconciliation
- Client-Side Security: Never expose secret keys on the client side
- Amount Validation: Validate amounts on both client and server side
- Currency Consistency: Ensure currency matches customer's region
- Testing: Use Stripe test cards in development environment
- Compliance: Follow PCI compliance guidelines when handling payment data
Testing
Use Stripe test keys and test cards:
Test Cards
| Card Number | Scenario |
|---|---|
| 4242 4242 4242 4242 | Successful payment |
| 4000 0000 0000 0002 | Card declined |
| 4000 0000 0000 9995 | Insufficient funds |
| 4000 0025 0000 3155 | Requires 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
frequencyfield (ONCE, MONTHLY, QUARTERLY, BIANNUAL, ANNUAL) - New schema: Uses
type(ONE_TIME/RECURRING) withrecurringIntervalandrecurringIntervalCount - This allows for more flexible billing periods (e.g., every 2 weeks, every 3 days, etc.)
- The
careValidateBundleIdfield has moved from Product to ProductPrice level
Related Documentation
- Stripe API Documentation
- Stripe.js Reference
- Payment Intents Guide
- Setup Intents Guide
- BNPL Methods
- Stripe Coupons
- Products API - Product and pricing management
- Sessions API - Session-based checkout flow