Skip to content

Tables

A table combined with a frame is where users post positions about the one big question. Each table carries its own positions.

A table can be branched from another table, which records where it came from in parent_id and carries nothing else forward. A table branched from nothing is a root table.

Users can also attach links and save documents on a table. Those belong to whoever added them rather than to the table's owner, and they are fetched from their own endpoints rather than arriving inside the table object.

The table object

AttributeTypeDescription
iduuidThe unique identifier of the table.
typestringThe table type: research, transaction, or discussion.
titlestringThe name or topic of the table.
purposestringAlso known as "the big question", it's the question the table is trying to answer.
frame_iduuidThe unique identifier of the frame attached to this table. Fixed when the table is created.
user_taguuidThe opaque user tag of the user that owns this table. Fixed when the table is created and never changes after. A user session token owns what it creates, so it never sends this. An administrator names the owner with it.
parent_iduuidThe id of the table this one was branched from. null when the table is a root table — one branched from nothing. Fixed when the table is created.
created_atdatetimeWhen the table was created. Never changes, and it is what paginated collections of tables are ordered by.
updated_atdatetimeWhen the table was last modified. It is what trending is ordered by.
positionsarrayAn array of 0–3 position objects.
json
{
  "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
  "type": "research",
  "title": "Ice Cream Scoop Showdown",
  "purpose": "Rank freezer-case pints across flavor, texture, and value",
  "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
  "user_tag": "6f2bd2cb-3ca5-47bd-a680-64cd7cd11312",
  "parent_id": "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9",
  "created_at": "2026-08-01T14:32:07Z",
  "updated_at": "2026-08-03T09:11:48Z",
  "positions": [
    {
      "id": "7c1e4a20-93b6-4f58-8a0d-5e2c19d4b73f",
      "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
      "val1_x": 88, "val1_y": 72,
      "val2_x": 40, "val2_y": 65,
      "val3_x": 22, "val3_y": 18,
      "val4_x": 70, "val4_y": 30
    },
    {
      "id": "2fa86d13-0c74-4e91-b5a3-6d871fe0c42a",
      "headline": "Texture & Creaminess — churn, overrun, mouthfeel",
      "val1_x": 76, "val1_y": 90,
      "val2_x": 55, "val2_y": 48,
      "val3_x": 15, "val3_y": 25,
      "val4_x": 62, "val4_y": 12
    },
    {
      "id": "c50b9e77-4a12-48d6-9f31-70e5b3ad816c",
      "headline": "Value & Availability — price per pint, stock",
      "val1_x": 50, "val1_y": 60,
      "val2_x": 80, "val2_y": 44,
      "val3_x": 30, "val3_y": 33,
      "val4_x": 95, "val4_y": 20
    }
  ]
}

The position object

A position is one person's reading of the big question, plotted against the frame the table is built on. It carries a headline naming the aspect being put forward and four paired values, one pair per quadrant of that frame. A table carries up to three, and they arrive nested in the table object rather than from an endpoint of their own.

AttributeTypeDescription
iduuidThe unique identifier of the position.
headlinestringThe aspect of the question that is being put forth. Up to 120 characters.
val1_xinteger (0-100)paired value for a quadrant of the frame.
val1_yinteger (0-100)paired value for a quadrant of the frame.
val2_xinteger (0-100)paired value for a quadrant of the frame.
val2_yinteger (0-100)paired value for a quadrant of the frame.
val3_xinteger (0-100)paired value for a quadrant of the frame.
val3_yinteger (0-100)paired value for a quadrant of the frame.
val4_xinteger (0-100)paired value for a quadrant of the frame.
val4_yinteger (0-100)paired value for a quadrant of the frame.
json
{
  "id": "7c1e4a20-93b6-4f58-8a0d-5e2c19d4b73f",
  "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
  "val1_x": 88, "val1_y": 72,
  "val2_x": 40, "val2_y": 65,
  "val3_x": 22, "val3_y": 18,
  "val4_x": 70, "val4_y": 30
}

A position is not addressable on its own — there is no endpoint that reads, changes, or removes one. Its place in the table's positions array is its order, and modifying a table replaces that whole array rather than reaching one member of it.

So the id is not a handle, though every read carries it. Nothing takes one: an id sent inside a position is ignored, and an update issues fresh ones, since the replacement destroys the positions that were there and creates the array you sent. Read it as the identity of the row you are looking at rather than as a way to reach that row again.

Creating a table

POST https://api.cxipgroup.com/tables
User token required

To create a table, send an authenticated request to the above endpoint with the table's attributes in the body. Omit the id — it is assigned by the API, and any id in the request body is ignored.

