FileLoveNote / build packageKit revision · 28 October 2026

11 / 15 · API Contracts

Requests, responses and safe retries

Build the browser and server against concrete contracts. Preserve local work until a matching result confirms what happened.

Existing wire contract versus required rebuild behavior.

Endpoints and examples below describe the implementation inspected on 26 October 2026. Blocks marked Required rebuild define improvements to implement and verify, not behavior already proven in the current app. JSON examples use fictional records and opaque placeholder IDs; they contain no credentials.

1 · Transport, identity and validation

Use same-origin HTTPS in production. JSON request bodies use Content-Type: application/json; request properties are at the top level. The internal parser’s body.data is not a wire envelope. Do not send an extra {data:{...}} wrapper. Form login and binary upload chunks use their own content types.

ConcernExisting contractRequired rebuild
AuthenticationSigned mediavault_session cookie; session reader also accepts Authorization: Bearer <signed-session-value>. OAuth is the only new login flow. Protected routes redirect unauthenticated or disabled accounts to /login with HTTP 302, including API calls.JSON clients must detect redirects/login HTML before parsing. Required rebuild: return JSON 401 with AUTH_REQUIRED and the mandatory error envelope below for API authentication failures; keep redirects for page navigation. This is a new contract, not the existing body.
Account guardDrive-note middleware compares optional x-filesandnotes-user-key with the signed-in account and returns 409 on mismatch. It does not establish ownership. Upload ownership is stricter: the creating sessionId.Derive every storage root and provider token from the authenticated session. Apply the mismatch guard consistently to all account APIs and namespace browser caches per user.
IDsBatch note/folder IDs: 1–100 letters, digits, underscores or hyphens; reject __proto__, constructor and prototype. Some older direct endpoints only check the character pattern. Drive IDs, revisions and tickets have separate formats.Use shared validators and explicit schemas for each endpoint. Reject unknown operation kinds, malformed objects, mismatched IDs, invalid parents/cycles and oversized requests. Do not silently coerce an invalid payload into an empty operation.
PrivacyNote bodies supplied for search, synchronization or explicit AI processing are request-scoped; only temporary encrypted sync packets may survive until their result is consumed.No SQL server-note table, durable readable projection, request-body logs or note-response caches. Canvas and media annotations have a separate documented scope; see Data Structure.
ConcurrencySixteen parallel note reads and eight parallel batch note writes; result order can differ from request order. Folder operations execute before note writes except deletions, which execute afterward.Correlate note results by ID and folder results by key. Do not queue two contradictory operations for one note ID in the same batch. Retain a local dispatch-token snapshot for every item.

Timestamps in document records are milliseconds. Drive revisions are opaque strings. A missing field and explicit null are different; preserve them. DocumentRecord, folder and canvas fields are defined in 10 Data Structure.

Required rebuild · Request and file limits

These are target limits for the rebuild, not a claim that the existing runtime enforces them. Reject non-identity Content-Encoding with HTTP 415. Cap the streamed request-body bytes before parsing/allocation or ticket acceptance and validate per-item sizes. JSON sizes are serialized UTF-8 bytes, including envelopes and escaping; raw binary chunk sizes count binary body bytes. Do not compare only parsed note-text length. Keep readable note text in the browser when a limit is exceeded; never truncate it silently.

BoundaryTarget maximumBehavior
Metadata/status mutation request64 KiB (65,536 bytes)Reject oversized request with 413; return maxBytes without echoing submitted content.
Note sync or search JSON request8 MiB (8,388,608 bytes)Limit the whole encoded request; batch at most 80 items per operation array and fetch at most 40 changed bodies per pull. One oversized item remains queued with a visible too-large state; smaller valid work can be sent separately.
Binary upload chunk8 MiB (8,388,608 bytes)Also enforce offset, declared/actual length, remaining file bytes and provider chunk constraints. Last chunk can be smaller.
One uploaded source file10 GiB (10,737,418,240 bytes)Validate declared total before begin; stream chunks. Existing Drive sources can still be linked without a new upload; upload is optional and requires a connection.
Canvas JSON512 KiB (524,288 bytes)Existing canvas serialized-size cap; keep an explicit limit in the rebuild.
Drive concurrency16 reads / 8 writesBound concurrent provider work; keep UI cancellation and retry state independent of a request queue.

