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. Production base URL:
https://api.inq.liveStaging 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
| Parameter | Description |
|---|---|
| updatedAt | ISO datetime. Returns only records updated strictly after this timestamp. Omit to fetch everything from the beginning. |
| 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)
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:
| 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-Match— 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). |
| 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).
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/recordingsNotebooks
GET /v1/notebooks/v1/notebooksLists 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/notebooksNotebook by ID
GET /v1/notebooks/{notebookId}/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-e2c3a596f976Pages by notebook
GET /v1/notebooks/{notebookId}/pages/v1/notebooks/{notebookId}/pagesLists 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/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 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