Skip to content
Gravity Tables
All documentation

Developer

REST API reference

Every endpoint under /wp-json/gt/v1/, with auth, request shape, response shape, and the permission rules the server applies before answering.

Gravity Tables ships a versioned REST surface at /wp-json/gt/v1/. The same endpoints power the in-page table interactions; you can call them directly from a separate dashboard, a headless front-end, a mobile app, or a server-side automation.

This page documents what’s stable and which guarantees the version namespace gives you.

Authentication#

Every endpoint requires a WordPress-authenticated request. Three working auth flavors:

The default for in-page calls. WordPress sets the auth cookie on login; Gravity Tables embeds a gt_rest_nonce on every page that renders a table. Pass the nonce as the X-WP-Nonce header.

const response = await fetch('/wp-json/gt/v1/tables/42/entries', {
  headers: { 'X-WP-Nonce': window.gtRestNonce },
  credentials: 'same-origin',
});

Application Passwords (server-to-server)#

For backend automations or external dashboards. Generate an app password from Users → Your Profile → Application Passwords in WordPress admin.

curl -u 'wp-username:abcd efgh ijkl mnop qrst uvwx' \
  https://your-site.com/wp-json/gt/v1/tables/42/entries

JWT / OAuth (via plugin)#

Any standard WordPress JWT plugin (MiniOrange, JWT Authentication for WP REST API) issues bearer tokens that work transparently. Gravity Tables doesn’t ship its own auth layer, it consumes whatever the WordPress REST infrastructure considers authenticated.

The five endpoints#

GET /tables/{id}/entries#

Paginated list of entries the caller is allowed to see.

Path parameters:

  • id, Gravity Form id

Query parameters:

  • page (default 1), page number
  • per_page (default 25, max 500)
  • filter, server-side filter, same syntax as the shortcode (status:approved)
  • search, full-text search across visible columns
  • sort, comma-separated column:direction (created:desc,name:asc)
  • columns, comma-separated, restricts which columns come back (smaller payload)

Response shape:

{
  "data": [
    { "id": 1042, "created": "2026-04-12T18:30:11Z", "fields": { "name": "Sarah Chen", "status": "approved", "value": 4500 } },
    { "id": 1041, "created": "2026-04-12T17:55:02Z", "fields": { "name": "Marcus Lee", "status": "pending", "value": 0 } }
  ],
  "page": 1,
  "per_page": 25,
  "total": 318,
  "total_pages": 13,
  "applied_filters": { "status": "approved" },
  "permissions": {
    "can_edit": ["status", "notes"],
    "can_bulk": ["approve", "reject", "export"]
  }
}

The permissions block tells you what the caller can do, the same data the shortcode uses to decide which cells render as editable. Use it to mirror the same UI logic in a custom front-end.

POST /tables/{id}/entries/{entry_id}/edit#

Inline edit. Same GFAPI::update_entry_field() pipeline the shortcode triggers.

Body:

{
  "field": "status",
  "value": "approved"
}

Response, success:

{
  "ok": true,
  "entry_id": 1042,
  "field": "status",
  "old_value": "pending",
  "new_value": "approved",
  "audit_id": 8741
}

Response, validation failure:

{
  "ok": false,
  "code": "validation_failed",
  "field": "value",
  "message": "Value must be a positive number"
}

The validation pipeline is the same as a fresh form submission, conditional logic, calculated-field re-runs, custom validation hooks all fire. If the field has a Gravity Forms validation rule, the API enforces it.

POST /tables/{id}/bulk#

Run a bulk action across many entries.

Body:

{
  "action": "approve",
  "entry_ids": [1042, 1041, 1040, 1039],
  "args": {}
}

The args object is forwarded to the registered bulk action, useful for custom actions that take a parameter (e.g. assign with args: { user: "rep1" }).

Response:

{
  "ok": true,
  "action": "approve",
  "succeeded": 4,
  "failed": 0,
  "message": "Approved 4 entries",
  "audit_id": 8742
}

The audit log records this as a single audit_id covering all four entry ids, not four separate rows.

POST /tables/{id}/export#

Start an export job. Large exports run async; small ones return inline.

Body:

