Case Create Endpoint
Overview
The /api/v1/dynamic-case endpoint allows third-party applications to create cases with form submissions. This endpoint supports two different request types:
- Form Title Based Request - Creates or reuses a form with the provided title and questions
- Form ID Based Request - Uses an existing form and validates submitted answers against predefined questions
Endpoint Details
Base URLs
- Production:
https://api.care360-next.carevalidate.com/api/v1/dynamic-case - Staging:
https://api-staging.care360-next.carevalidate.com/api/v1/dynamic-case
Method: POST
Content-Type: application/json
Authentication
Required Headers
cv-api-key: your-secret-api-key
Content-Type: application/json
The cv-api-key is your organization's secret API key provided by CareValidate.
Request Body Structure
Required Core Fields
| Field | Type | Description |
|---|---|---|
firstName | string | Patient's first name |
lastName | string | Patient's last name |
email | string | Patient's email address (must be valid email format) |
questions | array | Array of question objects (minimum 1 question required) |
Optional Core Fields
| Field | Type | Description | Format/Validation |
|---|---|---|---|
dob | string | Date of birth | YYYY-MM-DD format |
gender | string | Patient's gender | "MALE" or "FEMALE" (case insensitive) |
phoneNumber | string | Patient's phone number | E.164 format (e.g., "+1234567890") |
password | string | Password for user account | Any string |
shippingAddress | object | Patient's shipping address | See Address Object Structure |
languagePreferences | array | Patient's language preferences | Array of language codes (e.g., ["en", "es", "fr"]) |
Request Type Specific Fields
Form Title Based Request (Creates or Reuses Form)
| Field | Type | Description |
|---|---|---|
formTitle | string (required) | Title for the form (creates new form or reuses existing one with same title) |
formDescription | string (optional) | Description for the form |
Form ID Based Request (Uses Existing Form)
| Field | Type | Description |
|---|---|---|
formId | string (required) | UUID of existing form to use |
Note: You must provide either formTitle OR formId, but not both. If both are provided, formTitle takes priority.
Question Object Structure
Form Title Based Request Questions
{
"question": "What is your weight?",
"type": "TEXT",
"required": true,
"answer": "150 lbs",
"phi": false,
"hint": "Enter your current weight",
"placeholder": "e.g., 150 lbs",
"options": []
}
Form ID Based Request Questions
{
"questionId": "uuid-of-existing-question",
"answer": "150 lbs"
}
Required Question Fields
| Field | Type | Description | Used In |
|---|---|---|---|
questionId | string (UUID) | UUID of existing question | Form ID Based Request only |
question | string | Question text | Form Title Based Request only |
type | string | Question type (see Supported Question Types) | Form Title Based Request only |
required | boolean | Whether answer is required | Form Title Based Request only |
Optional Question Fields
| Field | Type | Description | Notes |
|---|---|---|---|
answer | string | Answer to the question | Format depends on question type |
phi | boolean | Whether question contains PHI | Default: false |
hint | string | Help text for question | - |
placeholder | string | Placeholder text | - |
options | array | Available options for select questions | Required for SINGLESELECT/MULTISELECT types |
Address Object Structure
{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
...other_fields,
"shippingAddress": {
"addressLine1": "1600 Pennsylvania Avenue NW",
"addressLine2": "",
"city": "Washington",
"state": "DC",
"country": "US",
"postalCode": "20500"
}
}
TEXT
Simple text input
{
"type": "TEXT",
"answer": "Any text response"
}
BOOLEAN
True/false questions
{
"type": "BOOLEAN",
"answer": "true"
}
Validation: Answer must be valid JSON boolean ("true" or "false")
DATE
Single date input
{
"type": "DATE",
"answer": "2024-01-15"
}
Validation: YYYY-MM-DD format
DATERANGE
Date range input
Form Title Based Request:
{
"type": "DATERANGE",
"answer": "August 10, 2021 - indefinite"
}
Form ID Based Request:
{
"questionId": "uuid-of-question",
"answer": {"startDate":"2025-08-05","endDate":"2025-08-25"}
}
Note: endDate can be null for indefinite ranges
Validation:
- Form Title Based: Human-readable format (e.g., "August 10, 2021 - indefinite")
- Form ID Based: JSON object with
startDateandendDatein YYYY-MM-DD format endDatemust be greater thanstartDatewhen providedendDatecan be null for indefinite ranges
SINGLESELECT
Single choice from options
{
"type": "SINGLESELECT",
"options": ["Option 1", "Option 2", "Option 3"],
"answer": "Option 1"
}
Validation: Answer must be one of the provided options
MULTISELECT
Multiple choices from options
Form Title Based Request:
{
"type": "MULTISELECT",
"options": ["Weight-loss Supplements", "Dieting", "Exercise"],
"answer": "[\"Weight-loss Supplements\", \"Dieting\"]"
}
Form ID Based Request:
{
"questionId": "uuid-of-question",
"answer": ["Weight-loss Supplements", "Dieting"]
}
Validation: JSON array of strings, all must be from provided options
FILE
File upload BASE64 content
{
"type": "FILE",
"answer": [{"name":"document.pdf","data":"base64-encoded-content","contentType":"application/pdf"}]
}
File upload URL
{
"type": "FILE",
"answer": [{"name":"document.pdf","data":"https://example.com/document.pdf" }]
}
Validation: Array of file objects with required name and data properties, optional contentType (e.g., "application/pdf", "image/jpeg", "text/plain")
WIDGET_USER_ID_DOCUMENT
ID document upload
{
"type": "WIDGET_USER_ID_DOCUMENT",
"answer": [{"name":"license.jpg","data":"base64-encoded-content","contentType":"image/jpeg"}]
}
Validation: Same as FILE type
WIDGET_BMI
BMI calculator widget
Form Title Based Request:
{
"type": "WIDGET_BMI",
"answer": "{\"height\":70,\"weight\":150,\"bmi\":21.5}"
}
Form ID Based Request:
{
"questionId": "uuid-of-question",
"answer": {"height":"70","weight":150,"bmi":21.5}
}
Validation:
- Form Title Based: JSON string containing object with
height,weight, andbmi - Form ID Based: Direct object with
height(string),weight(number), andbmi(number) properties - Height is stored in inches, weight in pounds
- All values must be positive
WIDGET_STATE_PICKER
US state selection
Form Title Based Request:
{
"type": "WIDGET_STATE_PICKER",
"answer": "California"
}
Form ID Based Request:
{
"questionId": "uuid-of-question",
"answer": "CA"
}
Validation:
- Form Title Based: Accepts full state names (e.g., "California", "New York") or 2-character codes
- Form ID Based: Must be valid 2-character US state code only (e.g., "CA", "NY")
WIDGET_VISIT_TYPE
Visit type selection
{
"type": "WIDGET_VISIT_TYPE",
"answer": "SYNC_VIDEO"
}
Validation:
- Must be one of: NO_SHOW, ASYNC_TEXT_EMAIL, SYNC_VIDEO, SYNC_PHONE, ORDER_FORM
STATEMENT
Informational text (no answer required)
{
"type": "STATEMENT",
"answer": ""
}
Special Requirements
Weight Question
Important: All requests must include at least one question related to weight. The system looks for questions containing "weigh" in the question text (case insensitive) and excludes questions containing "goal" or "initiative".
Example weight questions:
- "What is your current weight?"
- "Please enter your weight in pounds"
- "Current weight"
Request Examples
Form Title Based Request Example
{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"dob": "1990-01-15",
"gender": "male",
"phoneNumber": "+1234567890",
"formTitle": "Patient Intake Form",
"formDescription": "Initial patient information collection",
"shippingAddress": {
"addressLine1": "1600 Pennsylvania Avenue NW",
"addressLine2": "",
"city": "Washington",
"state": "DC",
"country": "US",
"postalCode": "20500"
},
"questions": [
{
"question": "What is your current weight?",
"type": "TEXT",
"required": true,
"answer": "150 lbs",
"phi": false
},
{
"question": "Do you have any allergies?",
"type": "BOOLEAN",
"required": true,
"answer": "true"
},
{
"question": "Select your preferred visit type",
"type": "SINGLESELECT",
"required": false,
"options": ["Video Call", "Phone Call", "In Person"],
"answer": "Video Call"
},
{
"question": "When did your symptoms start?",
"type": "DATERANGE",
"required": false,
"answer": "January 1, 2024 - indefinite"
},
{
"question": "Select your dietary preferences",
"type": "MULTISELECT",
"required": false,
"options": ["Weight-loss Supplements", "Dieting", "Exercise", "Nutrition Counseling"],
"answer": "[\"Weight-loss Supplements\", \"Dieting\"]"
}
]
}
Form ID Based Request Example
{
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@example.com",
"dob": "1985-05-20",
"gender": "female",
"phoneNumber": "+1987654321",
"shippingAddress": {
"addressLine1": "1600 Pennsylvania Avenue NW",
"addressLine2": "",
"city": "Washington",
"state": "DC",
"country": "US",
"postalCode": "20500"
},
"formId": "550e8400-e29b-41d4-a716-446655440000",
"questions": [
{
"questionId": "550e8400-e29b-41d4-a716-446655440001",
"answer": "140 lbs"
},
{
"questionId": "550e8400-e29b-41d4-a716-446655440002",
"answer": "false"
},
{
"questionId": "550e8400-e29b-41d4-a716-446655440003",
"answer": "SYNC_VIDEO"
},
{
"questionId": "550e8400-e29b-41d4-a716-446655440004",
"answer": {"startDate":"2025-08-05","endDate":null}
},
{
"questionId": "550e8400-e29b-41d4-a716-446655440005",
"answer": ["Weight-loss Supplements", "Dieting"]
}
]
}
cURL Examples
Form Title Based Request
curl -X POST "https://api.care360-next.carevalidate.com/api/v1/dynamic-case" \
-H "Content-Type: application/json" \
-H "cv-api-key: YOUR_SECRET_KEY_HERE" \
-d '{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"dob": "1990-01-15",
"gender": "MALE",
"phoneNumber": "+1234567890",
"formTitle": "Patient Intake Form",
"formDescription": "Initial patient information collection",
"shippingAddress": {
"addressLine1": "1600 Pennsylvania Avenue NW",
"addressLine2": "",
"city": "Washington",
"state": "DC",
"country": "US",
"postalCode": "20500"
},
"questions": [
{
"question": "What is your current weight?",
"type": "TEXT",
"required": true,
"answer": "150 lbs",
"phi": false
},
{
"question": "Do you have any allergies?",
"type": "BOOLEAN",
"required": true,
"answer": "true"
},
{
"question": "Select your preferred visit type",
"type": "SINGLESELECT",
"required": false,
"options": ["Video Call", "Phone Call", "In Person"],
"answer": "Video Call"
}
]
}'
Form ID Based Request
curl -X POST "https://api.care360-next.carevalidate.com/api/v1/dynamic-case" \
-H "Content-Type: application/json" \
-H "cv-api-key: YOUR_SECRET_KEY_HERE" \
-d '{
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@example.com",
"dob": "1985-05-20",
"gender": "FEMALE",
"phoneNumber": "+1987654321",
"shippingAddress": {
"addressLine1": "1600 Pennsylvania Avenue NW",
"addressLine2": "",
"city": "Washington",
"state": "DC",
"country": "US",
"postalCode": "20500"
},
"formId": "550e8400-e29b-41d4-a716-446655440000",
"questions": [
{
"questionId": "550e8400-e29b-41d4-a716-446655440001",
"answer": "140 lbs"
},
{
"questionId": "550e8400-e29b-41d4-a716-446655440002",
"answer": "false"
},
{
"questionId": "550e8400-e29b-41d4-a716-446655440003",
"answer": "SYNC_VIDEO"
}
]
}'
Response Format
Success Response
{
"success": true,
"data": {
"caseId": "550e8400-e29b-41d4-a716-446655440000",
"formResponseId": "550e8400-e29b-41d4-a716-446655440001"
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Indicates if the request was successful |
data.caseId | string (UUID) | Unique identifier for the created case |
data.formResponseId | string (UUID) | Unique identifier for the form response |
Error Responses
Authentication Errors
{
"status": 401,
"error": "Invalid request"
}
Validation Errors
{
"success": false,
"error": "Validation failed: email: Invalid email address"
}
{
"success": false,
"error": "Question 'What is your age?' with questionId: 550e8400-e29b-41d4-a716-446655440000: Answer is required"
}
8. Missing Required Answer
Error: Answer is required for question: 'Question Text' with questionId: uuid
Solution: Provide an answer for all required questions
Payment and Shipping Information
Payment Options
The case creation endpoint supports two payment methods:
- Setup Intent (stripeSetupId): For saving payment methods for future use
- Payment Intent (stripePaymentId): For immediate payment processing
Github sample code showing how to use this section of the knowledge base with WordPress, its Elementor forms extension, and the WPGetAPI extension may be found in this repo. This may also be useful for Shopify or other integrations.
In order to use CareValidate's Stripe account for payment, it is suggested to use Stripe Elements with our publishable key. It also has versions available for major frameworks, such as React. The example shows how to get the shippingAddress and stripeSetupId. The email field shown below should match what is passed to the main endpoint above.
const stripe = await loadStripe(
"pk_live_51HqSIiKAXrtjbq2dtXcGLkFqhqPquraau6jRB8nDCrDVIGj7me2ZEAiQxZNwuG9A7Y1Gzn6vg8xslQuCpoTByMKd00cmPemstt"
);
//see the below curl example for payment secret
const elements = stripe.elements({ clientSecret: paymentSecret });
//capture shipping information with the same settings used by CareValidate
let shippingAddress;
const addressElement = elements.create('address',
{ mode: 'shipping', allowedCountries: ['US'] });
addressElement.on('change', e => {
const addr = e.value.address;
if (e.complete && addr) {
shippingAddress = {
addressLine1: addr.line1,
addressLine2: addr.line2,
city: addr.city,
state: addr.state,
country: addr.country,
postalCode: addr.postal_code
}
}
});
//skipping mounting and styling the elements
const pay = elements.create('payment');
//validate payment data after entry with the same settings used by CareValidate
const result = await stripe.confirmSetup({
elements: elements!,
redirect: 'if_required',
confirmParams: {
payment_method_data: { billing_details: { email } }
}
})
const stripeSetupId = result.setupIntent.id
To obtain the payment secret for Stripe Elements, you may call our API with your CareValidate API key and the US Dollar amount of the transaction. The amount passed here should match what you pass for paymentAmount to the main endpoint. The example here sets up a transaction for 50 cents.
curl --location --request POST 'https://api.care360-next.carevalidate.com/api/v1/payments/setup' \
--header 'cv-api-key: <redacted>'
Payment Intent Workflow
For immediate payment processing using stripePaymentId, follow this workflow:
- Apply Promo Codes: If promo codes are provided, validate them using
/api/v1/promo-codesand apply the discount to calculate the final amount - Create Payment Intent: Call
/api/v1/payments/intentwith the discounted amount - Process Payment: Use the returned
paymentIntentSecretin your frontend to collect payment - Create Case: Pass the payment intent ID as
stripePaymentIdwhen creating the case
Important: When using payment intents, promo codes must be applied to the amount before calling the /api/v1/payments/intent endpoint. The payment intent should be created with the final discounted amount.
Example case creation with stripePaymentId:
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"paymentDescription": "Description of what the patient purchased",
"paymentAmount": 0.5,
"stripePaymentId": "pi_3SG0o8GkkQS2eXzh0BKpSaq0",
"productBundleId": "<your-product-bundle-id>",
"questions": [...]
}
Example payment intent creation with promo code applied:
# Step 1: Validate promo code (if provided)
curl -X GET "https://api.care360-next.carevalidate.com/api/v1/promo-codes?code=SAVE10" \
-H "cv-api-key: YOUR_SECRET_KEY_HERE"
# Step 2: Apply discount to amount (e.g., 10% off $50 = $45)
# Step 3: Create payment intent with discounted amount
curl -X POST "https://api.care360-next.carevalidate.com/api/v1/payments/intent" \
-H "cv-api-key: YOUR_SECRET_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"amount": 4500,
"paymentMethodTypes": ["card", "klarna"]
}'
Response:
{
"status": 200,
"success": true,
"message": "payment Intent initiated successfully",
"data": {
"paymentIntentSecret": "pi_3SG0o8GkkQS2eXzh0BKpSaq0_secret_CBrwqmT726jzfcnvCt0qBcS9e"
}
}
Then use the paymentIntentSecret in your frontend to process the payment. After successful payment confirmation, use the payment intent ID (not the secret) as stripePaymentId in the case creation.
Frontend Integration with Payment Intent
Here's how to use the payment intent secret in your frontend with Stripe Elements:
const stripe = await loadStripe(
"pk_live_51HqSIiKAXrtjbq2dtXcGLkFqhqPquraau6jRB8nDCrDVIGj7me2ZEAiQxZNwuG9A7Y1Gzn6vg8xslQuCpoTByMKd00cmPemstt"
);
// Use the paymentIntentSecret from the /api/v1/payments/intent response
const elements = stripe.elements({ clientSecret: paymentIntentSecret });
// Create payment element
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// Handle form submission
const handleSubmit = async (event) => {
event.preventDefault();
// Save form state to session storage for redirect payment methods
const formData = {
firstName: document.getElementById('firstName').value,
lastName: document.getElementById('lastName').value,
email: document.getElementById('email').value,
promoCode: document.getElementById('promoCode').value,
// ... save other form fields
};
sessionStorage.setItem('caseFormData', JSON.stringify(formData));
// Apply promo code discount if provided
let finalAmount = 5000; // Original amount in cents
if (formData.promoCode) {
try {
const promoResponse = await fetch(`/api/v1/promo-codes?code=${formData.promoCode}`, {
headers: { 'cv-api-key': 'your-api-key' }
});
const promoData = await promoResponse.json();
if (promoData.success && promoData.data.length > 0) {
const discount = promoData.data[0];
if (discount.percentDiscount) {
finalAmount = Math.round(finalAmount * (1 - discount.percentDiscount / 100));
} else if (discount.flatDiscount) {
finalAmount = Math.max(0, finalAmount - (discount.flatDiscount * 100));
}
}
} catch (error) {
console.error('Promo code validation failed:', error);
}
}
// Create payment intent with discounted amount
const paymentIntentResponse = await fetch('/api/v1/payments/intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'cv-api-key': 'your-api-key'
},
body: JSON.stringify({
amount: finalAmount,
paymentMethodTypes: ['card', 'klarna']
})
});
const { data } = await paymentIntentResponse.json();
const paymentIntentSecret = data.paymentIntentSecret;
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: 'https://your-website.com/payment-return',
},
});
if (error) {
// Show error to customer
console.error(error);
} else {
// Payment succeeded, extract payment intent ID and create case
const paymentIntentId = paymentIntentSecret.split('_secret_')[0];
// Use paymentIntentId (not the secret) as stripePaymentId in case creation
console.log('Payment Intent ID for case creation:', paymentIntentId);
}
};
The key difference from setup intent is using stripe.confirmPayment() instead of stripe.confirmSetup(). The payment intent ID is extracted from the secret by splitting on _secret_ and should be used as stripePaymentId in the case creation endpoint (not the secret itself).
Return URL and Redirect Payment Methods
The return_url parameter is required for redirect payment methods like Klarna, Affirm, and other buy-now-pay-later options. Here's what you need to know:
Return URL Requirements:
- Must be a page/view in your frontend application
- Will receive the payment intent ID as a query parameter when the user completes payment
- Should handle both successful and failed payment scenarios
Session Storage Requirement: Because redirect payment methods take the user away from your form to complete payment, you must save the current form state to session storage before initiating payment. This ensures you can restore the form data when the user returns.
Deprecated
curl -m 70 -X POST https://us-central1-care360-next.cloudfunctions.net/initiatePayment \
-H "Content-Type: application/json" \
-d '{
"key": "<redacted>"
}'
The response JSON will be of the following form, although the ... parts will be filled in with alphanumeric strings.
{ "success":true, "paymentSecret": "seti_..._secret_..." }
Alternative Payment Gateway NMI
We offer the ability to use NMI as a gateway with either our payment processor or your own. To do this you would pass nmiPaymentToken in place of the stripeSetupId. The payment token may be obtained by following the directions of the NMI Payment Gateway Integration portal for the collect.js library.
Collect.js Public Keys
Replace the following placeholders with your own NMI Collect.js public keys from the NMI portal:
- Staging: 9742U4-z2K5kp-a2s5uN-37z9W2
- Production: qxFPr8-fs8V54-UmRqKu-x4dH8p
Example (refer to NMI's documentation for exact usage in your stack):
<!-- NMI Collect.js include -->
<script src="https://secure.networkmerchants.com/token/Collect.js"></script>
<!-- Use your public key -->
<script>
const nmiPublicKey = '9742U4-z2K5kp-a2s5uN-37z9W2';
// Initialize and use Collect.js with nmiPublicKey per NMI docs
</script>
Troubleshooting
CORS
If a Cross-Origin Resource Sharing (CORS) error message is received, especially one like the following
Access to fetch at 'https://us-central1-care360-next.cloudfunctions.net/initiatePayment' from origin 'https://example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
then it likely means the request has been sent from the patient's web browser to one of our endpoints. However, sending a request to an endpoint from a browser inherently means that the API key has been exposed to the patient, meaning anyone could create cases as if they were the storefront owner. For this reason, there must be a server to relay the request and inject the key to keep it safe.