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/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" }| Status | Meaning |
|---|---|
| 400 | Invalid query parameter (e.g. malformed updatedAt). |
| 401 | Missing, malformed, or revoked API key. |
| 500 | Unexpected server-side error. Safe to retry with exponential backoff. |
Transcripts
GET /v1/transcripts/v1/transcriptsLists 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/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
GET /v1/pages/v1/pagesLists 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/v1/notebooks/{notebookId}/pagesSame 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/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 /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