The required normalized size error is {error:{code:"PAYLOAD_TOO_LARGE",message:"This request is too large.",retryable:false},maxBytes:8388608} with HTTP 413. Adapt this explicitly when interoperating with existing string errors; maxBytes is the limit of the failed boundary. Note content, filenames containing private data and credentials must not be echoed. An oversized pending note remains locally recoverable and exportable.

2 · Sign-in, connection and note-storage status

Method / pathRequestResponse / behavior
POST /auth/googleForm: optional mode=signup; signup requires the accepted-terms form value.302 to Google; missing consent/configuration redirects to signup/login with an error query. Callback GET /auth/google/callback?code=...&state=... validates state/profile, rotates the session and redirects into the app.
POST /logoutCurrent session; no JSON payload needed.Clears the session/cookie and redirects. Do not interpret the redirected HTML as a JSON success envelope.
GET /api/google-drive/statusNone.200 {configured:boolean,connected:boolean,email:string}. Sign-in and Drive consent are separate; configured or connected alone does not prove note storage is active.
GET /google-drive/connect / GET /google-drive/callbackBrowser OAuth redirect flow.Starts/finishes separate Drive consent. Never put refresh tokens in browser payloads.
POST /google-drive/disconnectAuthenticated browser action.Existing route revokes/clears the connection and returns 200 {ok:true}; it can redirect for an accepted form target. The current handler reads that redirect target from a different parser location than most JSON fields; do not invent a new JSON wrapper to match it. Client cleanup semantics must be checked before reuse. Required rebuild: retain the local queue/cache and original Drive files. See Failure & Recovery.
GET /api/notes/storageNone.200 {active,connected,folderId,folderName,notesCount,migrationPending,migrationStage,userKey}. Booleans: active/connected/migrationPending; notesCount:number; remaining fields:string. The existing handler can attempt activation when connected; it is not a purely passive status read. Required rebuild: this GET is passive and performs no activation, migration, upload or deletion. Activate only after explicit user action through authenticated POST /api/notes/storage/migrate, using empty arrays for a new account or a verified import for existing data.
POST /api/notes/storage/migrateJSON {documents:DocumentRecord[],folders:Folder[]}; optional account guard header.200 storage status on success. Uses receipt-checked legacy migration and activates Drive. Account mismatch 409; migration errors generally 503. Never delete a unique source before verified transfer.

Synthetic active storage response

HTTP 200
Content-Type: application/json

{
  "active": true,
  "connected": true,
  "folderId": "drive-app-folder",
  "folderName": "FileLoveNote",
  "notesCount": 1,
  "migrationPending": false,
  "migrationStage": "active",
  "userKey": "0123456789abcdef0123456789abcdef"
}

Legacy browser import uses GET /api/local-first/bootstrap → {enabled:true,browserOnly:true,documents:[],migrationReceipt:string,noteFolders:[],media:[],exportedAt:number}, followed only after durable browser import by POST /api/notes/storage/browser-import with {receipt:string} → {ok:true}. Receipt mismatch gives 409. Active Drive middleware can reject the old bootstrap path with 409, so the rebuild must define and test migration ordering rather than assume every old route remains available in every state.

3 · Incremental note retrieval

POST /api/drive-notes is the Drive-mode pull. Send your confirmed revision map, including all locally known remote versions. The map is not a page offset. After each successful batch, retain unchanged content and merge the returned revision knowledge; repeat while hasMore is true. Render IndexedDB first.