Every table has an owner, and how you name one depends on the token you send. A user session token owns what it creates, so the owner is its own user tag and a user_tag in the body is ignored. An administrator token is bound to nobody, so it names the owner with user_tag, and that tag must be one of your own user tags — naming a tag from another client's account is a 422, not a way into that account. The rule is the same for links and documents, and is explained once in the Authenticating guide.

Send the table's positions along with its other attributes. A table carries up to three, and it may carry none — leave positions out and the table is created without any, which is a 201 like any other.

To branch a table from an existing one, name that table in parent_id. Branching records where the new table came from and copies nothing else — send the full payload for the new table exactly as you would for any other create, and it starts with the positions you give it rather than its parent's. Any table you can read may be a parent. Omit parent_id and the new table is a root table, branched from nothing.

A parent_id naming no table is a 422, and so is one that isn't a single, well-formed UUID. The two report against different fields — parent for a table that doesn't exist, parent_id for a value that couldn't be read as an id — so a client distinguishing "the table is gone" from "I sent a malformed id" has both.

Parameters:

ParameterValueDescription
typestring (required)The table type: research, transaction, or discussion.
frame_iduuid (required)The id of the frame this table is built on.
titlestring (required)The title of the table. Up to 50 characters.
purposestring (required)The purpose of the table, generally referred to as the "big question". Up to 255 characters.
positionsarray (optional)An array of 0–3 position objects. Omit it to create the table with none.
parent_iduuid (optional)The id of the table this one is branched from. Omit it for a root table.
user_taguuid (admin only)The user tag that will own the table. Required when you send an administrator token, ignored when you send a user session token.

Request:

bash
curl -i "https://api.cxipgroup.com/tables" \
  -X POST \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "research",
    "title": "Ice Cream Scoop Showdown",
    "purpose": "Rank freezer-case pints across flavor, texture, and value",
    "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
    "positions": [
      {
        "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
        "val1_x": 88, "val1_y": 72,
        "val2_x": 40, "val2_y": 65,
        "val3_x": 22, "val3_y": 18,
        "val4_x": 70, "val4_y": 30
      },
      {
        "headline": "Texture & Creaminess — churn, overrun, mouthfeel",
        "val1_x": 76, "val1_y": 90,
        "val2_x": 55, "val2_y": 48,
        "val3_x": 15, "val3_y": 25,
        "val4_x": 62, "val4_y": 12
      },
      {
        "headline": "Value & Availability — price per pint, stock",
        "val1_x": 50, "val1_y": 60,
        "val2_x": 80, "val2_y": 44,
        "val3_x": 30, "val3_y": 33,
        "val4_x": 95, "val4_y": 20
      }
    ]
  }'
