Sync API Reference

Build your own backend for NoteFlow in any language. This page documents the HTTP contract your server needs to implement.

Overview

NoteFlow's "Bring Your Own Sync" feature lets users configure a custom HTTP endpoint to sync their notes across devices. Your server needs to implement just two operations at a single URL:

  • GET — Return notes modified since a given timestamp
  • POST — Accept notes that are newer on the client

That's it. No WebSockets, no OAuth, no complex handshakes. A single URL, two HTTP methods, JSON in and out.

Authentication

All requests include an API key in the X-API-Key header. The user configures this key in the extension settings alongside the endpoint URL.

Request Header
X-API-Key: your-secret-key-here

How you validate this key is up to you — a simple static token check, a database lookup, or a full auth layer. The extension doesn't care as long as a 200 response comes back with the expected shape.

GET — Fetch Notes

The extension calls this to pull down any notes that have been modified since the last sync.

Request

HTTP
GET {syncUrl}?last_sync={unix_timestamp_seconds}
X-API-Key: {key}
Parameter Location Description
last_sync Query string Unix timestamp in seconds. Return all notes modified after this time. Will be 0 on first sync.
X-API-Key Header User-configured API key for authentication.

Response

200 OK — application/json
{
  "status": "success",
  "server_time": 1719000000000,
  "notes": [
    {
      "video_id": "dQw4w9WgXcQ",
      "video_title": "Rick Astley - Never Gonna Give You Up",
      "markdown": "# My notes\n\n[0:15] Intro starts here",
      "updated_at": "1719000000000"
    }
  ]
}
Field Type Description
status string "success" on success
server_time number Current server time in milliseconds (epoch). The extension stores this and sends it as last_sync on the next request (converted to seconds).
notes array Notes modified since last_sync. Empty array if nothing changed.
notes[].video_id string YouTube video ID (e.g. dQw4w9WgXcQ)
notes[].video_title string Human-readable video title
notes[].markdown string Full note content in Markdown
notes[].updated_at string Last modified time in milliseconds (epoch), as a string

POST — Push Notes

The extension sends locally-modified notes to your server. These are notes that have a newer updatedAt than what the server last reported.

Request

HTTP
POST {syncUrl}
Content-Type: application/json
X-API-Key: {key}
Request Body
{
  "notes": [
    {
      "videoId": "dQw4w9WgXcQ",
      "videoTitle": "Rick Astley - Never Gonna Give You Up",
      "markdown": "# Updated notes content",
      "updatedAt": 1719000000000
    }
  ]
}
Field Type Description
notes[].videoId string YouTube video ID
notes[].videoTitle string Video title
notes[].markdown string Note content in Markdown
notes[].updatedAt number Last modified time in milliseconds (epoch)

Response

200 OK — application/json
{
  "status": "success"
}
Note the naming difference: The GET response uses snake_case (video_id, updated_at) while the POST body uses camelCase (videoId, updatedAt). Your server must handle both conventions.

Sync Behavior

  • Manual trigger — Sync only runs when the user clicks "Sync Now" in the extension. There's no background polling or automatic sync.
  • Last-write-wins — Conflict resolution is based on updatedAt timestamps. Whichever side has the more recent timestamp wins.
  • No tombstones — When a user deletes a note locally, it's removed from local storage but no deletion event is sent to the server. If the note still exists on the server, it will reappear on the next sync.
  • Delta sync — Only notes modified since last_sync are exchanged in either direction.

Sync Flow

  1. Extension sends GET with the stored last_sync timestamp
  2. Server returns notes modified since that time
  3. Extension merges server notes into local storage (last-write-wins)
  4. Extension sends POST with any local notes that are newer than the server's versions
  5. Extension stores the server_time from the GET response for next sync

Error Handling

The extension checks for a 200 status code and a JSON body with "status": "success". Anything else is treated as a failure and shown to the user as a sync error.

Recommended error responses:

401 Unauthorized
{
  "status": "error",
  "message": "Invalid API key"
}
500 Internal Server Error
{
  "status": "error",
  "message": "Database connection failed"
}

The extension doesn't retry automatically — the user will need to click "Sync Now" again.

Implementation Checklist

Here's the minimum you need to get a working sync backend:

  • A single URL that accepts both GET and POST
  • Validate the X-API-Key header on every request
  • Store notes keyed by video_id with their updated_at timestamp
  • GET: filter notes where updated_at > last_sync (convert seconds → ms for comparison)
  • GET: return current server time in ms as server_time
  • POST: upsert each note by videoId, storing the new updatedAt
  • Return { "status": "success" } on success with a 200 status
  • Return appropriate HTTP error codes with a JSON body on failure

That's roughly 50–100 lines of code in most languages. A SQLite database or even a JSON file on disk is plenty for personal use.