Skip to main content

Add Products to Case

POST/api/v1/cases/:caseId/products

Attaches one or more products to an existing case. Supports an optional top-level payment that covers all products in the request. Each product can also include an optional intake form and subscription configuration.

cv-api-key
Productionhttps://api.care360-next.carevalidate.com/api/v1/cases/:caseId/products
Staginghttps://api-staging.care360-next.carevalidate.com/api/v1/cases/:caseId/products

Parameters​

Headers
cv-api-keystringrequired
Your unique API key for authentication.
Content-Typestringrequired
Must be application/json.
Example: application/json
Path Parameters
caseIdstring (UUID)required
Unique identifier of the case to add products to
Request Body
paymentobjectoptional
Optional payment covering all products in this request. A single payment can cover one or more products. For BNPL redirect flows (Klarna, Afterpay), you can pass a pending `PAYMENT_INTENT` here — products will be attached as inactive (`CLOSE`) and automatically activated once the PaymentIntent resolves (see Payment Status Lifecycle below). `amount` is required whenever a pending or already-charged `PAYMENT_INTENT` is provided, since it cannot be inferred from the PaymentIntent in that state.
Show 5 child properties
amountnumberrequired
Payment amount
currencystringrequired
ISO 4217 currency code
Example: USD
descriptionstringoptional
Payment description
providerReferenceobjectoptional
Payment gateway reference
Show 2 child properties
typestringrequired
Payment reference type
Values:SETUP_INTENTPAYMENT_INTENT
idstringrequired
Gateway reference ID (e.g. Stripe PaymentIntent or SetupIntent ID)
applyPromoCodebooleanoptional
Send `amount` as the already-discounted amount you built your PaymentIntent for — the same amount you'd send without this flag. When `providerReference.type` is `PAYMENT_INTENT`, `applyPromoCode: true` uses the case's already-attached promo code to record the implied discount for your records (`discountedAmount`/promo code are stored against the payment); `amount` itself is never changed. Cannot be combined with `promoCode`. Currently has no effect for `SETUP_INTENT` or when no `providerReference` is given. Defaults to false.
productsarrayrequired
Products to attach to the case. Each case can include one or more products. When a top-level `payment` is provided, it is linked to all products in this array.
Show 4 child properties
idstring (UUID)required
Organization product ID
visitTypestringoptional
Visit type for this product
Values:ASYNC_TEXT_EMAILSYNC_IN_PERSONSYNC_PHONESYNC_VIDEO
formobjectoptional
Healthcare intake form (see Form section below)
Show 3 child properties
titlestringoptional
Form title
descriptionstringoptional
Form description
questionsarrayoptional
Array of form questions
subscriptionobjectoptional
Optional subscription for this case product. Values are stored on the case product and returned in GET /api/v1/cases and in GraphQL.
Show 2 child properties
intervalstringoptional
Subscription interval (case-sensitive, lowercase)
Values:dayweekmonthyear
intervalCountintegeroptional
Positive integer for interval count (e.g. 1, 2, 3, 6, 12)

Form Question Fields​

Question Object
questionstringrequired
Question text
answerstringrequired
User response
typestringrequired
Question type
Values:TEXTSINGLESELECTMULTISELECT
requiredbooleanoptional
Whether the question is mandatory
phibooleanoptional
PHI indicator. Questions marked with `true` are treated as Protected Health Information and stored per HIPAA compliance requirements.
optionsarrayoptional
Selectable options for SINGLESELECT and MULTISELECT question types

Payment Status Lifecycle​

When a PAYMENT_INTENT is still pending at attach time (the BNPL pre-redirect flow), the product and its payment move through the following states as Stripe webhook events arrive. No polling is required — the case product's status/reason and the linked payment's status update automatically.

EventProduct statusProduct reasonPayment status
Pending PAYMENT_INTENT attachedCLOSEPending paymentUNPAID
payment_intent.amount_capturable_updated (BNPL hold acquired)OPENnullUNPAID (capture happens later)
payment_intent.succeeded (charge captured)OPENnullPAID
payment_intent.payment_failedCLOSEPayment failedERROR