javascript
const response = await fetch("https://api.cxipgroup.com/tables", {
  method: "POST",
  body: JSON.stringify({
    type: "research",
    title: "Ice Cream Scoop Showdown",
    purpose: "Rank freezer-case pints across flavor, texture, and value",
    frame_id: "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
    positions: [
      {
        "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
        "val1_x": 88, "val1_y": 72,
        "val2_x": 40, "val2_y": 65,
        "val3_x": 22, "val3_y": 18,
        "val4_x": 70, "val4_y": 30
      },
      {
        "headline": "Texture & Creaminess — churn, overrun, mouthfeel",
        "val1_x": 76, "val1_y": 90,
        "val2_x": 55, "val2_y": 48,
        "val3_x": 15, "val3_y": 25,
        "val4_x": 62, "val4_y": 12
      },
      {
        "headline": "Value & Availability — price per pint, stock",
        "val1_x": 50, "val1_y": 60,
        "val2_x": 80, "val2_y": 44,
        "val3_x": 30, "val3_y": 33,
        "val4_x": 95, "val4_y": 20
      }
    ]
  }),
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Response:

json
{
  "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
  "type": "research",
  "title": "Ice Cream Scoop Showdown",
  "purpose": "Rank freezer-case pints across flavor, texture, and value",
  "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
  "user_tag": "6f2bd2cb-3ca5-47bd-a680-64cd7cd11312",
  "parent_id": "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9",
  "created_at": "2026-08-01T14:32:07Z",
  "updated_at": "2026-08-01T14:32:07Z",
  "positions": [
    {
      "id": "7c1e4a20-93b6-4f58-8a0d-5e2c19d4b73f",
      "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
      "val1_x": 88, "val1_y": 72,
      "val2_x": 40, "val2_y": 65,
      "val3_x": 22, "val3_y": 18,
      "val4_x": 70, "val4_y": 30
    },
    {
      "id": "2fa86d13-0c74-4e91-b5a3-6d871fe0c42a",
      "headline": "Texture & Creaminess — churn, overrun, mouthfeel",
      "val1_x": 76, "val1_y": 90,
      "val2_x": 55, "val2_y": 48,
      "val3_x": 15, "val3_y": 25,
      "val4_x": 62, "val4_y": 12
    },
    {
      "id": "c50b9e77-4a12-48d6-9f31-70e5b3ad816c",
      "headline": "Value & Availability — price per pint, stock",
      "val1_x": 50, "val1_y": 60,
      "val2_x": 80, "val2_y": 44,
      "val3_x": 30, "val3_y": 33,
      "val4_x": 95, "val4_y": 20
    }
  ]
}

Status codes:

StatusWhen
201 CreatedThe table was created. Its URL is in the Location header.
401 UnauthorizedMissing or invalid token.
422 Unprocessable ContentValidation failed. More than three positions, a missing frame_id, an unknown type, or a parent_id that names no table or isn't a single well-formed UUID lands here. So does an administrator token sending no user_tag, or one naming a tag that isn't yours.

Listing tables

GET https://api.cxipgroup.com/tables
User token required

To list tables, send an authenticated request to the above endpoint with optional parameters. This endpoint is paginated — the response is a bare array, with pagination metadata in the response headers.

Tables come back newest first, ordered by created_at descending with id breaking ties. The ordering is on created_at rather than updated_at deliberately: created_at never moves, so a table cannot hop between pages while you are paging through the collection, where an edit anywhere in it would reshuffle an updated_at ordering underneath you.

Pass ids to narrow the list to a specific set of tables — the request to make when a user has picked tables out of search results and you want the full records back. It is a filter on the collection rather than a fetch by id: an id matching no table drops out of the results instead of making the whole request a 404. Compare the length of the returned array against the number of ids you sent to detect ids that resolved to nothing — though not when you send user_tag alongside, because a shortfall then means either that an id matched no table or that its table belongs to a different owner, and the count cannot tell you which.

Send ids as a comma-separated string (ids=a,b,c) or as a repeated parameter (ids[]=a&ids[]=b). The indexed-bracket form ids[0]=a&ids[1]=b is rejected with a 422 rather than quietly ignored, so an unreadable selection never comes back looking like the whole collection.

Pass user_tag to narrow the list to the tables one owner owns, naming that owner by the opaque tag every table read already carries in its user_tag field. It takes a single tag rather than a list, a table having exactly one owner.

user_tag narrows what comes back. It does not decide what you are allowed to see. Reads have never been owner-scoped: any caller may name any tag, and two callers naming the same tag get the same answer whether or not either holds it. So a session that wants only its own tables has to name its own tag — that is not the default. Leaving the parameter out returns every owner's tables, which is what it returned before this filter existed.

A well-formed tag that owns nothing comes back as an empty page rather than a 404, the way an id matching no table drops out of a selection. Only the tag's format is checked and never whether it exists, so asking about an owner that was never minted is a legitimate question with an empty answer.

Surrounding whitespace is stripped before the tag is read, so a value pasted out of a log or templated with a trailing newline still matches, the same way it does inside an ids list. What earns a 422 is a value that survives that stripping and still isn't a tag — blank, whitespace only, or unreadable. Where ids drops an entry it can't read and carries on, a lone selector nobody can interpret leaves only "every table" and "no tables" to guess between, and those are opposites.

Parameters:

ParameterValueDescription
idsstring (optional)A comma-separated list of table ids to limit the results to, naming at most 100 tables. Repeating an id does not consume the limit twice. Omitting ids, or sending it empty, returns the whole collection.
user_taguuid (optional)The opaque tag of the owner whose tables you want, narrowing the results to that one owner. Any caller may name any tag. Surrounding whitespace is ignored. A well-formed tag that owns nothing returns an empty page, and a blank or unreadable value is a 422.
limitinteger (optional)The number of records to return per page. Defaults to 20, with a maximum of 100.
pageinteger (optional)The page of records you want returned. 0 or 1 returns the first page. A value beyond the total number of pages returns nothing.

Request:

bash
curl -i -G "https://api.cxipgroup.com/tables" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  --data-urlencode "limit=100" \
  --data-urlencode "page=1"
javascript
const response = await fetch("https://api.cxipgroup.com/tables?limit=100&page=1", {
  method: "GET",
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Request, narrowed to a selection:

bash
curl -i -G "https://api.cxipgroup.com/tables" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  --data-urlencode "ids=984c03a9-7a64-4046-a26f-57cc773c7ec5,1c7f6b04-9d2e-4a13-8b57-3e0af9c62d18"
javascript
const ids = [
  "984c03a9-7a64-4046-a26f-57cc773c7ec5",
  "1c7f6b04-9d2e-4a13-8b57-3e0af9c62d18"
];

const response = await fetch(`https://api.cxipgroup.com/tables?ids=${ids.join(",")}`, {
  method: "GET",
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Request, narrowed to an owner:

bash
curl -i -G "https://api.cxipgroup.com/tables" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  --data-urlencode "user_tag=6f2bd2cb-3ca5-47bd-a680-64cd7cd11312"
javascript
const response = await fetch("https://api.cxipgroup.com/tables?user_tag=6f2bd2cb-3ca5-47bd-a680-64cd7cd11312", {
  method: "GET",
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Response:

json
[
  {
    "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
    "type": "research",
    "title": "Ice Cream Scoop Showdown",
    "purpose": "Rank freezer-case pints across flavor, texture, and value",
    "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
    "user_tag": "6f2bd2cb-3ca5-47bd-a680-64cd7cd11312",
    "parent_id": "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9",
    "created_at": "2026-08-01T14:32:07Z",
    "updated_at": "2026-08-03T09:11:48Z",
    "positions": [
      {
        "id": "7c1e4a20-93b6-4f58-8a0d-5e2c19d4b73f",
        "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
        "val1_x": 88, "val1_y": 72,
        "val2_x": 40, "val2_y": 65,
        "val3_x": 22, "val3_y": 18,
        "val4_x": 70, "val4_y": 30
      }
      // ...
    ]
  }
  // ...
]

Status codes:

StatusWhen
200 OKThe body is a bare array of tables.
401 UnauthorizedMissing or invalid token.
422 Unprocessable ContentThe ids filter names more than 100 tables or arrived in a shape that can't be read as a list of ids, the user_tag is blank or can't be read as a tag, or the limit is negative.

Searching tables

GET https://api.cxipgroup.com/tables/search
User token required

To search tables, send an authenticated request to the above endpoint with your query in q. The match is a case-insensitive substring against a table's title, its purpose — the big question — and the headline of any position plotted on it. A table that matches on more than one of those still comes back once.

Like listing tables, this endpoint is paginated, returns a bare array, and orders results newest first by created_at. Results are not ranked by relevance. Search is not owner-scoped — it reaches every table, not only the ones your user tag owns.

Omitting q, or sending it empty, matches every table.

Pass user_tag to narrow the search to one owner's tables, so q=housing&user_tag=… is that owner's tables matching that text. It behaves exactly as it does when listing tables, down to refusing a blank or unreadable value with a 422. Naming an owner narrows the search rather than scoping it — a search with no user_tag still reaches every owner's tables.

Parameters:

ParameterValueDescription
qstring (optional)The text to search for. % and _ are matched literally rather than as wildcards.
user_taguuid (optional)The opaque tag of the owner whose tables you want, narrowing the results to that one owner. Any caller may name any tag. Surrounding whitespace is ignored. A well-formed tag that owns nothing returns an empty page, and a blank or unreadable value is a 422.
limitinteger (optional)The number of records to return per page. Defaults to 20, with a maximum of 100.
pageinteger (optional)The page of records you want returned. 0 or 1 returns the first page. A value beyond the total number of pages returns nothing.

Request:

bash
curl -i -G "https://api.cxipgroup.com/tables/search" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  --data-urlencode "q=ice cream" \
  --data-urlencode "limit=20"
javascript
const response = await fetch("https://api.cxipgroup.com/tables/search?q=ice+cream&limit=20", {
  method: "GET",
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Response:

json
[
  {
    "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
    "type": "research",
    "title": "Ice Cream Scoop Showdown",
    "purpose": "Rank freezer-case pints across flavor, texture, and value",
    "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
    "user_tag": "6f2bd2cb-3ca5-47bd-a680-64cd7cd11312",
    "parent_id": "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9",
    "created_at": "2026-08-01T14:32:07Z",
    "updated_at": "2026-08-03T09:11:48Z",
    "positions": [
      {
        "id": "7c1e4a20-93b6-4f58-8a0d-5e2c19d4b73f",
        "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
        "val1_x": 88, "val1_y": 72,
        "val2_x": 40, "val2_y": 65,
        "val3_x": 22, "val3_y": 18,
        "val4_x": 70, "val4_y": 30
      }
      // ...
    ]
  }
  // ...
]

Status codes:

StatusWhen
200 OKThe body is a bare array of matching tables.
401 UnauthorizedMissing or invalid token.
422 Unprocessable ContentThe user_tag is blank or can't be read as a tag, or the limit is negative. Other unusable pagination values resolve rather than failing.

GET https://api.cxipgroup.com/tables/trending
User token required

To get the trending tables, send an authenticated request to the above endpoint. It returns the five most recently updated tables, most recent first — ordered by updated_at descending, with id breaking ties so the cut is stable when timestamps collide.

This is a fixed top-five list rather than a browsable collection, so it takes no parameters and is not paginated. page and limit have no effect, and the response carries none of the pagination headers.

Request:

bash
curl -i "https://api.cxipgroup.com/tables/trending" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
javascript
const response = await fetch("https://api.cxipgroup.com/tables/trending", {
  method: "GET",
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Response:

json
[
  {
    "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
    "type": "research",
    "title": "Ice Cream Scoop Showdown",
    "purpose": "Rank freezer-case pints across flavor, texture, and value",
    "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
    "user_tag": "6f2bd2cb-3ca5-47bd-a680-64cd7cd11312",
    "parent_id": "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9",
    "created_at": "2026-08-01T14:32:07Z",
    "updated_at": "2026-08-04T16:05:22Z",
    "positions": [
      {
        "id": "7c1e4a20-93b6-4f58-8a0d-5e2c19d4b73f",
        "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
        "val1_x": 88, "val1_y": 72,
        "val2_x": 40, "val2_y": 65,
        "val3_x": 22, "val3_y": 18,
        "val4_x": 70, "val4_y": 30
      }
      // ...
    ]
  }
  // ... up to four more
]

Status codes:

StatusWhen
200 OKThe body is a bare array of up to five tables.
401 UnauthorizedMissing or invalid token.

Listing a table's children

GET https://api.cxipgroup.com/tables/:id/children
User token required

To list the tables branched from a table, send an authenticated request to the above endpoint. This endpoint is paginated — the response is a bare array, with pagination metadata in the response headers.

It returns one step down and no further. A table branched from one of the children is not in the results, so a longer line is something you assemble a step at a time by calling this endpoint again with a child's id.

Children come back newest first, ordered the same way as the tables collection, so a client paging children sees the ordering it already knows.

Reads are not owner-scoped, so a table someone else branched from this one is listed like any other. A table nothing was branched from is an empty array rather than a 404 — the table exists, and the answer is that it has no children.

This is also the set that has to be cleared before deleting the table stops returning a 409.

Parameters:

ParameterValueDescription
iduuid (required)The unique identifier of the table whose children you want.
limitinteger (optional)The number of records to return per page. Defaults to 20, with a maximum of 100.
pageinteger (optional)The page of records you want returned. 0 or 1 returns the first page. A value beyond the total number of pages returns nothing.

Request:

bash
curl -i -G "https://api.cxipgroup.com/tables/b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9/children" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  --data-urlencode "limit=100" \
  --data-urlencode "page=1"
javascript
const tableId = "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9";

const response = await fetch(`https://api.cxipgroup.com/tables/${tableId}/children?limit=100&page=1`, {
  method: "GET",
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Response: Every table in the array carries the requested table's id as its parent_id.

json
[
  {
    "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
    "type": "research",
    "title": "Ice Cream Scoop Showdown",
    "purpose": "Rank freezer-case pints across flavor, texture, and value",
    "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
    "user_tag": "6f2bd2cb-3ca5-47bd-a680-64cd7cd11312",
    "parent_id": "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9",
    "created_at": "2026-08-01T14:32:07Z",
    "updated_at": "2026-08-03T09:11:48Z",
    "positions": [
      {
        "id": "7c1e4a20-93b6-4f58-8a0d-5e2c19d4b73f",
        "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
        "val1_x": 88, "val1_y": 72,
        "val2_x": 40, "val2_y": 65,
        "val3_x": 22, "val3_y": 18,
        "val4_x": 70, "val4_y": 30
      }
      // ...
    ]
  }
  // ...
]

Status codes:

StatusWhen
200 OKThe body is a bare array of the tables branched from this one, empty when nothing was.
401 UnauthorizedMissing or invalid token.
404 Not FoundNo table with that id.

Averaging positions across tables

GET https://api.cxipgroup.com/tables/positions_average
User token required

To average the positions across a set of tables, send an authenticated request to the above endpoint naming those tables in ids. Every position plotted on every named table is averaged into one result carrying the same eight coordinates a position does — four paired values, one pair per quadrant of the frame. Each coordinate is averaged on its own: val1_x is the mean of every val1_x in the selection, val1_y the mean of every val1_y, and so on down the eight, each rounded to two decimal places.

This is the aggregate view of "these tables, taken together" — the companion to fetching the same selection's full records with ids on the tables list.

ids is required here. The result is the average of a selection, so averaging every position in the system is not a meaningful default.

user_tag is not a filter on this endpoint. An average is of a selection taken together, across whoever owns the tables in it, so narrowing one by owner is not a question this endpoint answers — and sending the parameter is a 422 rather than being ignored, so a request that asked for one owner's average never comes back carrying everybody's. To average what one owner has, list their tables with user_tag first and name the ids that come back.

Parameters:

ParameterValueDescription
idsstring (required)A comma-separated list of table ids to average across, naming at least one table and at most 100. Accepts the same shapes as the ids filter on listing tables, and rejects the same ones.

The positions average object:

AttributeTypeDescription
val1_xdecimal (0-100)The mean of val1_x across every position on the named tables. null if there were none.
val1_ydecimal (0-100)The mean of val1_y, or null.
val2_xdecimal (0-100)The mean of val2_x, or null.
val2_ydecimal (0-100)The mean of val2_y, or null.
val3_xdecimal (0-100)The mean of val3_x, or null.
val3_ydecimal (0-100)The mean of val3_y, or null.
val4_xdecimal (0-100)The mean of val4_x, or null.
val4_ydecimal (0-100)The mean of val4_y, or null.
position_countintegerHow many positions the average was drawn from.
table_countintegerHow many of the named tables actually exist.

It has a position's coordinates but is not one: it carries no headline naming what the averaged pairs describe, no id, and its values are fractional where a plotted position's are whole numbers.

Coordinates come back as null rather than 0 when nothing in the selection has a position, so null can be told apart from a mean that genuinely works out to zero. table_count counts the tables that resolved rather than the ids you sent, so comparing it against the length of your ids list tells you whether any of them matched nothing.

Request:

bash
curl -i -G "https://api.cxipgroup.com/tables/positions_average" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  --data-urlencode "ids=984c03a9-7a64-4046-a26f-57cc773c7ec5,1c7f6b04-9d2e-4a13-8b57-3e0af9c62d18"
javascript
const ids = [
  "984c03a9-7a64-4046-a26f-57cc773c7ec5",
  "1c7f6b04-9d2e-4a13-8b57-3e0af9c62d18"
];

const response = await fetch(`https://api.cxipgroup.com/tables/positions_average?ids=${ids.join(",")}`, {
  method: "GET",
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Response:

json
{
  "val1_x": 82.0, "val1_y": 81.0,
  "val2_x": 47.5, "val2_y": 56.5,
  "val3_x": 18.5, "val3_y": 21.5,
  "val4_x": 66.0, "val4_y": 21.0,
  "position_count": 6,
  "table_count": 2
}

Status codes:

StatusWhen
200 OKThe body is the positions average object.
401 UnauthorizedMissing or invalid token.
422 Unprocessable Contentids was missing or empty, named more than 100 tables, or arrived in a shape that can't be read as a list of ids. Sending user_tag at all lands here too, whatever its value.

Getting a table

GET https://api.cxipgroup.com/tables/:id
User token required

To retrieve a single table by its id, send an authenticated request to the above endpoint. Reads are not owner-scoped — any valid token can retrieve any table.

Parameters:

ParameterValueDescription
iduuid (required)The unique identifier of the table to retrieve.

Request:

bash
curl -i "https://api.cxipgroup.com/tables/984c03a9-7a64-4046-a26f-57cc773c7ec5" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
javascript
const response = await fetch("https://api.cxipgroup.com/tables/984c03a9-7a64-4046-a26f-57cc773c7ec5", {
  method: "GET",
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Response:

json
{
  "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
  "type": "research",
  "title": "Ice Cream Scoop Showdown",
  "purpose": "Rank freezer-case pints across flavor, texture, and value",
  "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
  "user_tag": "6f2bd2cb-3ca5-47bd-a680-64cd7cd11312",
  "parent_id": "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9",
  "created_at": "2026-08-01T14:32:07Z",
  "updated_at": "2026-08-03T09:11:48Z",
  "positions": [
    {
      "id": "7c1e4a20-93b6-4f58-8a0d-5e2c19d4b73f",
      "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
      "val1_x": 88, "val1_y": 72,
      "val2_x": 40, "val2_y": 65,
      "val3_x": 22, "val3_y": 18,
      "val4_x": 70, "val4_y": 30
    },
    {
      "id": "2fa86d13-0c74-4e91-b5a3-6d871fe0c42a",
      "headline": "Texture & Creaminess — churn, overrun, mouthfeel",
      "val1_x": 76, "val1_y": 90,
      "val2_x": 55, "val2_y": 48,
      "val3_x": 15, "val3_y": 25,
      "val4_x": 62, "val4_y": 12
    },
    {
      "id": "c50b9e77-4a12-48d6-9f31-70e5b3ad816c",
      "headline": "Value & Availability — price per pint, stock",
      "val1_x": 50, "val1_y": 60,
      "val2_x": 80, "val2_y": 44,
      "val3_x": 30, "val3_y": 33,
      "val4_x": 95, "val4_y": 20
    }
  ]
}

Status codes:

StatusWhen
200 OKThe body is the table.
401 UnauthorizedMissing or invalid token.
404 Not FoundNo table with that id.

Modifying a table

PUT https://api.cxipgroup.com/tables/:id
User token required

To modify a table, send an authenticated request to the above endpoint with the attributes you want to change. Every one of them is optional — an attribute you leave out keeps the value it already has, so a partial body is a partial update in spite of the PUT.

positions is the exception, and it replaces rather than merges. Send the key and the array you send becomes the table's whole set, with whatever was there before discarded. Leave the key out and the existing positions are untouched. An empty array is how you clear them.

Discarded is literal, so the positions in the response carry ids the table has never had before — even a position you sent back unchanged is a new row rather than an edited one. Nothing accepts a position's id on the way in, so there is no way to hold one across an update, and no reason to keep one you read.

A table's owner is fixed at creation and an update cannot reassign it, so user_tag is never a change here. A user session token may only modify tables its own tag owns. An administrator token acts by id, and may name a user_tag alongside — but the tag it names has to be that table's owner, so there is no modifying one owner's table while naming another. Either way, a mismatch is a 403.

A table's frame is fixed when the table is created, and an update cannot re-frame it: a frame_id that differs from the one the table already has is a 422. Sending back the frame_id you read is fine — it's the value changing that's refused, not the field's presence. Re-framing would leave the table's positions on the coordinates they hold while swapping out the axes they were plotted against, so there is no way to do it.

A table's parent is fixed when the table is created, and an update cannot move it: a parent_id that differs from the one the table already has is a 422, and so is a null sent against a table that has a parent. Sending back the parent_id you read is fine — it's the value changing that's refused, not the field's presence — so a client that fetches a table, edits its title, and sends the whole object back doesn't have to strip the field out first.

An update reads parent_id as strictly as a create does, so a value that isn't a single, well-formed UUID is a 422 here too. Sent against a branched table, such a value comes back reported twice on the same field — once for being unreadable and once as a change, since a value that can't be read is not the parent the table has.

Parameters:

ParameterValueDescription
iduuid (required)The unique identifier of the table to modify.
typestring (optional)The table type: research, transaction, or discussion.
frame_iduuid (optional)The id of the frame this table is built on. Fixed at creation — send back the value the table already carries, or leave it out.
titlestring (optional)The title of the table. Up to 50 characters.
purposestring (optional)The purpose of the table, generally referred to as the "big question". Up to 255 characters.
positionsarray (optional)An array of 0–3 position objects, replacing the table's whole set.
parent_iduuid (optional)The id of the table this one is branched from. Fixed at creation — send back the value the table already carries, or leave it out.
user_taguuid (admin only)The user tag that owns the table. An administrator token may name it, in which case it must be the tag that already owns the table. Leave it out and the table is modified by id alone. Ignored when you send a user session token.

Request:

bash
curl -i "https://api.cxipgroup.com/tables/984c03a9-7a64-4046-a26f-57cc773c7ec5" \
  -X PUT \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "research",
    "title": "Ice Cream Scoop Showdown",
    "purpose": "Rank freezer-case pints across flavor, texture, and value",
    "positions": [
      {
        "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
        "val1_x": 88, "val1_y": 72,
        "val2_x": 40, "val2_y": 65,
        "val3_x": 22, "val3_y": 18,
        "val4_x": 70, "val4_y": 30
      },
      {
        "headline": "Texture & Creaminess — churn, overrun, mouthfeel",
        "val1_x": 76, "val1_y": 90,
        "val2_x": 55, "val2_y": 48,
        "val3_x": 15, "val3_y": 25,
        "val4_x": 62, "val4_y": 12
      },
      {
        "headline": "Value & Availability — price per pint, stock",
        "val1_x": 50, "val1_y": 60,
        "val2_x": 80, "val2_y": 44,
        "val3_x": 30, "val3_y": 33,
        "val4_x": 95, "val4_y": 20
      }
    ]
  }'
javascript
const response = await fetch("https://api.cxipgroup.com/tables/984c03a9-7a64-4046-a26f-57cc773c7ec5", {
  method: "PUT",
  body: JSON.stringify({
    type: "research",
    title: "Ice Cream Scoop Showdown",
    purpose: "Rank freezer-case pints across flavor, texture, and value",
    positions: [
      {
        "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
        "val1_x": 88, "val1_y": 72,
        "val2_x": 40, "val2_y": 65,
        "val3_x": 22, "val3_y": 18,
        "val4_x": 70, "val4_y": 30
      },
      {
        "headline": "Texture & Creaminess — churn, overrun, mouthfeel",
        "val1_x": 76, "val1_y": 90,
        "val2_x": 55, "val2_y": 48,
        "val3_x": 15, "val3_y": 25,
        "val4_x": 62, "val4_y": 12
      },
      {
        "headline": "Value & Availability — price per pint, stock",
        "val1_x": 50, "val1_y": 60,
        "val2_x": 80, "val2_y": 44,
        "val3_x": 30, "val3_y": 33,
        "val4_x": 95, "val4_y": 20
      }
    ]
  }),
  headers: {
    "Content-type": "application/json; charset=UTF-8",
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Response:

json
{
  "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
  "type": "research",
  "title": "Ice Cream Scoop Showdown",
  "purpose": "Rank freezer-case pints across flavor, texture, and value",
  "frame_id": "09a8e2c1-4e6f-438a-a603-1cdc7ccfa2b3",
  "user_tag": "6f2bd2cb-3ca5-47bd-a680-64cd7cd11312",
  "parent_id": "b3f9d0e2-5a71-4c8e-9d24-8f6a1c07e5b9",
  "created_at": "2026-08-01T14:32:07Z",
  "updated_at": "2026-08-04T16:05:22Z",
  "positions": [
    {
      "id": "a41d7f68-2b0c-4e35-9c86-1fd30a8e57b2",
      "headline": "Flavor Intensity — cocoa, vanilla bean, fruit punch",
      "val1_x": 88, "val1_y": 72,
      "val2_x": 40, "val2_y": 65,
      "val3_x": 22, "val3_y": 18,
      "val4_x": 70, "val4_y": 30
    },
    {
      "id": "5e93c082-71af-4d6b-83e0-2c47b9f1a6d5",
      "headline": "Texture & Creaminess — churn, overrun, mouthfeel",
      "val1_x": 76, "val1_y": 90,
      "val2_x": 55, "val2_y": 48,
      "val3_x": 15, "val3_y": 25,
      "val4_x": 62, "val4_y": 12
    },
    {
      "id": "08b6f2d4-6c39-4a17-95be-e1d78c034f9a",
      "headline": "Value & Availability — price per pint, stock",
      "val1_x": 50, "val1_y": 60,
      "val2_x": 80, "val2_y": 44,
      "val3_x": 30, "val3_y": 33,
      "val4_x": 95, "val4_y": 20
    }
  ]
}

Status codes:

StatusWhen
200 OKThe body is the updated table.
401 UnauthorizedMissing or invalid token.
403 ForbiddenThe table belongs to a different user tag.
404 Not FoundNo table with that id.
422 Unprocessable ContentValidation failed. More than three positions, a position outside the 0–100 coordinate range, a frame_id that differs from the frame the table is built on, or a parent_id that isn't a single well-formed UUID or that differs from the table's current parent, lands here.

Deleting a table

DELETE https://api.cxipgroup.com/tables/:id
User token required

To delete a table, send an authenticated request to the above endpoint. A successful delete returns an empty body, and takes the table's positions with it — along with every link and document users have put on it, whichever user tag owns them.

Deleting is owner-scoped the same way modifying is. A user session token may only delete tables its own tag owns, and an administrator token acting by id may name a user_tag only if that tag is the table's owner.

A table another table was branched from cannot be deleted: the request is a 409 for as long as anything points at it through parent_id. Branched tables are never taken with it, so clearing the way means deleting them first, working back from the newest.

To find what is in the way, list the table's children. That endpoint returns one step down, which is the same step this 409 is raised over.

Parameters:

ParameterValueDescription
iduuid (required)The unique identifier of the table to delete.

Request:

bash
curl -i "https://api.cxipgroup.com/tables/984c03a9-7a64-4046-a26f-57cc773c7ec5" \
  -X DELETE \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
javascript
const response = await fetch("https://api.cxipgroup.com/tables/984c03a9-7a64-4046-a26f-57cc773c7ec5", {
  method: "DELETE",
  headers: {
    "Authorization": "Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"
  },
});

Status codes:

StatusWhen
204 No ContentThe table was deleted. The body is empty.
401 UnauthorizedMissing or invalid token.
403 ForbiddenThe table belongs to a different user tag.
404 Not FoundNo table with that id.
409 ConflictThe table is still referenced by one or more branched tables.

Shapecaster API Documentation