FieldPresence / typeMeaning
Request revisionsoptional object: logical note ID → string; default {}The browser’s known Drive version per note. Do not mark an unreadable response as a successfully downloaded version.
Response documentsarray of decorated DocumentRecordAt most 40 changed/missing note contents per pull. An unreadable note can have driveReadError; keep the good browser content/base instead of replacing it with the stub.
Response revisionsobject: logical note ID → opaque stringCurrent index used to distinguish unchanged versions and removals. Do not delete based on one missing content batch or an unsuccessful response.
Response hasMorebooleanMore changed content must be pulled; update the request map as content is safely applied.
Response removedarray of logical note ID stringsExplicit removals detected during refresh. Apply deletion reconciliation with pending edits and tombstones; this is not permission to discard unsent text.

A changed note in an incremental batch

POST /api/drive-notes
Content-Type: application/json

{
  "revisions": {
    "doc-example": "opaque-revision-before"
  }
}
HTTP 200
Content-Type: application/json

{
  "documents": [
    {
      "id": "doc-example",
      "title": "Research notes",
      "folderId": "note-folder-example",
      "content": {
        "type": "doc",
        "content": [
          {
            "type": "paragraph",
            "content": [
              {
                "type": "text",
                "text": "My observations about the source file."
              }
            ]
          }
        ]
      },
      "createdAt": 1793000000000,
      "updatedAt": 1793000001000,
      "trashedAt": null,
      "driveRevision": "opaque-revision-a",
      "driveFileId": "drive-note-example"
    }
  ],
  "revisions": {
    "doc-example": "opaque-revision-a"
  },
  "hasMore": false,
  "removed": []
}

Old GET /api/drive-notes returns 409 with {error:string} asking the client to refresh. Do not build a new full-content polling protocol around it. The incremental contract requires active Drive mode; inactive storage is a browser-only state, not an empty remote dataset. Transport failures generally surface as 503 with a generic error, not a successful empty list.

4 · Batched note and folder changes

POST /api/drive-notes/push accepts three optional arrays, each defaulting to an empty array in the current implementation. Each array may contain at most 80 entries. Exceeding the cap creates a 413 error internally; a direct request’s outer catch currently often reports 503, while the station path preserves the processing status. Required rebuild: reject invalid top-level JSON and enforce count/byte limits before accepting a ticket, with explicit 400/413 responses.

Item / fieldPresence / typeContract
Upsert id, documentrequired string; required DocumentRecordItem ID must equal document.id. The document is a full record, not a partial patch. Missing folderId falls back to folder-notes in the batch path; new clients should always supply a valid Notes folder.
Upsert baseDocumentoptional DocumentRecordLast confirmed shared ancestor for three-way merge. Retain it in the browser; do not create a readable server snapshot.
Upsert tokenstring; optional in existing validation, required for rebuildStable identity of this queued version. Reuse it after a lost response; a new edit gets a new token. Server metadata can retain lastToken for duplicate detection.
Delete id, tokenID required string; token string required for rebuildDelete the app-owned Markdown note, not an original media source referenced in its content. The current delete worker primarily consumes id; a token also identifies station retries. Local tombstones must already exist.
Folder key,id,kind,bodykey:string, id:string, kind enum, body:objectKinds: create/rename/move/delete/restore. Key is the stable outgoing action key; the implementation echoes it but does not durably deduplicate all folder actions by key.
Folder create / rename bodyname:string, non-empty after trim; create parentId?:stringCreate is idempotent if id already exists. The root cannot be created/replaced/renamed. Parent must exist and be active.
Folder move bodyparentId?:string, beforeId?:string, afterId?:stringDefault parent is folder-notes. Reject self/ancestor cycles. beforeId/afterId control sibling ordering.
Folder delete / restore bodyempty object is sufficientDelete removes folder metadata/subtree after successful note processing; the client must also enqueue contained-note deletions. Restore handles legacy trash markers; it does not recreate permanently erased content.

One new version, one note deletion and a parent-folder operation

POST /api/drive-notes/push
Content-Type: application/json