If a PaymentIntent fails, only the product(s) and payment tied to it are closed/errored — the case itself remains OPEN. To retry, re-add the same product with a new PAYMENT_INTENT; it starts a fresh pending-payment cycle independent of the failed one.

Promo Code Discount (PAYMENT_INTENT)​

Always send amount as the amount you actually built your PaymentIntent for — a Stripe PaymentIntent's amount is fixed when you create it and can't be reduced through this endpoint. applyPromoCode: true never changes amount. When providerReference.type is PAYMENT_INTENT, it instead uses the case's already-attached promo code to work out what discount your amount implies, and records that against the payment for your own reporting (visible via the case's payment history as discountedAmount and the linked promo code) — regardless of whether that PaymentIntent is a BNPL hold, already succeeded, or still pending.

For example, a case has a 50%-off promo code and you've already applied it yourself, charging $100 instead of the $200 list price: create your PaymentIntent for $100, then call this endpoint with amount: 100 and applyPromoCode: true. The stored payment's amount stays 100; discountedAmount is recorded as 100 (the discount your $100 charge implies against the $200 list price).

It cannot be combined with the promoCode field. It's a no-op for SETUP_INTENT or when no providerReference is given — the case's promo code is never looked up there.

A 100%-off (or otherwise ≥100%) percent-discount promo code causes this endpoint to reject the request with a 400 — there's no gross amount a 100%-off discount can be worked back out from a net amount of more than $0. If this happens, cancel the PaymentIntent: the authorization otherwise stays open on the customer's funding source until it expires on its own.

A BNPL hold's case promo code is still applied automatically at capture if you don't use this flag. If you don't pass applyPromoCode, a BNPL hold attached to a case with a promo code gets that discount applied once, automatically, when the hold is captured — expect the captured amount to be lower than the amount you originally sent. Use applyPromoCode: true instead when you've already applied the discount yourself and want that reflected immediately, rather than only after capture.

Examples​

curl -X POST "https://api.care360-next.carevalidate.com/api/v1/cases/550e8400-e29b-41d4-a716-446655440000/products" \
-H "cv-api-key: <redacted>" \
-H "Content-Type: application/json" \
-d '{
"products": [
{
"id": "8dff2c47-b2ab-42d8-9af0-4c4dbca8b4b6",
"visitType": "SYNC_VIDEO",
"subscription": {
"interval": "month",
"intervalCount": 3
}
},
{
"id": "621527f5-3877-48b3-b27f-df395a5308bb",
"form": {
"title": "Healthcare Intake Form",
"description": "Patient intake form for healthcare services",
"questions": [
{
"question": "How much do you weigh?",
"answer": "138 lbs",
"phi": true,
"type": "TEXT"
},
{
"question": "What are your weight loss goals?",
"answer": "Lose 1-20lbs for good",
"type": "SINGLESELECT",
"required": true,
"options": [
"Lose 1-20lbs for good",
"Lose 21-50lbs for good",
"Lose over 50 for good",
"Maintain my healthy weight"
]
}
]
}
}
]
}'

Responses​

▶200SuccessProducts successfully attached to the case.
{
"success": true,
"data": {
"caseId": "550e8400-e29b-41d4-a716-446655440000"
}
}
▶400Bad RequestInvalid request body or parameters. Also returned when a pending or already-charged `PAYMENT_INTENT` is provided without an `amount`.
{
"success": false,
"message": "Invalid request"
}
▶401UnauthorizedMissing or invalid authentication token.
{
"success": false,
"message": "Unauthorized"
}
▶409ConflictCase conflict (e.g., duplicate product attachment).
{
"success": false,
"message": "Case conflict"
}
▶422Validation FailedRequest body failed validation rules.
{
"success": false,
"message": "Validation failed"
}
▶500Internal Server ErrorUnexpected server error.
{
"success": false,
"message": "Internal server error"
}

Try It Out​


API Version: v2.2