Skip to main content

Case Create Endpoint

POST/api/v1/dynamic-case

Creates a new case with a form submission. Supports two modes: Form Title Based (creates or reuses a form by title) and Form ID Based (submits answers to an existing form by ID).

cv-api-key
Productionhttps://api.care360-next.carevalidate.com/api/v1/dynamic-case
Staginghttps://api-staging.care360-next.carevalidate.com/api/v1/dynamic-case

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
firstNamestringrequired

Patient's first name

lastNamestringrequired

Patient's last name

emailstringrequired

Patient's email address (must be valid email format)

questionsarrayrequired

Array of question objects (minimum 1 question required)

Optional Core Fields
dobstringoptional

Date of birth

Example: 1990-01-15
genderstringoptional

Patient's gender

Values:MALEFEMALE
phoneNumberstringoptional

Patient's phone number in E.164 format

Example: +1234567890
passwordstringoptional

Password for user account

shippingAddressobjectoptional

Patient's shipping address

Show 6 child properties
addressLine1stringrequired

Street address line 1

addressLine2stringoptional

Street address line 2

citystringrequired

City

statestringrequired

State code

countrystringrequired

Country code

postalCodestringrequired

Postal code

languagePreferencesarrayoptional

Patient's language preferences. Array of language codes.

Example: ["en", "es", "fr"]
statusstringoptional

Initial status for the case. Any other status will result in a 400 validation error. Case insensitive, default: OPEN.

Values:OPENABANDONED
idempotencyKeystringoptional

Optional unique key to prevent duplicate case creation. Scoped per organization.

Request Type Specific Fields

Form Title Based Request (Creates or Reuses Form)
formTitlestringrequired

Title for the form (creates new form or reuses existing one with same title)

formDescriptionstringoptional

Description for the form

Form ID Based Request (Uses Existing Form)
formIdstringrequired

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
questionIdstring (UUID)required

UUID of existing question (Form ID Based Request only)

questionstringrequired

Question text (Form Title Based Request only)

typestringrequired

Question type — see Supported Question Types below (Form Title Based Request only)

requiredbooleanrequired

Whether answer is required (Form Title Based Request only)

Optional Question Fields
answerstringoptional

Answer to the question. Format depends on question type.

phibooleanoptional

Whether question contains PHI. Default: false.

hintstringoptional

Help text for question

placeholderstringoptional

Placeholder text

optionsarrayoptional

Available options for select questions. Required for SINGLESELECT/MULTISELECT types.

Address Object Structure

{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"shippingAddress": {
"addressLine1": "1600 Pennsylvania Avenue NW",
"addressLine2": "",
"city": "Washington",
"state": "DC",
"country": "US",
"postalCode": "20500"
}
}

Supported Question Types

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"

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

Request Examples

Form Title Based Request Example

{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"dob": "1990-01-15",
"gender": "male",
"phoneNumber": "+1234567890",
"status": "OPEN",
"idempotencyKey": "unique-case-key-123",
"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",
"idempotencyKey": "unique-case-key-456",
"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

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",
"idempotencyKey": "unique-case-key-123",
"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"
}
]
}'

Responses

200SuccessCase created successfully.
{
"success": true,
"data": {
"caseId": "550e8400-e29b-41d4-a716-446655440000",
"formResponseId": "550e8400-e29b-41d4-a716-446655440001"
}
}
401Authentication ErrorReturned when the API key is invalid or missing.
{
"status": 401,
"error": "Invalid request"
}
400Validation ErrorReturned when the request body fails validation.
{
"success": false,
"error": "Validation failed: email: Invalid email address"
}
400Missing Required AnswerReturned when a required question is missing an answer.
{
"success": false,
"error": "Question 'What is your age?' with questionId: 550e8400-e29b-41d4-a716-446655440000: Answer is required"
}
409ConflictA record with the provided idempotencyKey already exists for this organization.
{
"status": 409,
"success": false,
"code": "IDEMPOTENCY_ERROR",
"description": "A record with the provided idempotencyKey already exists for this organization."
}

Try It Out

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

    • When creating the case using /dynamic-case API endpoint, pass the original (non-discounted) paymentAmount along with the stripeSetupId.
  2. Payment Intent (stripePaymentId): For immediate payment processing

    • Example: The original amount is $600. A promo code (FLAT100) provides a flat $100 discount, reducing the payable amount to $500. When calling the /payment/intent API, use paymentAmount as 500. While creating the case using /dynamic-case API endpoint, pass the original (non-discounted) paymentAmount as 600 along with the stripePaymentId.

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 and non-discounted paymentAmount 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"],
"metadata": {
"email": "testemail@example.com",
"phone": "+1404567890"
}
}'

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.