{
  "station": true,
  "upserts": [
    {
      "id": "doc-example",
      "document": {
        "id": "doc-example",
        "title": "Research notes",
        "folderId": "note-folder-example",
        "content": {
          "type": "doc",
          "content": [
            {
              "type": "paragraph",
              "content": [
                {
                  "type": "text",
                  "text": "My observations about the source file."
                }
              ]
            }
          ]
        },
        "createdAt": 1793000000000,
        "updatedAt": 1793000001000,
        "trashedAt": null,
        "driveRevision": "opaque-revision-a",
        "driveFileId": "drive-note-example"
      },
      "baseDocument": {
        "id": "doc-example",
        "title": "Research notes",
        "folderId": "note-folder-example",
        "content": {
          "type": "doc",
          "content": [
            {
              "type": "paragraph",
              "content": [
                {
                  "type": "text",
                  "text": "My observations about the source file."
                }
              ]
            }
          ]
        },
        "createdAt": 1793000000000,
        "updatedAt": 1793000000000,
        "trashedAt": null,
        "driveRevision": "opaque-revision-a",
        "driveFileId": "drive-note-example"
      },
      "token": "queue-version-example"
    }
  ],
  "deletes": [
    {
      "id": "doc-remove-example",
      "token": "delete-version-example"
    }
  ],
  "folders": [
    {
      "key": "folder-op:000000000001",
      "id": "note-folder-example",
      "kind": "create",
      "body": {
        "name": "Sources",
        "parentId": "folder-notes"
      }
    }
  ]
}
HTTP 200
Content-Type: application/json

{
  "queued": true,
  "ticket": "0123456789abcdef0123456789abcdef"
}

Completed batch result

Without a station, or when retrieving a completed station result, the top level is {notes:NoteResult[],folders:FolderResult[]}. HTTP 200 can contain per-item failures. Process every item; never treat response.ok alone as a full-batch save.

Result variantFieldsClient action
Note saved{id,ok:true,document:DocumentRecord}Keep the confirmed document/revision as base. Clear only the pending operation matching the token captured at dispatch; preserve a newer local version.
Note deleted{id,ok:true,deleted:true}Acknowledge that deletion version. Keep stale-work protection. It does not authorize deleting linked media bytes.
Note failed{id,ok:false,status:number,error:string}Retain the queue entry. status may be 400, 404, 409 or 503 internally; error text is often generic even for validation failures.
Note conflictFailed result plus document?:DocumentRecord|null,conflictDocument:DocumentRecord; status 409Preserve the server conflict copy and inspect the returned main document. A conflict is not an offline retry and must not lose the user version.
Folder result{key,id,kind,ok:boolean,error?:string}Match key, not index. Existing folder failures have no numeric status. Retain failed actions and dependent notes. Folder deletions are withheld when any note/folder operation in the batch failed.

Successful note and folder results

HTTP 200
Content-Type: application/json

{
  "notes": [
    {
      "id": "doc-example",
      "ok": true,
      "document": {
        "id": "doc-example",
        "title": "Research notes",
        "folderId": "note-folder-example",
        "content": {
          "type": "doc",
          "content": [
            {
              "type": "paragraph",
              "content": [
                {
                  "type": "text",
                  "text": "My observations about the source file."
                }
              ]
            }
          ]
        },
        "createdAt": 1793000000000,
        "updatedAt": 1793000001000,
        "trashedAt": null,
        "driveRevision": "opaque-revision-a",
        "driveFileId": "drive-note-example"
      }
    },
    {
      "id": "doc-remove-example",
      "ok": true,
      "deleted": true
    }
  ],
  "folders": [
    {
      "key": "folder-op:000000000001",
      "id": "note-folder-example",
      "kind": "create",
      "ok": true
    }
  ]
}

Per-item conflict inside an HTTP 200 result

HTTP 200
Content-Type: application/json

