Gravi API · v1

Build on your Gravi pages

A JSON API for everything the editor does: create pages, add and arrange blocks, read form responses and analytics. Keys belong to a project on the Pro plan.

Base URL
https://api.gravi.page/v1
OpenAPI 3.1 document
https://api.gravi.page/v1/openapi.json
On this page

Getting started

  1. Create a key in your project's Settings → API. It is shown once, so copy it somewhere safe.
  2. Send it as a bearer token. Every request returns JSON:
    curl "https://api.gravi.page/v1/project" \
      -H "Authorization: Bearer $GRAVI_API_KEY"
  3. Add a block to a page. Without a position it takes the first free spot, and open editors update live:
    curl -X POST "https://api.gravi.page/v1/pages/$PAGE_ID/blocks" \
      -H "Authorization: Bearer $GRAVI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"type":"text","markdown":"Hello from the **API**"}'

The reference below lists every endpoint with its parameters and responses.

Authentication

Send the key in the Authorization header: Authorization: Bearer gravi_…. A key acts as the owner who created it and only reaches its own project. Keys never belong in a browser or a public repository; the API does not allow cross-origin calls.

A key stops working when it is revoked or expires, when its creator is no longer an owner, or when the project leaves the Pro plan (403 PLAN_REQUIRED; the key is kept).

Scopes

Each key has scopes; a call without the right one gets 403 FORBIDDEN.

  • content:readRead the project, pages and blocks.
  • content:writeCreate and change pages, blocks, header, theme, files and block passwords.
  • responses:readRead forms and their responses.
  • responses:writeChange and delete responses. Responses contain personal data, so this scope is off in the presets.
  • analytics:readRead page analytics.

Rate limits

Per key: 120 requests a minute (bursts up to 30), of which 30 may change data. Over the limit the API answers 429 with a Retry-After header in seconds.

JSON bodies up to 256 KB; images up to your plan's upload size.

Errors

Errors share one envelope. Branch on code, not on the message:

{
  "error": {
    "code": "VALIDATION",
    "message": "The request is not valid.",
    "details": { "field": "slug" }
  }
}
StatusCodes
401UNAUTHENTICATEDACCOUNT_BLOCKEDACCOUNT_DELETED
403FORBIDDENPLAN_REQUIREDLIMIT_REACHED
404NOT_FOUND
409CONFLICTSLUG_TAKENSLUG_RESERVED
422SLUG_INVALIDVALIDATION
429RATE_LIMITED
502FETCH_FAILED

Pagination

Lists return { data, nextCursor }. Pass limit (1–100, default 50) and the nextCursor of the previous page as cursor until it is null.

Reference

Generated from the OpenAPI document.

GET/project

Get the key's project

Scope: Any key

Name, plan and limits, usage, public URLs and the block types with their sizes.

Response

200Project

curl "https://api.gravi.page/v1/project" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
GET/pages

List pages

Scope: content:read

Query parameters

  • limitinteger1–100 · default 50
  • cursorstring≤ 200 chars

Response

200PageList

curl "https://api.gravi.page/v1/pages" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
POST/pages

Create a page

Scope: content:write

Creates a published page from an optional template.

Request body

  • slugstringrequired1–200 chars
  • template"blank" | "creator" | "developer" | "business"
  • titlestring≤ 80 chars

Response

201Page

curl -X POST "https://api.gravi.page/v1/pages" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"jane","title":"Jane Doe"}'
GET/pages/{pageId}

Get a page with its blocks

Scope: content:read

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Response

200Page

curl "https://api.gravi.page/v1/pages/{pageId}" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
PATCH/pages/{pageId}

Update a page's address, status, SEO or language

Scope: content:write

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Request body

  • slugstring1–200 chars
  • status"published" | "unpublished"
  • seoobject
    • titlestring or null≤ 70 chars
    • descriptionstring or null≤ 200 chars
    • noindexboolean
  • locale"en" | "ru"

Response

200Page

curl -X PATCH "https://api.gravi.page/v1/pages/{pageId}" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"published","seo":{"title":"Jane Doe — designer"}}'
DELETE/pages/{pageId}

Delete a page

Scope: content:write

Soft delete: the page can be restored from the dashboard for 30 days.

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Response

204No content

curl -X DELETE "https://api.gravi.page/v1/pages/{pageId}" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
PATCH/pages/{pageId}/header

Update the page header

Scope: content:write

bio takes Markdown; avatarFileId: null removes the avatar.

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Request body

  • titlestring≤ 80 chars
  • biostring≤ 40000 chars
  • bioJsonany
  • avatarFileIdstring or null1–64 chars

Response

200Page

