Skip to main content

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:

  1. Form Title Based Request - Creates or reuses a form with the provided title and questions
  2. 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

FieldTypeDescription
firstNamestringPatient's first name
lastNamestringPatient's last name
emailstringPatient's email address (must be valid email format)
questionsarrayArray of question objects (minimum 1 question required)

Optional Core Fields

FieldTypeDescriptionFormat/Validation
dobstringDate of birthYYYY-MM-DD format
genderstringPatient's gender"MALE" or "FEMALE" (case insensitive)
phoneNumberstringPatient's phone numberE.164 format (e.g., "+1234567890")
passwordstringPassword for user accountAny string
shippingAddressobjectPatient's shipping addressSee Address Object Structure
languagePreferencesarrayPatient's language preferencesArray of language codes (e.g., ["en", "es", "fr"])

Request Type Specific Fields

Form Title Based Request (Creates or Reuses Form)

FieldTypeDescription
formTitlestring (required)Title for the form (creates new form or reuses existing one with same title)
formDescriptionstring (optional)Description for the form

Form ID Based Request (Uses Existing Form)

FieldTypeDescription
formIdstring (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

FieldTypeDescriptionUsed In
questionIdstring (UUID)UUID of existing questionForm ID Based Request only
questionstringQuestion textForm Title Based Request only
typestringQuestion type (see Supported Question Types)Form Title Based Request only
requiredbooleanWhether answer is requiredForm Title Based Request only

Optional Question Fields

FieldTypeDescriptionNotes
answerstringAnswer to the questionFormat depends on question type
phibooleanWhether question contains PHIDefault: false
hintstringHelp text for question-
placeholderstringPlaceholder text-
optionsarrayAvailable options for select questionsRequired 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 startDate and endDate in YYYY-MM-DD format
  • endDate must be greater than startDate when provided
  • endDate can 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, and bmi
  • Form ID Based: Direct object with height (string), weight (number), and bmi (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

FieldTypeDescription
successbooleanIndicates if the request was successful
data.caseIdstring (UUID)Unique identifier for the created case
data.formResponseIdstring (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:

  1. Setup Intent (stripeSetupId): For saving payment methods for future use
  2. 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:

  1. Apply Promo Codes: If promo codes are provided, validate them using /api/v1/promo-codes and apply the discount to calculate the final amount
  2. Create Payment Intent: Call /api/v1/payments/intent with the discounted amount
  3. Process Payment: Use the returned paymentIntentSecret in your frontend to collect payment
  4. Create Case: Pass the payment intent ID as stripePaymentId when 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.