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/notebooks

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/notebooks

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.

Rate limits

Each API key is allowed 25 requests per minute across all endpoints, using a fixed 60-second window aligned to UTC minute boundaries. Requests beyond the limit return 429 Too Many Requests.

Response headers

Every response (both success and 429) carries:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUnix epoch (seconds) when the current window resets.
Retry-AfterSeconds to wait before retrying. Only on 429 responses.

429 response

HTTP/2 429
retry-after: 37
x-ratelimit-limit: 25
x-ratelimit-remaining: 0
x-ratelimit-reset: 1782563700
content-type: application/json

{ "error": "Rate limit exceeded" }

Best practices

  • Combine polling with ETag / If-None-Match — a 304 still counts toward the limit, but stays cheap on bandwidth and downstream processing.
  • On 429, sleep Retry-After seconds and retry. Do not tight-loop.
  • Watch X-RateLimit-Remaining in success responses and pace yourself before you hit zero.
  • Need a higher limit? Contact info@inq.dev.

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.
429Rate limit exceeded. See the Rate limits section. The response includes Retry-After (in seconds) and X-RateLimit-* headers.
500Unexpected server-side error. Safe to retry with exponential backoff.

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 by notebook

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

Lists the pages of a single notebook, using the uniform { items, hasMore } list envelope, with each item's transcription content inlined. Same updatedAt + limit pagination as the other list endpoints. 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).

Response item

{
  "notebookId": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976",
  "pageAddress": "2427721724661766",
  "syncedAt": "2026-06-15T18:22:00.000Z",
  "createdAt": "2026-06-10T09:00:00.000Z",
  "updatedAt": "2026-06-15T18:22:00.000Z",
  "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.

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 Pages by notebook (unwrapped — no envelope), with the full transcription 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.

Response

{
  "notebookId": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976",
  "pageAddress": "2427721724661766",
  "syncedAt": "2026-06-15T18:22:00.000Z",
  "createdAt": "2026-06-10T09:00:00.000Z",
  "updatedAt": "2026-06-15T18:22:00.000Z",
  "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.

Example

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