curl -X PATCH "https://api.gravi.page/v1/pages/{pageId}/header" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Jane Doe","bio":"Designer in **Lisbon**. Say hi at [jane.dev](https://jane.dev)."}'
PUT/pages/{pageId}/theme

Set the page theme

Scope: content:write

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Request body

  • presetstringrequired1–64 chars
  • overridesmap of any

Response

200Page

curl -X PUT "https://api.gravi.page/v1/pages/{pageId}/theme" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"preset":"paper"}'
GET/pages/{pageId}/blocks

List blocks

Scope: content:read

In desktop reading order.

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Query parameters

  • limitinteger1–100 · default 50
  • cursorstring≤ 200 chars

Response

200BlockList

curl "https://api.gravi.page/v1/pages/{pageId}/blocks" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
POST/pages/{pageId}/blocks

Add a block

Scope: content:write

Shorthands: url (link, video), markdown (text, article), imageUrl / fileId (image). Without position the block takes the first free spot. An existing id returns the stored block with 200 (idempotent retries).

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Request body

  • idstring
  • typestringrequired1–32 chars
  • paramsmap of any
  • urlstring1–2048 chars
  • markdownstring≤ 40000 chars
  • imageUrlstring1–2048 chars
  • fileIdstring1–64 chars
  • size"icon" | "compact" | "small" | "banner" | "wide" | "large" | "auto" | "full" | "full_auto" | object
  • position"end" | "start" | "auto" | object

Response

201Block

curl -X POST "https://api.gravi.page/v1/pages/{pageId}/blocks" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"link","url":"https://github.com/jane","size":"banner"}'
PUT/pages/{pageId}/blocks/order

Reorder blocks

Scope: content:write

Each list names every block once; blocks re-flow in that order, keeping sizes.

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Request body

  • desktoparray of string≤ 1000 items
  • mobilearray of string≤ 1000 items

Response

200BlockOrderResult

curl -X PUT "https://api.gravi.page/v1/pages/{pageId}/blocks/order" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"desktop":["intro","github","photo"]}'
PATCH/pages/{pageId}/blocks/{blockId}

Update a block

Scope: content:write

params is partial (null removes a key); markdown replaces text content.

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

  • blockIdstringrequired

    Block id (id of a block).

Request body

  • paramsmap of any
  • markdownstring≤ 40000 chars
  • size"icon" | "compact" | "small" | "banner" | "wide" | "large" | "auto" | "full" | "full_auto" | object

Response

200Block

curl -X PATCH "https://api.gravi.page/v1/pages/{pageId}/blocks/{blockId}" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown":"Hello **world**","size":"wide"}'
DELETE/pages/{pageId}/blocks/{blockId}

Delete a block

Scope: content:write

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

  • blockIdstringrequired

    Block id (id of a block).

Response

204No content

curl -X DELETE "https://api.gravi.page/v1/pages/{pageId}/blocks/{blockId}" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
PUT/pages/{pageId}/blocks/{blockId}/lock

Protect a block with a password

Scope: content:write

A new password (+ hint), or lockId of another locked block on the page to share its password. Visitors see a placeholder until they unlock it; the content never reaches them.

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

  • blockIdstringrequired

    Block id (id of a block).

Request body

One of:

  • passwordstringrequired4–128 chars
  • hintstring≤ 80 chars
  • lockIdstringrequired1–64 chars

Response

200Block

curl -X PUT "https://api.gravi.page/v1/pages/{pageId}/blocks/{blockId}/lock" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"password":"open-sesame","hint":"The magic words"}'
DELETE/pages/{pageId}/blocks/{blockId}/lock

Remove a block's password

Scope: content:write

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

  • blockIdstringrequired

    Block id (id of a block).

Response

204No content

curl -X DELETE "https://api.gravi.page/v1/pages/{pageId}/blocks/{blockId}/lock" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
POST/files

Upload an image

Scope: content:write

multipart/form-data with a file field (up to the plan's upload size), or JSON { url } to import an image. Unused files are removed after 24 hours.

Request body

Also accepts multipart/form-data with a file field.

  • urlstringrequired1–2048 chars

Response

201File

curl -X POST "https://api.gravi.page/v1/files" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/photo.jpg"}'
GET/forms

List forms

Scope: responses:read

Newest response first. Forms whose block was deleted keep their responses.

Query parameters

  • limitinteger1–100 · default 50
  • cursorstring≤ 200 chars

Response

200FormList

curl "https://api.gravi.page/v1/forms" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
GET/forms/{formId}/responses

List a form's responses

Scope: responses:read

Newest first (with q: by relevance). Without status, spam is left out. from / to bound the submission time (to exclusive).

Path parameters

  • formIdstringrequired

Query parameters

  • limitinteger1–100 · default 50
  • cursorstring≤ 2000 chars
  • status"new" | "in_progress" | "done" | "spam"
  • fromstring (date-time)
  • tostring (date-time)
  • qstring≤ 200 chars

Response

200FormResponseList

curl "https://api.gravi.page/v1/forms/{formId}/responses" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
PATCH/responses/{responseId}

Update a response

Scope: responses:write

Status, read flag and the internal note (an empty note removes it).

Path parameters

  • responseIdstringrequired

Request body

  • status"new" | "in_progress" | "done" | "spam"
  • notestring≤ 2000 chars
  • readboolean

Response

200FormResponse

curl -X PATCH "https://api.gravi.page/v1/responses/{responseId}" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"done","read":true,"note":"Replied by email"}'
DELETE/responses/{responseId}

