Inq Data API

v1

REST API for pulling handwritten notebook content, audio recordings, and structured transcripts captured by Inq smart pens. Single-user scope: each API key authenticates one end user accessing their own data.

Overview

All endpoints are GET requests returning application/json. Production base URL:

https://api.inq.live

Staging environments use https://api-{stage}.inq.live (e.g. api-dev.inq.live).

Response shape

All list endpoints return a uniform envelope:

{
  "items": [ /* endpoint-specific records */ ],
  "hasMore": false
}

hasMore indicates whether more records exist past the current page. Pagination is delta-based via the updatedAt query parameter — pass the latest updatedAt you have seen to fetch only newer records.

Common query parameters

ParameterDescription
updatedAtISO datetime. Returns only records updated strictly after this timestamp. Omit to fetch everything from the beginning.
limitMax records per response. Default 50, max 200.

Authentication

Every request must include your API key in the Authorization header as a Bearer token:

Authorization: Bearer inq_live_<your-key>

Create and manage keys in the Developer Portal. Keys are shown in full only once at creation — store them securely. Revoked keys are rejected immediately.

Requests with a missing, malformed, or revoked key return 401 Unauthorized.

Caching (ETag / 304)

Every list response includes a strong ETag derived from the underlying record identity (record IDs + updatedAt + hasMore). It is stable across requests as long as the data has not changed, even though signed S3 URLs in the body are regenerated every call.

Send the previous ETag back in If-None-Match to skip the payload when nothing has changed:

# First request — receive ETag in the response
curl -i -H "Authorization: Bearer $INQ_KEY" \
  https://api.inq.live/v1/transcripts

HTTP/2 200
etag: "5a449cd23d1a2d8db21a1dc749f01c14"
content-type: application/json
...

# Second request — pass the ETag back, get a 304 with no body
curl -i \
  -H "Authorization: Bearer $INQ_KEY" \
  -H 'If-None-Match: "5a449cd23d1a2d8db21a1dc749f01c14"' \
  https://api.inq.live/v1/transcripts

HTTP/2 304
etag: "5a449cd23d1a2d8db21a1dc749f01c14"

Use ETag caching for polling — typical delta-sync clients can poll every few minutes and pay near-zero bandwidth on unchanged data.

Errors

Errors are returned as JSON with an error field:

{ "error": "Invalid API key" }
StatusMeaning
400Invalid query parameter (e.g. malformed updatedAt).
401Missing, malformed, or revoked API key.
500Unexpected server-side error. Safe to retry with exponential backoff.

Transcripts

GET /v1/transcripts
GET/v1/transcripts

Lists structured page-level transcripts. Each item is a signed S3 URL to a JSON file containing the typed/recognized content for one notebook page (handwriting recognition output, structured into content regions).

Response item

{
  "signedUrl": "https://...amazonaws.com/external/transcripts/...@2026-06-10.json?X-Amz-...",
  "notebookId": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976",
  "pageAddress": "2427721724661766",
  "updatedAt": "2026-06-10T10:30:00.000Z",
  "contentType": "application/json",
  "size": 4821
}

Fetch signedUrl directly to read the transcript JSON. Signed URLs are valid for 60 minutes. The cache file is keyed on updatedAt, so if a page is re-transcribed the URL changes and the old file expires within 30 days.

Example

curl -H "Authorization: Bearer $INQ_KEY" \
  "https://api.inq.live/v1/transcripts?updatedAt=2026-06-01T00:00:00Z"

Recordings

GET /v1/recordings
GET/v1/recordings

Lists audio recordings captured alongside handwriting sessions. Each item includes metadata plus a signed URL to the source audio file (M4A on iOS, WAV on Android).

Response item

{
  "id": "rec_01HXYZ...",
  "name": "Morning standup",
  "description": null,
  "startedAt": "2026-06-10T09:00:00.000Z",
  "endedAt":   "2026-06-10T09:23:14.000Z",
  "durationMs": 1394000,
  "pauseTimestamps": [],
  "createdAt": "2026-06-10T09:00:00.000Z",
  "updatedAt": "2026-06-10T09:23:30.000Z",
  "signedUrl": "https://...amazonaws.com/recordings/...m4a?X-Amz-...",
  "contentType": "audio/mp4",
  "size": 2845611,
  "transcript": null,
  "summary": null,
  "diarization": null
}

Note: durationMs is in milliseconds, not seconds. transcript, summary, and diarization are populated only if the user requested AI processing.

Example

curl -H "Authorization: Bearer $INQ_KEY" \
  https://api.inq.live/v1/recordings

Notebooks

GET /v1/notebooks
GET/v1/notebooks

Lists notebooks (the physical Inq paper notebooks) bound to the user. Metadata only — no page content here.

Response item

{
  "id": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976",
  "name": "Engineering journal",
  "coverColor": "#1F2937",
  "size": "A5",
  "binding": "spiral",
  "volume": "lined",
  "notebookTypeId": "type_classic_a5",
  "archived": false,
  "numberOfPages": 80,
  "lastEditedAt": "2026-06-15T18:22:00.000Z",
  "createdAt": "2026-04-01T08:00:00.000Z",
  "updatedAt": "2026-06-15T18:22:00.000Z"
}

Example

