Case Data API v2

Major changes from v1

  1. 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.

  2. Allows for filtering and querying cases by project-specific case properties, not just metadata.

  3. Introduces a bulk get-by-id and get-by-external-id endpoint.

  4. Clearer serialization format that’s more in-line with commonly used terminology elsewhere on HQ.

  5. 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:

  • Not blank

  • Contains only letters and numbers

  • Doesn’t start with a number

  • Doesn’t start with “xml”

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:

fields

A comma-separated list of field names to include. Only specified fields will appear in the response. Use dot notation for nested fields.

exclude

A 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

http://commcarehq.org/case_api

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.