Delete a response

Scope: responses:write

Path parameters

  • responseIdstringrequired

Response

204No content

curl -X DELETE "https://api.gravi.page/v1/responses/{responseId}" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
POST/forms/{formId}/responses/bulk

Change or delete many responses

Scope: responses:write

Up to 100 ids, or every response matching filter (the first 200 now, the rest in the background: pending: true).

Path parameters

  • formIdstringrequired

Request body

  • idsarray of string1–100 items
  • filterobject
    • status"new" | "in_progress" | "done" | "spam"
    • fromstring (date-time)
    • tostring (date-time)
    • qstring≤ 200 chars
  • action"setStatus" | "markRead" | "markUnread" | "delete"required
  • status"new" | "in_progress" | "done" | "spam"

Response

200BulkResult

curl -X POST "https://api.gravi.page/v1/forms/{formId}/responses/bulk" \
  -H "Authorization: Bearer $GRAVI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filter":{"status":"new"},"action":"markRead"}'
GET/pages/{pageId}/analytics

Get page analytics

Scope: analytics:read

Totals, a daily series and the most clicked blocks and links for UTC days from–to (default: the last 30 days). Days before the plan's history are clamped (clamped: true).

Path parameters

  • pageIdstringrequired

    Page id (from GET /v1/pages).

Query parameters

  • fromstring (date)
  • tostring (date)

Response

200Analytics

curl "https://api.gravi.page/v1/pages/{pageId}/analytics" \
  -H "Authorization: Bearer $GRAVI_API_KEY"
GET/openapi.json

This OpenAPI document

Scope: No key

Response

200application/json

curl "https://api.gravi.page/v1/openapi.json"

Schemas

Error

Every error uses this envelope.

  • errorobjectrequired
    • codestringrequired

      Stable code to branch on, e.g. VALIDATION

    • messagestringrequired
    • detailsmap of any

Project

  • idstringrequired
  • namestringrequired
  • planstringrequired
  • scopesarray of stringrequired

    Scopes of the calling key

  • limitsmap of number | booleanrequired
  • usageobjectrequired
    • pagesintegerrequired≥ 0
    • publishedPagesintegerrequired≥ 0
    • storageBytesintegerrequired≥ 0
  • urlsobjectrequired
    • dashboardstringrequired
    • pagesarray of stringrequired
  • blockTypesarray of objectrequired
    • typestringrequired
    • sizesarray of stringrequired
    • defaultSizestringrequired

      A size key such as compact or wide (see blockTypes)

    • entitlementstring

PageSummary

  • idstringrequired
  • slugstringrequired
  • urlstringrequired
  • status"published" | "unpublished"required
  • unpublishedReasonstring or nullrequired
  • titlestringrequired
  • locale"en" | "ru"required
  • blockCountintegerrequired≥ 0
  • versionintegerrequired≥ 0
  • createdAtstring (date-time) or nullrequired
  • updatedAtstring (date-time) or nullrequired
  • publishedAtstring (date-time) or nullrequired

PageList

  • dataarray of PageSummaryrequired
  • nextCursorstring or nullrequired

Page

  • idstringrequired
  • slugstringrequired
  • urlstringrequired
  • status"published" | "unpublished"required
  • unpublishedReasonstring or nullrequired
  • titlestringrequired
  • locale"en" | "ru"required
  • blockCountintegerrequired≥ 0
  • versionintegerrequired≥ 0
  • createdAtstring (date-time) or nullrequired
  • updatedAtstring (date-time) or nullrequired
  • publishedAtstring (date-time) or nullrequired
  • seoobjectrequired
    • titlestring or nullrequired
    • descriptionstring or nullrequired
    • noindexbooleanrequired
  • headerobjectrequired
    • titlestringrequired
    • bioanyrequired

      Rich text JSON (bio profile) or null

    • bioMarkdownstringrequired
    • avatarobject or nullrequired
      • fileIdstringrequired
      • urlstringrequired
      • widthnumber
      • heightnumber
  • thememap of anyrequired
  • blocksarray of Block