{
  "notes": [
    {
      "id": "doc-example",
      "ok": false,
      "status": 409,
      "error": "A conflicting version was preserved.",
      "document": {
        "id": "doc-example",
        "title": "Research notes",
        "folderId": "note-folder-example",
        "content": {
          "type": "doc",
          "content": [
            {
              "type": "paragraph",
              "content": [
                {
                  "type": "text",
                  "text": "My observations about the source file."
                }
              ]
            }
          ]
        },
        "createdAt": 1793000000000,
        "updatedAt": 1793000001000,
        "trashedAt": null,
        "driveRevision": "opaque-revision-a",
        "driveFileId": "drive-note-example"
      },
      "conflictDocument": {
        "id": "conflict-doc-example",
        "title": "Research notes (conflict)",
        "folderId": "note-folder-example",
        "content": {
          "type": "doc",
          "content": [
            {
              "type": "paragraph",
              "content": [
                {
                  "type": "text",
                  "text": "My observations about the source file."
                }
              ]
            }
          ]
        },
        "createdAt": 1793000000000,
        "updatedAt": 1793000001000,
        "trashedAt": null,
        "driveRevision": "opaque-revision-a",
        "driveFileId": "drive-note-example"
      }
    }
  ],
  "folders": []
}

The server does not echo the upsert/delete token in each note result. Capture {id,token} when dispatching and compare that captured token with the current IndexedDB pending token when applying the result. Folder actions use their echoed key. Echoing operation tokens is optional; mandatory normalized error codes and envelopes are defined in section 10. The existing wire has neither. Preserve the captured-dispatch-token comparison rule.

5 · Temporary tickets and replay

Request / stateExisting responseRecovery
Push with station:true and station available200 {queued:true,ticket:string}; ticket is 32 lowercase hex characters. A duplicate completed request can return its batch result directly.Receipt means encrypted staging, not Drive success. Keep pending browser work.
GET /api/drive-notes/push-ticket?ticket=... while processing200 {queued:true,ticket}Poll with bounded delay/backoff. The current browser polls at about 400ms with a 120s overall wait; these are implementation values, not a service guarantee.
Completed ticket200 batch result, or {error:string} with processing failure status such as 400/413/503.Reading a completed result consumes and deletes the account-scoped encrypted ticket file. A subsequent poll gives 404, even if the earlier result was lost in transit.
Missing/invalid/consumed ticket404 {error:string}Do not drop the queue. Replay the original logical operation with the original token and folder key; inspect resulting metadata before inventing a new operation.
No station configuredPush can return the completed batch directly; ticket GET returns 404.Handle both immediate and queued responses. No assumption that station:true guarantees a queued response.

Station duplicate identity uses note IDs/tokens and folder keys/kinds, not a content hash. Never reuse a token/key for different content. There is no general durable upload-session service or end-to-end encryption implied by these tickets. Replay rules and account/disconnect behavior are specified in 12 Failure & Recovery.

6 · Note and folder read compatibility

Method / pathShapeImportant boundary
GET /api/notes?folderId=<id>{documents:[{id,title,folderId,createdAt,updatedAt,goalReferenceCount,driveRevision?}]}List summaries, not the incremental full-content protocol. /api/documents aliases exist; choose /api/notes for new code.
GET /api/notes/:id{document:DocumentRecord}; 400 invalid ID / 404 absentIn browser-only mode, load the browser store directly. A server miss must never create or overwrite a local note.
POST /api/notes/:id{document:DocumentRecord,baseDocument?:DocumentRecord} → {ok:true,document}Compatibility route: Drive middleware persists the write in active mode; inactive mode returns 409 browserOnly. Prefer the tokenized batch queue for new sync code.
GET /api/note-folders?includeTrashed=true{folders:Folder[]}Returns Notes metadata without loading full Drive text.
POST /api/note-folders{id?,name,parentId?} → {ok:true,folder}Old direct create; batch creates should supply stable id for retry.
POST /api/note-folders/:id/rename or /move{name} or {parentId,beforeId?,afterId?} → {ok:true,folder}400 root/cycle/invalid input; 404 missing parent/folder. Inactive mode returns 409 browserOnly.
DELETE /api/note-folders/:id{ok:boolean,folders:string[],failed:array}Existing permanent deletion path. Batch deletion ordering differs; test both if both are retained.

Required rebuild: use the batch contract for Drive changes and local IndexedDB operations when Drive is inactive. Do not enable a hidden server-note fallback to make old routes return success. Removing a source block from a note is an editor update only: it must not call a source-file deletion endpoint.

