Case Data API v2
Major changes from v1
Introduces straightforward, JSON-based case creations and modifications, individually and in bulk. Previously this was only possible by constructing XForms, a complex XML document structure.
Allows for filtering and querying cases by project-specific case properties, not just metadata.
Introduces a bulk get-by-id and get-by-external-id endpoint.
Clearer serialization format that’s more in-line with commonly used terminology elsewhere on HQ.
Performant deep pagination at scale. This makes it more suitable for fetching very large data sets, such as for populating an analytics database.
Supported Endpoints and Methods
All endpoints are available under
www.commcarehq.org/a/<domain>/api/case/v2/
Endpoint |
Description |
|---|---|
GET / |
Query list of cases |
GET /<case_id> |
Get individual case |
GET /ext/<ext_id>/ |
Get individual case by external ID |
GET /<case_id>,<case_id>… |
Get multiple cases by ID |
POST /bulk-fetch/ |
Get cases in bulk by ID or external ID |
POST / |
Create new case |
POST / |
Create or update cases in bulk |
PUT /<case_id> |
Update existing case |
PUT /ext/<ext_id>/ |
Upsert case by external ID |
PUT / |
Upsert case by external ID |
Single Case Serialization Format
Below is the return format for an individual case. It is used by each of the supported endpoints.
{
"domain": "queens-gambit",
"case_id": "79e25a30-8db5-4a5a-827b-0297b254e87f",
"case_type": "patient",
"case_name": "Elizabeth Harmon",
"external_id": "1",
"owner_id": "20cc9dda-b90a-4af3-aa3d-fc67184e73ef",
"date_opened": "2020-03-19T14:31:34.133000Z",
"last_modified": "2020-03-19T14:31:34.133000Z",
"server_last_modified": "2020-03-19T14:31:34.133000Z",
"indexed_on": "2020-03-19T14:31:34.133000Z",
"closed": false,
"date_closed": null,
"properties": {
"dob": "1948-11-02"
},
"indices": {
"parent": {
"case_id": "eb1a8c23-6d6d-4b61-894c-ae1c437d8dac",
"case_type": "household",
"relationship": "child"
}
}
}
Included fields
Field Name |
Description |
|---|---|
domain |
|
case_id |
|
case_type |
|
case_name |
|
external_id |
|
owner_id |
|
date_opened |
ISO 8601 UTC datetime |
last_modified |
ISO 8601 UTC datetime |
server_last_modified |
ISO 8601 UTC datetime |
indexed_on |
ISO 8601 UTC datetime This represents the time the case was most recently indexed for use in the API. This field is used in paginating the API results. It is subject to change without notice from regular maintenance operations. When returned from in the response to a create or update request, it represents the time the response was processed, as it hasn’t yet been indexed. |
closed |
Boolean true or false. Always present |
date_closed |
ISO 8601 UTC datetime. Field is always present, but null for open cases. |
properties |
Contains all user-defined properties, represented as strings. |
indices |
Dict containing a series of indices (not included by default) |
indices.<name> |
User-provided name for an index, typically “parent” or “host”, but not constrained |
indices.<name>.case_id |
|
indices.<name>.case_type |
|
indices.<name>.relationship |
Either “child” or “extension” |
Case Create/Update/Upsert Format
Below is the format expected by the PUT and POST endpoints when creating or updating a case.
{
"case_id": "5160d95d-efdc-4fbb-ba11-5f5bccdde950",
"case_type": "patient",
"case_name": "Elizabeth Harmon",
"owner_id": "20cc9dda-b90a-4af3-aa3d-fc67184e73ef",
"temporary_id": "1",
"external_id": "1",
"properties": {
"dob": "1948-11-02"
},
"indices": {
"parent": {
"case_id": "eb1a8c23-6d6d-4b61-894c-ae1c437d8dac",
"case_type": "household",
"relationship": "child"
}
}
}
Included Fields
Note: All values other than close must be string types, including property values.
Field Name |
Description |
|---|---|
case_id |
Only allowed in bulk updates. Will be server generated for case creations, and passed in as part of the resource URI for individual updates. |
case_type |
Required for new cases, optional for updates. Max length 255 characters. |
case_name |
Required for new cases, optional for updates. Max length 255 characters. |
owner_id |
UUID of the case owner. Not validated server-side. Required for new cases, optional for updates. Max length 255 characters. |
temporary_id |
Bulk create/update only. Must be unique per request. Other cases may reference this ID in index definitions. Not saved. |
external_id |
Max length 255 characters |
properties |
Enumeration of property_name / value pairs. All user-defined. All values must be strings, and all property names must be valid XML element names, meaning:
|
indices |
Dict containing a series of indices |
indices.<name> |
User-provided name for an index, typically “parent” or “host”, but not constrained. Like property names, this must be a valid XML element name. |
indices.<name>.case_id |
|
indices.<name>.temporary_id |
Bulk create/update only. Can be used in lieu of providing a case_id in instances where the referenced case is also created in the same request. |
indices.<name>.case_type |
|
indices.<name>.relationship |
Must be either “child” or “extension”. See the Extension Cas es feature flag. |
close |
Boolean True or False, defaults to False |
API Usage
Get List of Cases
Interface - GET /a/<domain>/api/case/v2/
This endpoint returns a list of cases, which can be filtered as described below.
Filter Param |
Description |
|---|---|
limit |
Defaults to 20, maximum 5000 |
external_id |
|
case_type |
|
owner_id |
|
case_name |
|
closed |
Boolean true or false |
Indices.parent
indices.host
indices.<name> |
id of a parent or host case (or other type). Will return children/extensions of that case |
last_modified.gt / last_modified.gte / last_modified.lt / last_modified.lte |
Accepts ISO 8601 date or datetime values |
server_last_modified.gt (and gte, lt, lte) |
Accepts ISO 8601 date or datetime values |
indexed_on.gt (and gte, lt, lte) |
Accepts ISO 8601 date or datetime values |
date_opened.gt (gte, lte, lt) |
Accepts ISO 8601 date or datetime values |
date_closed.gt (gte, lte, lt) |
Accepts ISO 8601 date or datetime values |
properties.<property> (eg: “properties.state”) |
User-defined case properties. This supports only exact matches like “properties.state=bihar” or “properties.is_testing=false”. Empty values and missing values are treated the same, so “properties.state=” will match cases where state isn’t set, and those where it’s set to the empty string. |
Return value is a list of cases, each serialized as described in “Single Case Serialization Format”.
Limiting Response Fields
Two optional, mutually exclusive query parameters allow you to control which fields appear in the response:
fieldsA comma-separated list of field names to include. Only specified fields will appear in the response. Use dot notation for nested fields.
excludeA comma-separated list of field names to remove from the response. Use dot notation for nested fields.
Using both fields and exclude in the same request will return an
error.
Dot-param syntax for grouping sub-fields under a parent:
?fields=case_id,external_id&fields.properties=edd,lmp,age
This is equivalent to:
?fields=case_id,external_id,properties.edd,properties.lmp,properties.age
Examples
Include only specific fields:
GET /a/<domain>/api/case/v2/?case_type=patient&fields=case_id&fields.properties=edd,age
{
"matching_records": 1,
"cases": [
{
"case_id": "abc123",
"properties": {
"edd": "2013-12-09",
"age": "22"
}
}
]
}
Exclude specific fields:
GET /a/<domain>/api/case/v2/<case_id>?exclude=case_name&exclude.properties=husband_name
These parameters work on all GET endpoints and on the case object returned
by POST/PUT (create/update) endpoints. For list and bulk responses, the
filtering applies to each individual case object; envelope fields
(matching_records, next) are not affected.
Field filtering is preserved across paginated requests.
Pagination
While most other CommCare APIs use limit and offset for pagination, this doesn’t work well when pulling large data sets, as performance suffers the deeper you page. To better support pulling large datasets, this API uses what’s called “cursor pagination”. If there is more than one page of results, the response from this API includes a “next” link, which can be followed to get the next page of results. When iterated through in this way, you should obtain a complete set of results, ordered from oldest to newest.
If any cases are updated during the data pull, they may appear again towards the end of the results set.
Get individual case
Interface -
GET /a/<domain>/api/case/v2/<case_id>GET /a/<domain>/api/case/v2/ext/<ext_id>/
This API takes no filter parameters, though you can control which fields are
returned using the fields and exclude parameters as described in
“Limiting Response Fields”. The return value is a single case serialized as
described in “Single Case Serialization Format”.
Warning
If the case is identified by its external ID, and that ID is not unique, only one case will be returned.
Get Case’s index information (parent/child or host/extension)
Interface - GET /a/<domain>/api/case/v2/?indices.parent=<parent_case_id>
Indices are included in the serialization of individual cases, so to find a case’s parent, you need only look at indices.parent.case_id in the case itself.
To access “reverse” indices, you can use the list view with an index filter. For example, to identify children of a household case, you can run a query to find cases which have a parent index pointing to that household case’s ID.
Response format
{
"matching_records": 1,
"cases": [
{
"domain": "queens-gambit",
"case_id": "1",
"case_type": "assignment",
"case_name": "assignment",
"external_id": "1",
"owner_id": "1",
"date_opened": "2021-01-18T18:24:23.480723Z",
"last_modified": "2021-01-18T18:24:23.480723Z",
"server_last_modified": "2021-01-18T19:52:37.516558Z",
"indexed_on": "2021-01-18T19:55:13.707193Z",
"closed": false,
"date_closed": null,
"properties": {
"assignment_type": "primary"
},
"indices": {
"parent": {
"case_id": "1",
"case_type": "contact",
"relationship": "extension"
}
}
}
]
}
Get cases in bulk
1. GET request using Case IDs
Interface - GET /a/<domain>/api/case/v2/<case_id>,<case_id>,<case_id>
Limitations
The number of cases that can be fetched in this way is limited by the maximum URL length making it unsuitable for fetching more than approximately 100 cases. To fetch more cases use the bulk fetch endpoint.
2. POST request using Case IDs or External IDs
Interface - POST /a/<domain>/api/case/v2/bulk_fetch/
A more flexible approach to fetching cases in bulk is to use a POST request where the case IDs are supplied in the POST body. You may also specify external IDs in this way.
Body must have one or both of the ‘case_id’, ‘external_id’ fields.
{
"case_id": [
"30ad22bd-f828-4e3f-8287-a67a180cff4f",
"d5e5962a-c5f1-483c-a58a-590167d594a9"
],
"external_id": [
"id1",
"id2"
]
}
Note: This endpoint allows you to pull data about specific cases by ID or external ID.
Response format (cases truncated for clarity)
{
"matching_records": 2,
"missing_records": 2,
"cases": [
{"case_id": "30ad22bd-f828-4e3f-8287-a67a180cff4f", "properties": {"dob": "1980-08-20"}},
{"case_id": "d5e5962a-c5f1-483c-a58a-590167d594a9", "error": "not found"},
{"external_id": "id1", "error": "not found"},
{"case_id": "5262c03f-f483-4b36-9a08-cdaf89791e65", "external_id": "id2"},
]
}
Response field |
Description |
|---|---|
matching_records |
The number of cases that were found |
missing_records |
The number of cases that were not found |
cases |
The list of serialized cases. The cases in this list will be in the same order as the provided IDs. If a case was not found a stub record will be included in the results as shown above. If both case IDs and external IDs are supplied in the request body then the response will include both sets appended in the same list (cases fetched by ‘case_id’ first followed by cases fetched by ‘external_id’. |
Create Case
Interface - POST /a/<domain>/api/case/v2/
The body of the request should contain the case update format described in “Case Create/Update/Upsert Format”
Note
Requests to POST /a/<domain>/api/case/v2/ where the payload is a
case (not a list) will always create a case. If the data sets the
external_id property to an existing value, CommCare HQ will create a
duplicate external ID. (In other words, POST for an individual case
is not an upsert operation.) If there is a possibility of submitting
duplicate external_id values by accident, rather use the upsert
functionality offered by the PUT interface detailed below.
Return value includes two fields:
Param |
Description |
|---|---|
xform_id |
ID of the form generated to create the case |
case |
Serialized version of the case |
This response includes the current state of the case after the creation (or update) has been provided. Note that if you attempt to immediately fetch the case anyways, there may be a slight delay before the update is available.
Update Existing Case
Interface -
PUT /a/<domain>/api/case/v2/<case_id>PUT /a/<domain>/api/case/v2/ext/<ext_id>/
The body of the request should contain the case update format described in “Case Create/Update/Upsert Format”.
Note
If the case is identified by its external ID, then it will be updated if it is found, and created if it is not found. (In other words, PUT by external ID is an upsert operation.)
Return value includes two fields:
Param |
Description |
|---|---|
xform_id |
ID of the form generated to update the case |
case |
Serialized version of the new state of the case |
Bulk Create/Update Cases
Interface - POST /a/<domain>/api/case/v2/
The body of the request should contain a list of case updates in the format described in “Case Create/Update/Upsert Format”.
In addition to those fields, this endpoint also requires that each update include a “create” field set to either true or false. This is used to determine whether it is a case creation or update.
The response contains:
Param |
Description |
|---|---|
xform_id |
ID of the (single) form generated to update all cases |
cases |
Serialized version of the new state of the cases provided |
Upsert
If a case update includes a value for “external_id” and does not include a value for “case_id”, then the “create” field may be omitted, and the API will upsert the case based on the value of “external_id”. This operation is slower than if the “create” field is included. Ensure that the upserted cases in a single request are unique, and that concurrent requests cannot include the same cases.
Danger
If a single request tries to upsert the same case more than once, duplicate cases will be created! This is because CommCare HQ searches for all upserted cases by their external IDs before any changes are made.
Warning
This operation is vulnerable to a race condition where, if two simultaneous requests try to upsert the same case, two cases could be created.
Limitations
The bulk API will allow users to create or modify up to 100 cases in a single request. These will all be created in a single form submission.
If more than 100 cases are submitted, the server will return a 400 “Payload too large” response, without saving any changes.
Temporary Id
Case indices can be created using either case_id, external_id, or “temporary_id”. This “temporary_id” will not be stored and exists only during the processing of the request - it’s useful for creating both a case and its child in the same request, in situations where case_id and external_id are not suitable.
Example using temporary ID:
[
{
"create": true,
"case_type": "mother",
"case_name": "Cersei Lannister",
"owner_id": "20cc9dda-b90a-4af3-aa3d-fc67184e73ef",
"temporary_id": "1",
"properties": {
"dob": "1988-11-02"
}
},
{
"create": true,
"case_type": "baby",
"case_name": "Tommen Baratheon",
"owner_id": "9dd08c69-4ace-4e1d-9929-146d22d1070c",
"properties": {
"dob": "2008-03-01"
},
"indices": {
"parent": {
"temporary_id": "1",
"case_type": "mother",
"relationship": "child"
}
}
}
]
Form Submission
All case creations and updates will happen by submitting an XForm generated on the server. Here are some notable parameters associated with that:
Param |
Description |
|---|---|
username / user_id |
Corresponding to the user who submitted the request. The API cannot not be used to make it appear that another user made case updates. |
xmlns |
|
device_id |
User agent string from the request |
Most errors are caught before the form is submitted, but some types of issues may only be caught when the form is processed. In these instances, a XFormError is created, and no case changes will occur. The server will return a 400 error response including the ID of the XFormError and an error message.