curl -H "Authorization: Bearer $INQ_KEY" \
  https://api.inq.live/v1/notebooks

Notebook by ID

GET /v1/notebooks/{notebookId}
GET/v1/notebooks/{notebookId}

Fetch a single notebook by id. Returns the same record shape as the list endpoint but unwrapped (no items / hasMore envelope). Returns 404 Not Found if the notebook doesn't exist or isn't owned by the authenticated user — we deliberately don't distinguish so we don't leak existence of other users' notebooks.

Response

{
  "id": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976",
  "name": "Engineering journal",
  "coverColor": "#1F2937",
  "size": "A5",
  "binding": "spiral",
  "volume": "lined",
  "notebookTypeId": "type_classic_a5",
  "archived": false,
  "numberOfPages": 80,
  "lastEditedAt": "2026-06-15T18:22:00.000Z",
  "createdAt": "2026-04-01T08:00:00.000Z",
  "updatedAt": "2026-06-15T18:22:00.000Z"
}

Example

curl -H "Authorization: Bearer $INQ_KEY" \
  https://api.inq.live/v1/notebooks/31f6e055-d0eb-48d9-a7a9-e2c3a596f976

Pages

GET /v1/pages
GET/v1/pages

Lists individual notebook pages with stroke statistics and an optional join to the page transcript. One row per (notebookId, pageAddress), aggregated across all sync-session records for that page.

Response item

{
  "notebookId": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976",
  "pageAddress": "2427721724661766",
  "penId": "pen_98765",
  "syncedAt": "2026-06-15T18:22:00.000Z",
  "createdAt": "2026-06-10T09:00:00.000Z",
  "updatedAt": "2026-06-15T18:22:00.000Z",
  "strokeCount": 142,
  "pointCount": 18347,
  "firstStrokeAt": "2026-06-10T09:02:11.000Z",
  "lastStrokeAt":  "2026-06-15T18:21:50.000Z",
  "activeWritingMs": 873400,
  "hasTranscript": true,
  "transcriptUpdatedAt": "2026-06-15T18:25:00.000Z",
  "transcriptSignedUrl": "https://...amazonaws.com/external/transcripts/...json?X-Amz-..."
}

activeWritingMs is the sum of active writing sessions in milliseconds — gaps longer than 5 minutes are treated as breaks and excluded. Useful for time-on-task analytics.

When hasTranscript is true, transcriptSignedUrl points directly to the transcript JSON file (same content as /v1/transcripts), so a single /v1/pages call gives you everything you need per page.

Example

curl -H "Authorization: Bearer $INQ_KEY" \
  "https://api.inq.live/v1/pages?updatedAt=2026-06-01T00:00:00Z&limit=100"

Pages by notebook

GET /v1/notebooks/{notebookId}/pages
GET/v1/notebooks/{notebookId}/pages

Same envelope and item shape as /v1/pages, but scoped to a single notebook and with each item's transcription content inlined (the global /v1/pages endpoint omits it to keep the envelope lean — see Pages). Same updatedAt + limit pagination. Returns an empty list if the notebook doesn't exist or isn't owned by you (no 404 — we don't distinguish to avoid leaking existence of others' notebooks).

Why inline here? The caller has scoped the request to one notebook (bounded by limit, default 50), so we save round-trips by including content directly. Consumers doing notebook-level sync usually want both metadata and text in one shot.

Example

curl -H "Authorization: Bearer $INQ_KEY" \
  https://api.inq.live/v1/notebooks/31f6e055-d0eb-48d9-a7a9-e2c3a596f976/pages

Page by address

GET /v1/notebooks/{notebookId}/pages/{pageAddress}
GET/v1/notebooks/{notebookId}/pages/{pageAddress}

Fetch a single page by notebookId + pageAddress. Returns the same item shape as /v1/pages (unwrapped — no envelope) plusan extra transcription field with the full transcript content inlined. Returns 404 Not Found if the page doesn't exist or isn't owned by the authenticated user.

Why inline transcription here? Single-page calls mean "give me everything about this one page" — including text content. Saves a follow-up signed-URL fetch and avoids the 60-min URL expiry. List endpoints (/v1/pages, /v1/notebooks/{id}/pages) intentionally omit it to keep responses lean.

Response (extra fields shown)

{
  "notebookId": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976",
  "pageAddress": "2427721724661766",
  /* ...all standard page fields... */
  "hasTranscript": true,
  "transcriptUpdatedAt": "2026-06-15T18:25:00.000Z",
  "transcription": {
    "lastEditedAt": "2026-06-15T18:25:00.000Z",
    "contentRegions": [
      {
        "contentType": "WRITING",
        "exports": [
          { "dataFormat": "MARKDOWN", "exportData": "# Daily standup\n- ..." }
        ]
      }
    ]
  }
}

transcription is omitted when hasTranscript is false. Identity fields (notebookId, pageAddress, updatedAt) aren't repeated inside — they're already on the parent page. transcriptSignedUrl is also omitted on this endpoint since the content is inline (would be redundant); see the global /v1/pages endpoint for the URL-based shape.

Example

curl -H "Authorization: Bearer $INQ_KEY" \
  https://api.inq.live/v1/notebooks/31f6e055-d0eb-48d9-a7a9-e2c3a596f976/pages/2427721724661766