8 · Drive picker and resumable file upload

Method / pathRequestExisting response / error
GET /api/google-drive/listQuery folder=root|shared-with-me|all|<DriveFolderId>, q, pageToken, includeAll=true.200 {configured,connected,email,folders,items,breadcrumb,nextPageToken}. Folder: {id,name,parentId}; breadcrumb: {id,name}; item shape below. Empty nextPageToken ends pagination. includeAll is necessary for the all-file-type inline picker.
Disconnected listingNo configured client or expired/revoked auth.200 with configured:boolean, connected:false, email:"", and empty arrays/token. This is connection state, not proof an authenticated Drive is empty. Non-auth provider error is 502 with error and empty arrays.
POST /google-drive/upload/begin{name:string,size:number,type?:string,folderId?:string}200 {ok:true,uploadId:string,chunkSize:8388608,received:0}. Name required, positive finite size, allowed extension, configured size cap and existing Files folder. Invalid input/size currently 400, folder 404, provider auth 401, other provider failure 502.
PUT /google-drive/upload/chunk/:idBinary body; X-Chunk-Offset and X-Chunk-Length in bytes; actual Content-Length must agree when present.200 {ok:true,received:number}. Reject beyond total or invalid lengths with 400; offset mismatch 409 plus received; unknown/not-owned upload 404; provider failures 401/502.
GET /google-drive/upload/status/:idNo body. Use no-store.200 {ok:true,received,size,completed:boolean}. Resume from the server offset, not a guessed browser offset.
POST /google-drive/upload/finish/:idNo JSON fields needed.200 {ok:true,file:MediaRecord}; repeat returns same file with repeated:true. Incomplete upload 409 plus received/size; unknown session 404.
DELETE /google-drive/upload/:idNo body.200 {ok:true}; drops local upload bookkeeping/name claim. Do not interpret it as deletion of an already completed original Drive file.

Synthetic picker item

HTTP 200
Content-Type: application/json

{
  "configured": true,
  "connected": true,
  "email": "example@example.test",
  "folders": [],
  "items": [
    {
      "id": "drive-source-example",
      "name": "Source.pdf",
      "type": "book",
      "size": 1073741824,
      "mtime": 1793000000000,
      "thumbUrl": "",
      "downloadUrl": "/google-drive/raw/drive-source-example",
      "url": "/google-drive/raw/drive-source-example",
      "route": "gdrive:drive-source-example",
      "source": "gdrive",
      "folderId": "drive-parent-example"
    }
  ],
  "breadcrumb": [
    {
      "id": "root",
      "name": "Google Drive"
    }
  ],
  "nextPageToken": ""
}

A generic non-media item uses type:"file" and its url can be a Google Drive view link. Listing folderId is the original remote parent; upload begin’s folderId instead selects local Files library metadata placement. Upload bytes go to the dedicated FileLoveNote Drive upload folder, not a caller-selected remote parent. Do not conflate the two namespaces.

Start a synthetic resumable upload

POST /google-drive/upload/begin
Content-Type: application/json

{
  "name": "Source.pdf",
  "size": 1073741824,
  "type": "application/pdf",
  "folderId": "folder-home"
}
HTTP 200
Content-Type: application/json

{
  "ok": true,
  "uploadId": "0123456789abcdef0123456789abcdef",
  "chunkSize": 8388608,
  "received": 0
}

Upload sessions are bound to the creating login session ID and kept only in process memory. Reload can resume while the same session/process survives; server restart, expired bookkeeping or session rotation can yield 404. Completed records are retained about one hour; abandoned sessions are swept after 24 idle hours when cleanup runs. Current runtime does not guarantee cross-restart resumability. Name claims prevent two concurrent uploads from silently overwriting the same filename. Required rebuild: state recovery limits honestly; if durable resumability is chosen, persist only technical upload metadata, never duplicate GB-sized media or editor notes.

9 · Media, annotations and canvas

