Data Stores REST API
The Data Stores REST API lets an authenticated integration inspect, summarize, paginate, and tag entries stored by an Nviti form.
Use these endpoints to:
- Read the form definition
- Retrieve paginated form entries
- Count entries that match a date range or tag filter
- Calculate the most common values for selected fields
- Add or remove tags from one returned page of records
- Build external dashboards, exports, and batch processors
Endpoint
GET https://api.nviti.ng/api/v1/data-stores/{form_hash}
GET https://api.nviti.ng/api/v1/data-stores/{form_hash}/entries
GET https://api.nviti.ng/api/v1/data-stores/{form_hash}/entries/count
GET https://api.nviti.ng/api/v1/data-stores/{form_hash}/entries/unique-values
PATCH https://api.nviti.ng/api/v1/data-stores/{form_hash}/entries/tags
form_hash is the hash shown in the Data Stores dashboard. For example, the form dashboard URL:
https://nviti.ng/workspace/data-dashboards/OQxD4xbW
has the form hash OQxD4xbW, producing this API endpoint:
https://api.nviti.ng/api/v1/data-stores/OQxD4xbW
The old /api/v1/data-stores/query and /api/v1/data-stats/query routes are no longer available. The hash-based /api/v1/data-stores/{form_hash}/query route remains available for older integrations, but new integrations should use the focused REST endpoints above.
Authentication and Company Scope
The endpoint requires an Nviti API token using Bearer authentication:
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json
Content-Type: application/json
Requests are always restricted to the authenticated user's tenant and selected company. A valid form hash from another company returns 404 Not Found.
When the API token can access more than one company, send the company's hash in the X-Company-ID header:
X-Company-ID: COMPANY_HASH
If X-Company-ID is omitted, Nviti uses the first company available to the authenticated user. Supplying it explicitly is recommended for production integrations.
Basic Requests
curl --request GET \
--url "https://api.nviti.ng/api/v1/data-stores/OQxD4xbW/entries/count?date_from=2026-06-04T00%3A00%3A00%2B01%3A00&date_to=2026-06-04T23%3A59%3A59%2B01%3A00" \
--header "Authorization: Bearer ${NVITI_API_TOKEN}" \
--header "X-Company-ID: ${NVITI_COMPANY_HASH}" \
--header "Accept: application/json"
curl --request GET \
--url "https://api.nviti.ng/api/v1/data-stores/OQxD4xbW/entries?limit=20&page=1" \
--header "Authorization: Bearer ${NVITI_API_TOKEN}" \
--header "X-Company-ID: ${NVITI_COMPANY_HASH}" \
--header "Accept: application/json"
Query Parameters
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
date_from |
date/time | No | - | Inclusive lower bound for the record's created_at timestamp. |
date_to |
date/time | No | - | Inclusive upper bound for created_at. Must be after or equal to date_from. |
unique_fields |
string[] | Required on /entries/unique-values |
- | Fields to summarize. |
tags |
string[] | No | [] |
Include records containing any supplied tag. |
no_tags |
string[] | No | [] |
Exclude records containing any supplied tag. |
limit |
integer | No | 100 |
Number of rows per page on /entries and /entries/tags. Minimum 1, maximum 1000. |
page |
integer | No | 1 |
One-based page number on /entries and /entries/tags. Minimum 1. |
Tag Mutation Body
PATCH /entries/tags accepts the same filters and pagination fields as /entries, plus:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
apply_tags |
string[] | No | [] |
Add tags to records returned on the requested page. |
remove_tags |
string[] | No | [] |
Remove tags from records returned on the requested page. |
Tag arrays accept a maximum of 50 tags per field. Each tag may contain up to 100 characters.
Form Selection
The form is selected exclusively by the {form_hash} URL segment. Do not send source or slug in the request body.
The hash-based URL prevents integrations from depending on a mutable form name or slug. The API resolves the hash inside the authenticated tenant and selected company before querying entries.
Returned row fields come from the form entry's mapped data. System fields id, tags, metadata, and created_at are always included.
Resources
Form Definition
GET /api/v1/data-stores/{form_hash} returns the definition of the selected form:
{
"data": {
"name": "Prayer Request",
"description": "Collect prayer requests",
"fields": [
{
"name": "phone_number",
"label": "Phone",
"type": "TextInput",
"required": true
}
]
}
}
Entry Count
GET /entries/count returns the number of form entries matching the supplied date and tag filters.
{
"data": {
"count": 247
}
}
count is not limited by page or limit.
Unique Values
GET /entries/unique-values calculates value frequencies for every field listed in unique_fields.
{
"unique_fields": ["source", "status"]
}
Example response:
{
"data": {
"unique": {
"source": {
"distinct_count": 2,
"values": [
{
"value": "whatsapp",
"count": 180
},
{
"value": "web",
"count": 67
}
]
}
}
}
}
Important behavior:
unique_fieldsis required when requestingunique.- Dot notation may be used to read nested values, for example
customer.country. - Missing fields are counted as a
nullvalue. distinct_countreports the full number of distinct values.- The
valueslist contains at most the 100 most frequent values. - The metric uses all matching records and is not limited by row pagination.
Entries
GET /entries returns a page of matching records, ordered by created_at from newest to oldest.
{
"data": {
"data": [
{
"phone_number": "2348000000000",
"source": "whatsapp",
"id": "665f04408b15f40f400c5192",
"tags": ["processed"],
"metadata": {
"visitor_id": "visitor-123",
"ip": "127.0.0.1"
},
"created_at": "2026-06-04T05:10:39+00:00"
}
],
"meta": {
"page": 1,
"limit": 20,
"total": 247,
"total_pages": 13,
"has_more": true,
"next_page": 2,
"previous_page": null
}
}
}
Every row always includes:
| Field | Description |
|---|---|
id |
MongoDB identifier for the stored record. |
tags |
Current record tags. Returns an empty array when no tags exist. |
metadata |
Entry metadata, such as visitor details captured alongside the entry. Returns an empty object when no metadata exists. |
created_at |
Entry creation time formatted as an ISO 8601 timestamp. |
Date Filtering
date_from and date_to filter records using created_at. Both boundaries are inclusive.
Use explicit ISO 8601 timestamps with an offset or Z suffix to avoid timezone ambiguity:
GET /api/v1/data-stores/OQxD4xbW/entries?date_from=2026-06-01T00:00:00+01:00&date_to=2026-06-07T23:59:59+01:00
Tag Filtering
Tags are top-level properties of stored form entries.
Match Any Tag with tags
tags uses ANY-match behavior. A record is included when it contains at least one requested tag.
{
"tags": ["priority", "attendance"]
}
This matches a record tagged priority, a record tagged attendance, or a record containing both.
Exclude Any Tag with no_tags
no_tags excludes records containing any supplied tag.
{
"no_tags": ["processed", "archived"]
}
This excludes records containing processed, archived, or both.
Combine Tag Filters
{
"tags": ["attendance"],
"no_tags": ["processed"]
}
This selects attendance records that have not yet been marked as processed.
Tag values are trimmed, empty values are discarded, and duplicate values are removed before querying or mutating records. Tag matching is case-sensitive.
Applying and Removing Tags
apply_tags and remove_tags modify only the records returned in rows.data for the requested page.
They do not modify:
- Matching records on another page
- Records excluded by filters
- Records outside the selected company
- Records outside the requested date range
Use PATCH /entries/tags whenever either mutation field is non-empty.
Apply Tags
{
"no_tags": ["processed"],
"apply_tags": ["processed"],
"limit": 100,
"page": 1
}
Tags are merged without creating duplicates.
Remove Tags
{
"tags": ["processed"],
"remove_tags": ["processed", "archived"],
"limit": 100,
"page": 1
}
Tags that are not currently present are ignored.
Apply and Remove in One Request
{
"apply_tags": ["reviewed"],
"remove_tags": ["processed"]
}
If the same tag appears in both apply_tags and remove_tags, removal wins.
Mutation Response
When tags are applied or removed, the response includes a tagging section:
{
"data": {
"rows": {
"data": [],
"meta": {}
},
"tagging": {
"applied_tags": ["reviewed"],
"removed_tags": ["processed"],
"updated_count": 18
}
}
}
updated_count is the number of returned records whose stored tags actually changed. Records already in the requested final state are not counted as updated.
Filters, counts, and pagination metadata describe the matching records before tag mutations are applied. Returned rows are refreshed and show their tags after mutation.
Recommended Batch Retrieval Pattern
To retrieve each unprocessed entry once:
- Filter using
no_tags: ["processed"]. - Call
PATCH /entries/tagswithapply_tags: ["processed"]. - Handle the returned rows.
- Repeat using
page: 1untilrows.datais empty.
Always requesting page 1 is important because applying processed removes those records from the next no_tags: ["processed"] result set.
{
"no_tags": ["processed"],
"apply_tags": ["processed"],
"limit": 100,
"page": 1
}
apply_tagsandremove_tagsare executed by Nviti before the response is returned. This pattern marks a row as processed when it is retrieved, not when your downstream work completes. Use it when successful retrieval is your completion boundary or when your consumer can safely retry already-tagged rows. If you require strict acknowledgement after downstream processing, maintain the returned row IDs in your integration and use an acknowledgement workflow designed for that requirement.
Complete REST Workflow
GET /api/v1/data-stores/OQxD4xbW
GET /api/v1/data-stores/OQxD4xbW/entries/count?date_from=2026-06-01T00:00:00+01:00&date_to=2026-06-30T23:59:59+01:00&tags[]=attendance&no_tags[]=archived
GET /api/v1/data-stores/OQxD4xbW/entries/unique-values?unique_fields[]=source&date_from=2026-06-01T00:00:00+01:00&date_to=2026-06-30T23:59:59+01:00
GET /api/v1/data-stores/OQxD4xbW/entries?limit=50&page=1&tags[]=attendance&no_tags[]=archived
PATCH /api/v1/data-stores/OQxD4xbW/entries/tags
Example tag mutation body:
{
"tags": ["attendance"],
"no_tags": ["archived"],
"apply_tags": ["exported"],
"remove_tags": ["pending-export"],
"limit": 50,
"page": 1
}
Counting Entries Only
For dashboards that only need a count, call the count endpoint:
curl --request GET \
--url "https://api.nviti.ng/api/v1/data-stores/OQxD4xbW/entries/count?date_from=2026-06-04T00%3A00%3A00%2B01%3A00&date_to=2026-06-04T23%3A59%3A59%2B01%3A00" \
--header "Authorization: Bearer ${NVITI_API_TOKEN}" \
--header "X-Company-ID: ${NVITI_COMPANY_HASH}" \
--header "Accept: application/json"
This avoids calculating definitions, unique values, or rows that the caller does not need.
Validation and Error Responses
401 Unauthorized
The Bearer token is missing, invalid, or revoked.
403 Forbidden
The authenticated user does not have a usable tenant or selected company context.
404 Not Found
The supplied form hash is invalid or does not belong to the selected tenant and company.
422 Unprocessable Entity
The request failed validation. The response identifies invalid fields:
{
"message": "The unique fields field is required.",
"errors": {
"unique_fields": [
"The unique fields field is required."
]
}
}
Common validation failures:
- Requesting
/entries/unique-valueswithoutunique_fields - Setting
limitabove1000 - Setting
date_tobeforedate_from - Supplying more than 50 tags in a tag field
Performance Recommendations
- Use
/entries/countfor simple counters. - Avoid requesting
uniqueacross very large unbounded datasets; use date and tag filters. - Keep row pages reasonably sized. The maximum is 1000, but smaller batches reduce processing time and retry cost.
- Use explicit date ranges for scheduled reports.
- Use tags to create resumable processing workflows.
- Store API tokens in environment variables or a secrets manager. Never place them in source control.
- Send
X-Company-IDexplicitly when an API token can access multiple companies.
Keywords
data stores API, data query API, form hash, form entries API, entry tags, batch processing, data pagination, unique values, API integration, data exports