Data Tables
The Memberstack Admin REST API provides powerful endpoints for managing Data Tables. Data Tables allow you to store structured data with support for relationships, access control rules, and advanced querying capabilities.
- Make sure you have your secret key ready (refer to the Quick Start guide for authentication details)
- All Data Tables endpoints use the
/v2API version - Be mindful of the rate limit (25 requests per second)
- Data Tables must be created in the Memberstack dashboard (Data Tables section) before using the API
Overview
Data Tables API endpoints and capabilities.
The Data Tables API provides six endpoints for complete data management:
| Method | Endpoint | Description |
|---|---|---|
| GET | /v2/data-tables | List all data tables |
| GET | /v2/data-tables/:tableKey | Get a specific table by key or ID |
| POST | /v2/data-tables/:tableKey/records | Create a new record |
| POST | /v2/data-tables/:tableKey/records/query | Query records with filters |
| PUT | /v2/data-tables/:tableKey/records/:recordId | Update an existing record |
| DELETE | /v2/data-tables/:tableKey/records/:recordId | Delete a record |
Field Types
Data Tables support the following field types:
TEXT- String valuesTEXT_FILTERABLE- Short text optimized for filtering (255 character max)TEXT_UNIQUE- String values with unique constraintNUMBER- Numeric valuesDECIMAL- Decimal/currency valuesBOOLEAN- True/false valuesDATE- Date and time valuesEMAIL- Email address valuesURL- URL valuesREFERENCE- Single record relationshipREFERENCE_MANY- Multiple record relationshipsMEMBER_REFERENCE- Single member relationshipMEMBER_REFERENCE_MANY- Multiple member relationships
List Data Tables
Retrieve all data tables in your application.
Retrieve all data tables in your application.
curl --location --request GET 'https://admin.memberstack.com/v2/data-tables' \
--header 'x-api-key: sk_sb_your_secret_key'const axios = require('axios');
const API_KEY = process.env.MEMBERSTACK_SECRET_KEY;
const BASE_URL = 'https://admin.memberstack.com/v2/data-tables';
const headers = { "X-API-KEY": API_KEY };
const response = await axios.get(BASE_URL, { headers });
console.log(response.data);{
"data": {
"tables": [
{
"id": "tbl_cm1abc123def456",
"key": "products",
"name": "Products",
"createRule": "AUTHENTICATED",
"readRule": "PUBLIC",
"updateRule": "AUTHENTICATED_OWN",
"deleteRule": "ADMIN_ONLY",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-20T14:45:00.000Z",
"fields": [
{
"id": "cm2xyz789ghi012abc",
"key": "name",
"name": "Product Name",
"type": "TEXT",
"required": true,
"defaultValue": null,
"tableOrder": 0,
"referencedTableId": null
},
{
"id": "cm3def456jkl789xyz",
"key": "price",
"name": "Price",
"type": "DECIMAL",
"required": true,
"defaultValue": null,
"tableOrder": 0,
"referencedTableId": null
},
{
"id": "cm4ghi789mno012def",
"key": "category",
"name": "Category",
"type": "REFERENCE",
"required": false,
"defaultValue": null,
"tableOrder": 0,
"referencedTableId": "tbl_cm5cat001pqr345",
"referencedTable": {
"id": "tbl_cm5cat001pqr345",
"key": "categories",
"name": "Categories"
}
}
]
}
]
}
}Get Data Table
Retrieve a specific data table by key or ID.
Replace :tableKey with either:
- Table key (e.g.,
products) - Table ID (e.g.,
tbl_cm1abc123def456)
Retrieve a specific data table by key or ID.
products) or table ID (e.g. tbl_cm1abc123def456).curl --location --request GET 'https://admin.memberstack.com/v2/data-tables/products' \
--header 'x-api-key: sk_sb_your_secret_key'const axios = require('axios');
const API_KEY = process.env.MEMBERSTACK_SECRET_KEY;
const BASE_URL = 'https://admin.memberstack.com/v2/data-tables';
const headers = { "X-API-KEY": API_KEY };
// Get by table key
const response = await axios.get(`${BASE_URL}/products`, { headers });
// Or get by table ID
const responseById = await axios.get(`${BASE_URL}/tbl_cm1abc123def456`, { headers });{
"data": {
"id": "tbl_cm1abc123def456",
"key": "products",
"name": "Products",
"createRule": "AUTHENTICATED",
"readRule": "PUBLIC",
"updateRule": "AUTHENTICATED_OWN",
"deleteRule": "ADMIN_ONLY",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-20T14:45:00.000Z",
"fields": [
{
"id": "cm2xyz789ghi012abc",
"key": "name",
"name": "Product Name",
"type": "TEXT",
"required": true,
"defaultValue": null,
"tableOrder": 0,
"referencedTableId": null
}
]
}
}Create Data Record
Create a new record in a data table. Replace :tableKey with the table key or ID.
Create a new record in a data table.
curl --location --request POST 'https://admin.memberstack.com/v2/data-tables/products/records' \
--header 'x-api-key: sk_sb_your_secret_key' \
--header 'Content-Type: application/json' \
--data-raw '{
"data": {
"name": "Premium Widget",
"price": 29.99,
"inStock": true,
"category": "cm6cat001stu678abc"
}
}'curl --location --request POST 'https://admin.memberstack.com/v2/data-tables/products/records' \
--header 'x-api-key: sk_sb_your_secret_key' \
--header 'Content-Type: application/json' \
--data-raw '{
"data": {
"name": "Premium Widget",
"price": 29.99,
"inStock": true
},
"memberId": "mem_abc123"
}'const axios = require('axios');
const API_KEY = process.env.MEMBERSTACK_SECRET_KEY;
const BASE_URL = 'https://admin.memberstack.com/v2/data-tables';
const headers = {
"X-API-KEY": API_KEY,
"Content-Type": "application/json"
};
// Without member ownership (record will show "System" as creator)
const response = await axios.post(`${BASE_URL}/products/records`, {
data: {
name: "Premium Widget",
price: 29.99,
inStock: true,
category: "cm6cat001stu678abc" // Reference to another record
}
}, { headers });
// With member ownership
const ownedResponse = await axios.post(`${BASE_URL}/products/records`, {
data: {
name: "Member's Widget",
price: 29.99
},
memberId: "mem_abc123"
}, { headers });
console.log(response.data);{
"data": {
"id": "cm7new123vwx901def",
"tableKey": "products",
"data": {
"name": "Premium Widget",
"price": "29.99",
"inStock": true,
"category": "cm6cat001stu678abc"
},
"createdAt": "2024-01-25T09:15:00.000Z",
"updatedAt": "2024-01-25T09:15:00.000Z",
"internalOrder": 12345
}
}Response with memberId (note the createdByMemberId field):
{
"data": {
"id": "cm7new456xyz789ghi",
"tableKey": "products",
"createdByMemberId": "mem_abc123",
"data": {
"name": "Member's Widget",
"price": "29.99"
},
"createdAt": "2024-01-25T09:15:00.000Z",
"updatedAt": "2024-01-25T09:15:00.000Z",
"internalOrder": 12346
}
}The memberId parameter is validated against the current app and environment:
- The member must exist in the same app as the secret key
- The member’s environment must match the key type (live key = live members, sandbox key = sandbox members)
- Returns 404 with
"Member not found"if the member does not exist or fails validation - Returns 400 with
"memberId must be a non-empty string"if the value is not a valid string
- Use
memberIdwhen migrating data into tables withAUTHENTICATED_OWNaccess rules so members can access their own records - Fields with default values will be automatically populated if not provided
- For REFERENCE fields, provide the referenced record’s ID
- For REFERENCE_MANY fields, provide an array of record IDs
- Required fields must be provided or have a default value
- DECIMAL fields are returned as strings in create, update, and delete responses (e.g.
"29.99"); the query endpoint returns them as numbers.
Update Data Record
Update an existing record in a data table. Replace :tableKey with the table key or ID, and :recordId with the record ID.
Update an existing record in a data table.
curl --location --request PUT 'https://admin.memberstack.com/v2/data-tables/products/records/cm8abc123yza234ghi' \
--header 'x-api-key: sk_sb_your_secret_key' \
--header 'Content-Type: application/json' \
--data-raw '{
"data": {
"price": 39.99,
"inStock": false
}
}'const axios = require('axios');
const API_KEY = process.env.MEMBERSTACK_SECRET_KEY;
const BASE_URL = 'https://admin.memberstack.com/v2/data-tables';
const headers = {
"X-API-KEY": API_KEY,
"Content-Type": "application/json"
};
const response = await axios.put(
`${BASE_URL}/products/records/cm8abc123yza234ghi`,
{
data: {
price: 39.99,
inStock: false
}
},
{ headers }
);
console.log(response.data);{
"data": {
"id": "cm8abc123yza234ghi",
"tableKey": "products",
"data": {
"name": "Premium Widget",
"price": "39.99",
"inStock": false,
"category": "cm6cat001stu678abc"
},
"createdAt": "2024-01-25T09:15:00.000Z",
"updatedAt": "2024-01-26T11:30:00.000Z",
"internalOrder": 12345
}
}- Updates are partial - only include the fields you want to change
- The data object cannot be empty
- Unique constraints are validated (excluding the current record)
- Reference constraints are validated for relationship fields
Delete Data Record
Delete a record from a data table. Replace :tableKey with the table key or ID, and :recordId with the record ID.
Delete a record from a data table.
curl --location --request DELETE 'https://admin.memberstack.com/v2/data-tables/products/records/cm8abc123yza234ghi' \
--header 'x-api-key: sk_sb_your_secret_key'const axios = require('axios');
const API_KEY = process.env.MEMBERSTACK_SECRET_KEY;
const BASE_URL = 'https://admin.memberstack.com/v2/data-tables';
const headers = { "X-API-KEY": API_KEY };
const response = await axios.delete(
`${BASE_URL}/products/records/cm8abc123yza234ghi`,
{ headers }
);
console.log(response.data);{
"data": {
"id": "cm8abc123yza234ghi",
"tableKey": "products",
"data": {
"name": "Premium Widget",
"price": "39.99",
"inStock": false,
"category": "cm6cat001stu678abc"
},
"createdAt": "2024-01-25T09:15:00.000Z",
"updatedAt": "2024-01-26T11:30:00.000Z",
"internalOrder": 12345
}
}Deleting a record is permanent and cannot be undone.
- The deleted record data is returned in the response
- Consider the impact on related records before deletion
- Implement soft-delete in your application if you need to preserve data
Query Data Records
Query records with advanced filtering, sorting, and pagination. Replace :tableKey with the table key or ID.
Request Body
The query endpoint uses a Prisma-like query syntax with either findMany or findUnique.
| Parameter | Type | Description |
|---|---|---|
query.findMany | object | Query multiple records (mutually exclusive with findUnique) |
query.findUnique | object | Query a single record by ID (mutually exclusive with findMany) |
Query Options (findMany)
| Option | Type | Description |
|---|---|---|
where | object | Filter conditions |
include | object | Include related records or counts |
select | object | Select specific fields (cannot use with include) |
orderBy | object | array | Sort results |
take | number | Limit number of results (max 100) |
skip | number | Offset pagination (max 10000) |
after | number | string | Cursor-based pagination (cannot use with skip) |
_count | boolean | Return only the count of matching records |
Where Operators
Filter conditions support the following operators:
| Operator | Description | Example |
|---|---|---|
equals | Exact match | { price: { equals: 29.99 } } |
not | Not equal | { status: { not: 'archived' } } |
in | In array | { status: { in: ['active', 'pending'] } } |
notIn | Not in array | { status: { notIn: ['deleted'] } } |
lt | Less than | { price: { lt: 100 } } |
lte | Less than or equal | { price: { lte: 100 } } |
gt | Greater than | { price: { gt: 50 } } |
gte | Greater than or equal | { price: { gte: 50 } } |
contains | Contains substring | { name: { contains: 'widget' } } |
startsWith | Starts with | { name: { startsWith: 'Premium' } } |
endsWith | Ends with | { name: { endsWith: 'Pro' } } |
Logical operators AND, OR, and NOT can be used to combine conditions.
Query records with advanced filtering, sorting, and pagination.
const response = await axios.post(`${BASE_URL}/products/records/query`, {
query: {
findMany: {
where: {
inStock: true,
price: { gte: 20, lte: 100 }
},
orderBy: { price: 'asc' },
take: 10
}
}
}, { headers });// First page
const firstPage = await axios.post(`${BASE_URL}/products/records/query`, {
query: {
findMany: {
take: 20,
orderBy: { createdAt: 'desc' }
}
}
}, { headers });
// Next page using cursor
const nextPage = await axios.post(`${BASE_URL}/products/records/query`, {
query: {
findMany: {
take: 20,
after: firstPage.data.data.pagination.endCursor,
orderBy: { createdAt: 'desc' }
}
}
}, { headers });const response = await axios.post(`${BASE_URL}/products/records/query`, {
query: {
findMany: {
where: { inStock: true },
include: {
category: true, // Include referenced category
_count: {
select: {
orders: true // Count related orders
}
}
}
}
}
}, { headers });const response = await axios.post(`${BASE_URL}/products/records/query`, {
query: {
findMany: {
where: {
OR: [
{ price: { lt: 10 } },
{ AND: [
{ price: { gte: 50 } },
{ inStock: true }
]}
]
}
}
}
}, { headers });const response = await axios.post(`${BASE_URL}/products/records/query`, {
query: {
findMany: {
where: { inStock: true },
_count: true
}
}
}, { headers });
// Response: { "data": { "_count": 42 } }const response = await axios.post(`${BASE_URL}/products/records/query`, {
query: {
findUnique: {
where: { id: "cm8abc123yza234ghi" },
include: {
category: true
}
}
}
}, { headers });const response = await axios.post(`${BASE_URL}/products/records/query`, {
query: {
findMany: {
where: {
createdAt: {
gte: "2024-01-01T00:00:00.000Z",
lt: "2024-02-01T00:00:00.000Z"
}
}
}
}
}, { headers });{
"data": {
"records": [
{
"id": "cm8abc123yza234ghi",
"internalOrder": 12345,
"createdAt": "2024-01-25T09:15:00.000Z",
"updatedAt": "2024-01-26T11:30:00.000Z",
"data": {
"name": "Premium Widget",
"price": 29.99,
"inStock": true
}
}
],
"pagination": {
"hasMore": true,
"limit": 20,
"endCursor": 12345
}
}
}Response (findUnique)
{
"data": {
"record": {
"id": "cm8abc123yza234ghi",
"internalOrder": 12345,
"createdAt": "2024-01-25T09:15:00.000Z",
"updatedAt": "2024-01-26T11:30:00.000Z",
"data": {
"name": "Premium Widget",
"price": 29.99,
"inStock": true,
"category": {
"id": "cm6cat001stu678abc",
"data": {
"name": "Electronics"
}
}
}
}
}
}- Use cursor-based pagination (
after) for better performance on large datasets - Limit your
takevalue to only what you need (max 100) - Use
selectto retrieve only the fields you need - Maximum include depth is 3 levels
- Maximum of 10 includes per query
- Maximum of 50 where conditions per query
- Cannot use both
skipandaftertogether - Cannot use both
includeandselecttogether findUniqueonly allowsidin the where clausefindUniquedoes not supporttake,skip,after, ororderByfindUniquedoes not support top-level_count(use within includes instead)- REFERENCE_MANY and MEMBER_REFERENCE_MANY includes are only available in
findUnique
Error Handling
The API returns consistent error responses with a code and message:
{
"code": "generic-message",
"message": "Human-readable description of what went wrong"
}For Data Tables endpoints, code is generic-message for virtually all errors (validation and not-found alike), so branch on the HTTP status and read the message field for specifics. The shared auth layer is the exception: a missing or malformed secret key returns validation/invalid-secret-key and a malformed JSON body returns validation/invalid-json.
Common Errors
| Status | Error | Description |
|---|---|---|
| 400 | Table key is required | Missing table key in URL |
| 400 | Record ID is required | Missing record ID in URL |
| 400 | data must be an object | Invalid data format in request body |
| 400 | data cannot be empty | Update request with empty data object |
| 400 | Query parameter is required | Missing query in request body |
| 400 | Invalid table key or ID format | Table key contains invalid characters |
| 400 | take must be between 1 and 100 | Query take value out of range |
| 400 | Either query.findMany or query.findUnique parameter is required | Missing query type in request body |
| 400 | Cannot specify both query.findMany and query.findUnique | Both query types provided |
| 400 | query.findUnique requires where.id | Missing id in findUnique where clause |
| 404 | Data table not found | Table doesn't exist or wrong app |
| 404 | Data record not found | Record doesn't exist |
| 404 | Record not found | findUnique query returned no results |
Error Handling Example
try {
const response = await axios.post(`${BASE_URL}/products/records`, {
data: {
name: "New Product",
price: 29.99
}
}, { headers });
console.log('Record created:', response.data);
} catch (error) {
if (error.response) {
// Server responded with error
console.error('Error code:', error.response.data.code);
console.error('Error message:', error.response.data.message);
console.error('Status:', error.response.status);
switch (error.response.status) {
case 400:
// Handle validation errors
break;
case 404:
// Handle not found errors
break;
default:
// Handle other errors
}
} else {
// Network or other error
console.error('Network error:', error.message);
}
}