Method / pathRequest / responseBoundary
GET /google-drive/raw/:idAuthenticated bytes, honoring supported Range requests. Local existing media uses /stream/:name, /raw/:name, /media/:name.200 full response; 206 for valid ranges, 416 for unsatisfiable ranges where supported. Treat MIME/length/range headers as part of the contract. Large sources are streamed, not loaded as a complete JSON blob.
GET /google-drive/thumb/:id or /thumb/:nameImage/placeholder bytes.A placeholder is not evidence the original is missing. Do not block note editing on thumbnail generation.
GET / POST /progress/:nameGET → {seconds:number}; POST {seconds:number} → {ok:true}.name is a URL-encoded safe local name or gdrive:<id>. Existing valid nonnegative seconds are floored and stored with lastPlayedMs; invalid numeric input can be ignored rather than rejected.
POST /annotations/:name{time:number,text:string} → {id:string,timeLabel:string}.Nonempty text, finite nonnegative time; integer seconds; text truncated to 200 characters. 400 invalid annotation. Per-media annotation storage is explicitly separate from editor note storage.
POST /api/library-items/delete{items:[{source:"paper"|"local"|"gdrive",id:string}]} → {ok:boolean,deleted:[{source,id}],failed:[{source,id,error}]}.Existing destructive API removes selected source bytes/notes. Required rebuild: removing a note reference must never invoke it; explicit source deletion, if retained, needs separate scope/confirmation. File deletion is not implied by disconnect, reset or account deletion.
GET /api/notes/:docId/canvas/:canvasId200 {canvas:Canvas}.400 invalid IDs; 404 missing note; 500 corrupt stored JSON. Missing canvas under an existing note returns a new normalized canvas, not 404.
PUT /api/notes/:docId/canvas/:canvasId{canvas:Canvas} → {canvas,summary:{title,elementCount,refTitles}}.Maximum serialized canvas 512 KiB; 413 oversized. The server forces docId/id to route IDs and updatedAt to now; model normalization handles enums and connections.
DELETE /api/notes/:docId/canvas/:canvasId200 {deleted:boolean}.Deleting canvas data does not delete referenced notes or source files.

Canvas handlers currently depend on resolving the note through the server document service. Browser-only notes have no durable server note record. Required rebuild: define how an authorized browser-only canvas request proves its owning note or keep canvas local until Drive is active; do not restore server note content as a shortcut.

Save an empty synthetic canvas

PUT /api/notes/doc-example/canvas/canvas-example
Content-Type: application/json

{
  "canvas": {
    "format": "notesandfiles-canvas-v1",
    "id": "canvas-example",
    "docId": "doc-example",
    "title": "Source map",
    "createdAt": 1793000000000,
    "updatedAt": 1793000001000,
    "elements": [],
    "connectors": []
  }
}
HTTP 200
Content-Type: application/json

{
  "canvas": {
    "format": "notesandfiles-canvas-v1",
    "id": "canvas-example",
    "docId": "doc-example",
    "title": "Source map",
    "createdAt": 1793000000000,
    "updatedAt": 1793000002000,
    "elements": [],
    "connectors": []
  },
  "summary": {
    "title": "Source map",
    "elementCount": 0,
    "refTitles": []
  }
}

10 · Errors and retry decisions

SignalExisting meaningSafe response
302 / login HTMLNo valid login session or disabled account; browser redirects may conceal it from fetch callers.Stop account requests, retain local work and ask the user to sign in. Never parse an HTML page as an empty JSON list.
400 / 413Validation or size error where exposed. Batch validation can instead be nested under notes[].status or masked by the outer 503 catch.Fix the request; do not endlessly retry unchanged invalid work. Required rebuild returns stable machine codes without content-bearing error details.
401 / disconnected:trueDrive authorization unavailable or revoked; not the same as app-session authentication.Retain local queue/cache, reconnect deliberately and retry. Never switch to another account’s token.
404Missing resource, consumed ticket or process-local upload no longer owned/found.Interpret by endpoint. A missing ticket means replay the original operation; a missing source reference means show unavailable; never create an empty replacement note.
409Account mismatch, refresh-required old API, conflict, invalid upload offset, browser-only storage mode, or migration receipt mismatch.Use endpoint-specific fields and state. A conflictDocument requires content preservation; upload received permits offset recovery. Do not treat all 409s as the same event.
429 / 502 / 503 / network timeoutRate limit/provider failure or lost reply. Drive layer often maps provider failures to 503; AI can expose 429 directly.Bounded backoff with jitter; preserve exact tokens/keys and queued revisions. A lost reply may mean the remote write already succeeded.
200 partial batchOne or more notes[].ok or folders[].ok is false.Apply successful matching acknowledgments only, retain failures, and display truthful per-item status.

