inq Data API
v1REST 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. Base URL:
https://api-demo.inq.liveResponse shape
All list endpoints return a uniform envelope:
{
"items": [ /* endpoint-specific records */ ],
"hasMore": false,
"nextCursor": "<opaque cursor>"
}A response holds at most limit items. hasMore tells you whether more records are waiting after it. nextCursor is an opaque resume token returned on every response, including the last one — it is null only when nothing has ever matched and you sent neither cursor nor updatedAt. Don't parse it.
Delta sync
List endpoints are delta feeds ordered by change time. To keep a local copy in sync:
- First request: send no
cursor. Optionally passupdatedAtto start from a point in time instead of the beginning. - Store the last non-null
nextCursorand send it ascursoron every following request. Don't build the next request from item timestamps. hasMore: true— request again immediately with the new cursor.hasMore: false— you are caught up; poll again later with that same cursor.- Apply items as upserts keyed by
id— a record that changed again may be delivered more than once, and a caught-up poll can re-deliver the last few seconds of changes so that writes landing at the same moment are never missed.
# First request (optionally &updatedAt=2026-06-01T00:00:00Z)
curl -H "Authorization: Bearer $INQ_KEY" \
"https://api-demo.inq.live/v1/notebooks?limit=200"
# Every following request: pass the last nextCursor back
curl -H "Authorization: Bearer $INQ_KEY" \
"https://api-demo.inq.live/v1/notebooks?limit=200&cursor=$NEXT_CURSOR"Deletions (tombstones)
/v1/notebooks and /v1/recordings report deletions in the same feed. Every item carries a deleted flag: live records have "deleted": false plus all their fields, and a deleted record arrives as a tombstone with only its id:
{ "id": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976", "deleted": true }Remove the record from your copy when you see a tombstone. Tombstones are kept for about 90 days, then purged. If your client has not synced for longer than that, it may have missed deletions: discard the stored cursor, do a full resync (no cursor, no updatedAt), and drop local records that did not come back.
Common query parameters
| Parameter | Description |
|---|---|
| cursor | Opaque resume token — the nextCursor of your previous response. Send it on every request after the first. Takes precedence over updatedAt. |
| updatedAt | ISO-8601 datetime (any UTC offset, normalized to UTC). For the first request only: starts the feed at this point in time instead of the beginning. Afterwards, use cursor. |
| limit | Max 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)
The notebook and page endpoints (/v1/notebooks, /v1/notebooks/{notebookId}, /v1/notebooks/{notebookId}/pages and /v1/notebooks/{notebookId}/pages/{pageAddress}) return 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.
/v1/recordings deliberately sends no ETag: every response contains freshly signed S3 URLs that expire, so a cached body could hand you a dead link. It always returns a fresh 200.
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-demo.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-demo.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. A 304 has no body, so keep using the cursor you sent.
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 the following headers. They are exposed via CORS, so browser clients can read them too.
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum requests allowed in the current window. |
| X-RateLimit-Remaining | Requests remaining in the current window. |
| X-RateLimit-Reset | Unix epoch (seconds) when the current window resets. |
| Retry-After | Seconds 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-Matchwhere available (not on/v1/recordings) — a 304 still counts toward the limit, but stays cheap on bandwidth and downstream processing. - On 429, sleep
Retry-Afterseconds and retry. Do not tight-loop. - Watch
X-RateLimit-Remainingin 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" }| Status | Meaning |
|---|---|
| 400 | Invalid query parameter (e.g. malformed updatedAt, cursor, or limit). |
| 401 | Missing, malformed, or revoked API key. |
| 429 | Rate limit exceeded. See the Rate limits section. The response includes Retry-After (in seconds) and X-RateLimit-* headers. |
| 500 | Unexpected server-side error. Safe to retry with exponential backoff. |
Recordings
GET /v1/recordings/v1/recordingsLists 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). Paginated with cursor / updatedAt / limit, and deletions arrive as tombstones — see Delta sync. No ETag: every call returns a fresh 200.
Response item
{
"id": "rec_01HXYZ...",
"deleted": false,
"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
}Deleted recording
{ "id": "rec_01HXYZ...", "deleted": true }Note: durationMs is in milliseconds, not seconds. transcript, summary, and diarization are populated only if the user requested AI processing.
signedUrl expires after 60 minutes — don't store it; request the recordings again to get a fresh one. It is null when the audio file can't be served (e.g. some recordings migrated from older storage), so a live recording may have no playable URL.
Example
curl -H "Authorization: Bearer $INQ_KEY" \
"https://api-demo.inq.live/v1/recordings?cursor=$NEXT_CURSOR"Notebooks
GET /v1/notebooks/v1/notebooksLists notebooks (the physical inq paper notebooks) bound to the user. Metadata only — no page content here. Paginated with cursor / updatedAt / limit, and deletions arrive as tombstones — see Delta sync.
Response item
{
"id": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976",
"deleted": false,
"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"
}Deleted notebook
{ "id": "31f6e055-d0eb-48d9-a7a9-e2c3a596f976", "deleted": true }Example
curl -H "Authorization: Bearer $INQ_KEY" \
"https://api-demo.inq.live/v1/notebooks?cursor=$NEXT_CURSOR"Notebook by ID
GET /v1/notebooks/{notebookId}/v1/notebooks/{notebookId}Fetch a single notebook by id. Returns the same fields as a live list item, unwrapped (no envelope, no deleted flag). 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-demo.inq.live/v1/notebooks/31f6e055-d0eb-48d9-a7a9-e2c3a596f976Pages by notebook
GET /v1/notebooks/{notebookId}/pages/v1/notebooks/{notebookId}/pagesLists the pages of a single notebook, using the uniform { items, hasMore, nextCursor } list envelope, with each item's transcription content inlined. Same cursor / updatedAt / limit pagination as the other list endpoints, and an ETag for conditional requests. A page counts as changed when its handwriting or its transcript changes, so a page transcribed after your last poll comes back in the next delta. Returns 404 Not Found if the notebook doesn't exist, was deleted or isn't yours (all three look the same, so others' notebooks stay invisible).
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-demo.inq.live/v1/notebooks/31f6e055-d0eb-48d9-a7a9-e2c3a596f976/pagesPage by address
GET /v1/notebooks/{notebookId}/pages/{pageAddress}/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, its notebook was deleted, or it 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-demo.inq.live/v1/notebooks/31f6e055-d0eb-48d9-a7a9-e2c3a596f976/pages/2427721724661766