{
  "format": "xlsx",
  "columns": ["created", "name", "status", "value"],
  "filter": "status:approved",
  "search": ""
}

Response, small exports (< 5,000 rows by default):

{
  "ok": true,
  "ready": true,
  "download_url": "/wp-json/gt/v1/export-download/abc123def456...",
  "expires_at": "2026-04-12T19:30:00Z"
}

Response, large exports:

{
  "ok": true,
  "ready": false,
  "job_id": "exp_abc123",
  "status_url": "/wp-json/gt/v1/export-status/exp_abc123",
  "estimated_seconds": 8
}

Poll status_url until ready: true. The download_url is single-use and expires in 30 minutes.

GET /audit#

Read the audit log. Admin-only (manage_options capability).

Query parameters:

  • table_id, restrict to one form
  • entry_id, restrict to one entry
  • user_id, restrict to actions by one user
  • from / to, ISO date range
  • page / per_page

Response:

{
  "data": [
    {
      "audit_id": 8742,
      "type": "bulk_action",
      "action": "approve",
      "table_id": 42,
      "entry_ids": [1042, 1041, 1040, 1039],
      "user_id": 7,
      "user_login": "moderator-anna",
      "ip": "203.0.113.45",
      "timestamp": "2026-04-12T18:31:02Z"
    }
  ],
  "page": 1,
  "total": 1284
}

Permission rules the API enforces#

Before any endpoint returns data, the server runs through the same three-layer permission check the shortcode uses:

  1. Layer 1, view access via allowed_roles. Caller’s roles are matched against the table’s allow-list. A failure returns HTTP 403 with { "code": "view_denied" }.
  2. Layer 2, column-level edit access via allow_edit. The list of editable columns is computed per-caller and returned in the permissions.can_edit array on every list response.
  3. Layer 3, per-column role gate via edit_permissions. Each editable column may require a different role; an edit attempt to a column the caller doesn’t have the role for returns HTTP 403 with { "code": "field_edit_denied", "field": "value" }.

In addition, per-user scoping via filter_user_owns="..." is auto-applied to list responses if the table is configured for it. A subscriber user calling /entries sees only the rows where the configured column matches their user id.

Rate limits#

There are no hard rate limits applied by Gravity Tables itself, but the same WordPress request-per-second protections your hosting layer applies (Nginx, Cloudflare, WP Engine’s request throttling) apply here too.

For high-frequency polling (live dashboards, websocket bridges), use the auto_refresh shortcode option instead, it includes server-side ETag handling that returns 304 Not Modified when nothing has changed, making the polling cost effectively free.

Versioning#

The v1 namespace is locked. Compatible additions (new endpoints, new optional query parameters, new response fields) ship without a version bump. Breaking changes (renamed fields, removed endpoints, changed request shapes) trigger a v2 namespace alongside; v1 continues to work unchanged for at least 12 months after v2 ships.

The current shape:

StatusEndpointSince
StableGET /tables/{id}/entriesv3.0.0
StablePOST /tables/{id}/entries/{entry_id}/editv3.0.0
StablePOST /tables/{id}/bulkv3.2.0
StablePOST /tables/{id}/exportv4.0.0
StableGET /auditv4.0.3

Example: a headless dashboard#

async function loadDashboard() {
  const res = await fetch('/wp-json/gt/v1/tables/42/entries?filter=status:approved&per_page=100', {
    headers: { 'X-WP-Nonce': window.gtRestNonce },
    credentials: 'same-origin',
  });
  const { data, total, permissions } = await res.json();

  renderTable(data, { editable: permissions.can_edit });

  // If the user can run bulk actions, render the bulk toolbar
  if (permissions.can_bulk?.length > 0) {
    renderBulkBar(permissions.can_bulk);
  }
}

Gravity Tables itself does the same thing, but client-side code can fully replace the rendering layer when a custom design system requires it.

Errors#

All error responses follow the standard WordPress REST envelope:

{
  "code": "view_denied",
  "message": "You don't have permission to view this table.",
  "data": { "status": 403 }
}

Common codes: view_denied, field_edit_denied, validation_failed, invalid_action, entry_not_found, table_not_found.