Existing errors use strings and redirects, not a universal structured envelope. Required new-build contract: all JSON endpoint failures return {error:{code:string,message:string,retryable:boolean},requestId?:string,maxBytes?:number}. requestId is an optional opaque correlation ID, never an account credential; do not log submitted content under it. HTTP 413 requires maxBytes. Batch item failures retain their existing id or key/id/kind correlation and ok:false, add numeric status, and use the same structured error; their 413 failures also include maxBytes. Successful item shapes stay as documented above. Implement a compatibility adapter only if calling the old server; old 302/string examples are not desired new-build responses. retryable:true means the same idempotent request may be retried after the documented delay; false requires a user action or request correction.

HTTP / codeMeaningRetryable
401 · AUTH_REQUIREDMissing or expired app session. Sign in to the same app account.false
401 · DRIVE_REAUTH_REQUIREDApp session valid, Drive token unavailable/revoked. Reconnect verified Drive identity, not app logout.false
403 · FORBIDDEN / ACCOUNT_DISABLEDOperation disallowed or account blocked. For cross-account object IDs use 404 NOT_FOUND to avoid exposing existence.false
400 · VALIDATION_ERRORMalformed JSON, ID, schema or unknown action.false
404 · NOT_FOUNDUnavailable object or receipt; follow endpoint-specific recovery.false
409 · ACCOUNT_MISMATCH / REVISION_CONFLICT / UPLOAD_OFFSET_MISMATCH / MIGRATION_RECEIPT_MISMATCHUse the relevant account/version/offset/receipt recovery; do not replay unchanged invalid state.false
413 · PAYLOAD_TOO_LARGEBody, item or declared file exceeds its boundary; maxBytes required.false
415 · UNSUPPORTED_ENCODINGContent-Encoding is not absent/identity.false
429 · RATE_LIMITEDTransient provider/app rate limit; honor Retry-After.true
503 · PROVIDER_UNAVAILABLETransient upstream/network failure.true
500 · INTERNAL_ERRORUnexpected server failure; preserve work and surface a redacted diagnostic.false

Validate against api-error.schema.json and apply Failure & Recovery. Acceptance criteria belong in 15 Acceptance.

11 · Rebuild contract checklist

  1. Validate the synthetic examples in fixtures/api-examples.json and round-trip record fixtures from fixtures/records.json; generated schemas must retain the documented optional/null distinctions.
  2. Use request-scoped account identity for every route; test two accounts and session rotation. Never make a userKey header an authorization credential.
  3. Commit local document and pending operation atomically. Match responses against the captured dispatch token/key, preserve newer edits, and test one lost push response plus one consumed-ticket response.
  4. Test full revision pagination, unreadable stubs, missing list entries, deletion tombstones, folder dependency failures and a mixed-success batch.
  5. Test upload offset conflict, duplicate finish, two same-name files, process restart and session change. Report which resume boundary is supported.
  6. Verify original-source preservation for removing a reference, disconnect, reset and account deletion. Keep explicit note/Drive-Markdown deletion separate from media reference removal.
  7. Preserve supported blocks according to 13 Markdown Compatibility; malformed metadata must not blank a good note.
  8. Record actual contract test evidence and live OAuth/Drive evidence separately using 14 Setup & Operations and 15 Acceptance. These examples are specifications, not executed live tests.