Block

  • idstringrequired
  • typestringrequired
  • paramsmap of anyrequired

    Stored block parameters

  • markdownstring

    text / article: the content as Markdown

  • sizeobjectrequired
    • desktopstring or nullrequired
    • mobilestring or nullrequired
  • layoutobjectrequired
    • desktopobjectrequired
      • xintegerrequired≥ 0
      • yintegerrequired≥ 0
      • wintegerrequired≥ 0
      • hintegerrequired≥ 0
    • mobileobjectrequired
      • xintegerrequired≥ 0
      • yintegerrequired≥ 0
      • wintegerrequired≥ 0
      • hintegerrequired≥ 0
  • lockedbooleanrequired
  • lockIdstring or nullrequired

    The block's password lock, if any

  • filesmap of objectrequired

    Signed URLs of the files it uses

  • updatedAtstring (date-time) or nullrequired
  • metadata"pending"

    POST: link metadata is being fetched in the background

BlockList

  • dataarray of Blockrequired
  • nextCursorstring or nullrequired

BlockOrderResult

  • dataarray of Blockrequired

File

  • fileIdstringrequired
  • urlstringrequired

Form

  • idstringrequired
  • titlestring or nullrequired
  • pageIdstringrequired
  • pageSlugstringrequired
  • blockIdstringrequired
  • acceptingbooleanrequired
  • responseCountintegerrequired≥ 0

    Every status but spam

  • unreadCountintegerrequired≥ 0
  • statusCountsobjectrequired
    • newintegerrequired≥ 0
    • in_progressintegerrequired≥ 0
    • doneintegerrequired≥ 0
    • spamintegerrequired≥ 0
  • columnsarray of objectrequired
    • fieldIdstringrequired
    • labelstringrequired
    • type"short_text" | "long_text" | "email" | "phone" | "number" | "url" | "choice" | "date" | "time" | "rating" | "consent"required
    • removedbooleanrequired
  • lastResponseAtstring (date-time) or nullrequired
  • blockRemovedAtstring (date-time) or nullrequired

    Set once the form block was deleted

  • createdAtstring (date-time)required

    ISO 8601, UTC

FormList

  • dataarray of Formrequired
  • nextCursorstring or nullrequired

FormResponse

  • idstringrequired
  • formIdstringrequired
  • status"new" | "in_progress" | "done" | "spam"required
  • readbooleanrequired
  • notestring or nullrequired
  • answersarray of objectrequired
    • fieldIdstringrequired
    • labelstringrequired
    • type"short_text" | "long_text" | "email" | "phone" | "number" | "url" | "choice" | "date" | "time" | "rating" | "consent"required
    • valuestring | array of string | number | booleanrequired
  • metaobjectrequired
    • referrerHoststring
    • utmmap of string
    • device"mobile" | "tablet" | "desktop"required
    • locale"en" | "ru"required
  • createdAtstring (date-time)required

    ISO 8601, UTC

  • updatedAtstring (date-time)required

    ISO 8601, UTC

FormResponseList

  • dataarray of FormResponserequired
  • nextCursorstring or nullrequired

BulkResult

  • affectedintegerrequired≥ 0

    Responses changed or deleted by this call

  • pendingbooleanrequired

    A filter's remaining matches continue in the background

Analytics

  • pageIdstringrequired
  • fromstring (date)required

    First UTC day, after clamping

  • tostring (date)required

    Last UTC day (inclusive)

  • clampedbooleanrequired

    from was moved to the plan's first visible day

  • analyticsDaysintegerrequired≥ 0

    Days of history the plan shows

  • totalsobjectrequired
    • viewsintegerrequired≥ 0
    • uniquesintegerrequired≥ 0
    • clicksintegerrequired≥ 0
  • dailyarray of objectrequired
    • datestring (date)required
    • viewsintegerrequired≥ 0
    • uniquesintegerrequired≥ 0
    • clicksintegerrequired≥ 0
  • topBlocksarray of objectrequired
    • blockIdstringrequired
    • typestring or nullrequired
    • labelstringrequired
    • clicksintegerrequired≥ 0
  • headerClicksintegerrequired≥ 0
  • linksarray of objectrequired

    Clicked URLs (top 20, the rest in other)

    • keystringrequired
    • countintegerrequired≥ 0
  • referrersarray of objectrequired
    • keystringrequired
    • countintegerrequired≥ 0
  • countriesarray of objectrequired
    • keystringrequired
    • countintegerrequired≥ 0
  • devicesarray of objectrequired
    • keystringrequired
    • countintegerrequired≥ 0