Granite Sync API

The sync protocol (PLAN.md Appendix B).

GitHub OpenAPI

Operations 14

GET /v1/vaults/{vault_id}/tree Snapshot of the vault's files #
GET /v1/vaults/{vault_id}/changes Changes after a sequence number #
GET /v1/vaults/{vault_id}/files/{file_id}/info Who created and last changed a note, and who can open it #
POST /v1/vaults/{vault_id}/commit Apply a batch of changes atomically #
POST /v1/vaults/{vault_id}/blobs/check Which blobs still need uploading #
PUT /v1/vaults/{vault_id}/blobs/{hash} Upload a blob (up to 8 MiB) #
GET /v1/vaults/{vault_id}/blobs/{hash} Download a blob #
POST /v1/vaults/{vault_id}/uploads Start a resumable upload #
GET /v1/vaults/{vault_id}/uploads/{upload_id} How much of an upload arrived #
PATCH /v1/vaults/{vault_id}/uploads/{upload_id} Send the next part (up to 16 MiB) #
POST /v1/vaults/{vault_id}/uploads/{upload_id}/finish Finish an upload #
GET /v1/vaults/{vault_id}/files/{file_id}/versions A file's version history, newest first #
GET /v1/vaults/{vault_id}/files/{file_id}/versions/{version_id} One version #
POST /v1/vaults/{vault_id}/files/{file_id}/versions/{version_id}/restore Make an old version current again #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/granite-md:granite-md-sync-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

granite-md-sync-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Granite Sync API
  version: 1.0.0
  summary: REST API for Granite clients, bots, and integrations.
  description: 'Conventions (apply to every operation; see PLAN.md 3.2 and 3.6):


    - **Base URL** `https://api.granite.md`. All public operations live under `/v1`.

    After Phase 6 ships, `/v1` changes are additive only.

    - **Errors** use RFC 9457 problem details (`application/problem+json`, schema `Problem`).

    The `type` URI is `https://granite.md/problems/`; `code` refines it for programmatic handling.

    Every response carries `X-Request-ID`, also echoed as `request_id` in problems.

    - **Pagination** is cursor based. List operations accept `cursor` and `limit` and return

    `{ "items": [...], "next_cursor": "..." }`. `next_cursor` is absent on the last page.

    Cursors are opaque; clients must not parse or build them.

    - **Idempotency**: non-idempotent `POST` operations accept an `Idempotency-Key` header

    (1-255 printable ASCII). Repeating a request with the same key and body within 24 hours

    returns the original response; the same key with a different body returns `409` with

    code `idempotency_key_reused`.

    - **IDs** are UUIDv7 strings. **Timestamps** are RFC 3339 in UTC.

    - **Authentication**: `Authorization: Bearer ` with a session access token,

    a personal API token (`gr_pat_...`), or an OAuth access token. Scopes are listed per operation.

    - **Rate limits**: each personal API token may make 600 requests a minute. Over the limit

    the API answers `429` with code `token_rate_limited` and a `Retry-After` header (seconds).'
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
- url: https://api.granite.md
  description: Production
- url: http://localhost:8080
  description: Local development
security:
- bearerAuth: []
tags:
- name: Sync
  description: The sync protocol (PLAN.md Appendix B).
paths:
  /v1/vaults/{vault_id}/tree:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    get:
      operationId: getTree
      tags:
      - Sync
      summary: Snapshot of the vault's files
      description: 'Live files as of `at` (default: the current head). Pass the same `at` with `cursor` to get

        later pages of the same snapshot (Appendix B.3).'
      parameters:
      - name: at
        in: query
        required: false
        schema:
          type: integer
          format: int64
          minimum: 0
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: A page of the snapshot.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreePage'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/changes:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    get:
      operationId: getChanges
      tags:
      - Sync
      summary: Changes after a sequence number
      description: Answers 410 with code `cursor_expired` when `since` is older than the kept log.
      parameters:
      - name: since
        in: query
        required: true
        schema:
          type: integer
          format: int64
          minimum: 0
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 500
      responses:
        '200':
          description: Changes in order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangesPage'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/files/{file_id}/info:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    - $ref: '#/components/parameters/FileID'
    get:
      operationId: getNoteInfo
      tags:
      - Sync
      summary: Who created and last changed a note, and who can open it
      description: Anyone who can read the note. `created_by` is absent once history no longer reaches the first version.
      responses:
        '200':
          description: OK.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NoteInfo'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/commit:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    post:
      operationId: commitChanges
      tags:
      - Sync
      summary: Apply a batch of changes atomically
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRequest'
      responses:
        '200':
          description: Every op applied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResult'
        '409':
          description: Nothing applied; every conflicting op is listed (code `commit_conflict`).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/CommitConflictProblem'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/blobs/check:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    post:
      operationId: checkBlobs
      tags:
      - Sync
      summary: Which blobs still need uploading
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BlobCheckRequest'
      responses:
        '200':
          description: Missing hashes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlobCheckResult'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/blobs/{hash}:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    - $ref: '#/components/parameters/BlobHash'
    put:
      operationId: uploadBlob
      tags:
      - Sync
      summary: Upload a blob (up to 8 MiB)
      description: The server checks the bytes against the hash and the plan's limits. Larger blobs use upload sessions.
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '204':
          description: Stored (or already present).
        default:
          $ref: '#/components/responses/Error'
    get:
      operationId: downloadBlob
      tags:
      - Sync
      summary: Download a blob
      parameters:
      - name: Range
        in: header
        required: false
        description: One range, `bytes=start-end` or `bytes=start-`.
        schema:
          type: string
          maxLength: 100
      responses:
        '200':
          description: The whole blob.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '206':
          description: The requested range.
          headers:
            Content-Range:
              schema:
                type: string
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/uploads:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    post:
      operationId: createUpload
      tags:
      - Sync
      summary: Start a resumable upload
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadCreate'
      responses:
        '201':
          description: Session created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadStatus'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/uploads/{upload_id}:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    - $ref: '#/components/parameters/UploadID'
    get:
      operationId: getUpload
      tags:
      - Sync
      summary: How much of an upload arrived
      responses:
        '200':
          description: Status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadStatus'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: appendUpload
      tags:
      - Sync
      summary: Send the next part (up to 16 MiB)
      description: '`Upload-Offset` must equal the bytes received so far (code `upload_offset_mismatch` otherwise).'
      parameters:
      - name: Upload-Offset
        in: header
        required: true
        schema:
          type: integer
          format: int64
          minimum: 0
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: Status after this part.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadStatus'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/uploads/{upload_id}/finish:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    - $ref: '#/components/parameters/UploadID'
    post:
      operationId: finishUpload
      tags:
      - Sync
      summary: Finish an upload
      description: Checks the bytes against the hash and stores the blob.
      responses:
        '204':
          description: Stored.
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/files/{file_id}/versions:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    - $ref: '#/components/parameters/FileID'
    get:
      operationId: listVersions
      tags:
      - Sync
      summary: A file's version history, newest first
      description: Superseded versions are kept for the plan's history period (PLAN.md 1.2).
      parameters:
      - name: before
        in: query
        required: false
        description: Return versions with a lower seq (from `next_before` of the previous page).
        schema:
          type: integer
          format: int64
          minimum: 1
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: A page of versions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VersionList'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/files/{file_id}/versions/{version_id}:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    - $ref: '#/components/parameters/FileID'
    - $ref: '#/components/parameters/VersionID'
    get:
      operationId: getVersion
      tags:
      - Sync
      summary: One version
      description: Download its content with `GET /v1/vaults/{vault_id}/blobs/{blob_hash}`.
      responses:
        '200':
          description: The version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Version'
        default:
          $ref: '#/components/responses/Error'
  /v1/vaults/{vault_id}/files/{file_id}/versions/{version_id}/restore:
    parameters:
    - $ref: '#/components/parameters/VaultID'
    - $ref: '#/components/parameters/FileID'
    - $ref: '#/components/parameters/VersionID'
    post:
      operationId: restoreVersion
      tags:
      - Sync
      summary: Make an old version current again
      description: Commits a new version with the old content at the file's current path (a deleted file comes back).
      responses:
        '200':
          description: Restored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResult'
        '409':
          description: The file changed meanwhile.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/CommitConflictProblem'
        default:
          $ref: '#/components/responses/Error'
components:
  schemas:
    CommitConflict:
      type: object
      required:
      - op_index
      - code
      properties:
        op_index:
          type: integer
        code:
          type: string
          enum:
          - version_mismatch
          - path_taken
          - file_not_found
        current:
          $ref: '#/components/schemas/FileState'
    ChangesPage:
      type: object
      required:
      - changes
      - head_seq
      - has_more
      properties:
        changes:
          type: array
          items:
            $ref: '#/components/schemas/Change'
        head_seq:
          type: integer
          format: int64
        has_more:
          type: boolean
    UploadStatus:
      type: object
      required:
      - upload_id
      - hash
      - size
      - received
      properties:
        upload_id:
          type: string
        hash:
          type: string
        size:
          type: integer
          format: int64
        received:
          type: integer
          format: int64
    FieldError:
      type: object
      required:
      - field
      - detail
      properties:
        field:
          type: string
          description: JSON path of the invalid field, for example `ops[2].path`.
        detail:
          type: string
    Actor:
      type: object
      required:
      - id
      - email
      - name
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
        name:
          type: string
    CommitConflictProblem:
      type: object
      description: A problem document (type conflict, code commit_conflict) with the conflicts.
      required:
      - type
      - title
      - status
      - conflicts
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        code:
          type: string
        request_id:
          type: string
        conflicts:
          type: array
          items:
            $ref: '#/components/schemas/CommitConflict'
    TreePage:
      type: object
      required:
      - head_seq
      - files
      properties:
        head_seq:
          type: integer
          format: int64
          description: The seq this snapshot reflects; pass it as `at` for later pages.
        files:
          type: array
          items:
            $ref: '#/components/schemas/TreeFile'
        next_cursor:
          type: string
    Restriction:
      type: object
      required:
      - id
      - folder
      - private
      - created_at
      properties:
        id:
          type: string
          format: uuid
        folder:
          type: string
          description: The restricted folder or note.
        private:
          type: boolean
        private_to:
          type: string
          format: uuid
          description: The member a private folder belongs to.
        created_at:
          type: string
          format: date-time
    Problem:
      type: object
      description: RFC 9457 problem details.
      required:
      - type
      - title
      - status
      properties:
        type:
          type: string
          format: uri
          examples:
          - https://granite.md/problems/conflict
        title:
          type: string
        status:
          type: integer
          format: int32
        detail:
          type: string
        instance:
          type: string
        code:
          type: string
          description: Machine-readable refinement of `type`, for example `version_conflict`.
        request_id:
          type: string
        errors:
          type: array
          description: Per-field validation errors.
          items:
            $ref: '#/components/schemas/FieldError'
    CommitOpResult:
      type: object
      required:
      - op_index
      - file_id
      - version_id
      - seq
      properties:
        op_index:
          type: integer
        file_id:
          type: string
          format: uuid
        version_id:
          type: string
          format: uuid
        seq:
          type: integer
          format: int64
    BlobCheckResult:
      type: object
      required:
      - missing
      properties:
        missing:
          type: array
          items:
            type: string
    CommitResult:
      type: object
      required:
      - results
      - head_seq
      properties:
        head_seq:
          type: integer
          format: int64
        results:
          type: array
          items:
            $ref: '#/components/schemas/CommitOpResult'
    Change:
      type: object
      required:
      - seq
      - op
      - file_id
      - version_id
      - path
      - size
      - content_type
      - kind
      - deleted
      - author_id
      - committed_at
      properties:
        seq:
          type: integer
          format: int64
        op:
          type: string
          enum:
          - put
          - rename
          - delete
        file_id:
          type: string
          format: uuid
        version_id:
          type: string
          format: uuid
        path:
          type: string
        old_path:
          type: string
        blob_hash:
          type: string
          description: Absent for deletes.
        size:
          type: integer
          format: int64
        content_type:
          type: string
        kind:
          type: string
          enum:
          - markdown
          - text
          - binary
        deleted:
          type: boolean
        author_id:
          type: string
          format: uuid
        device_id:
          type: string
          format: uuid
        committed_at:
          type: string
          format: date-time
        client_mtime:
          type: string
          format: date-time
    FileState:
      type: object
      required:
      - file_id
      - version_id
      - path
      - deleted
      properties:
        file_id:
          type: string
          format: uuid
        version_id:
          type: string
          format: uuid
        path:
          type: string
        deleted:
          type: boolean
    TreeFile:
      type: object
      required:
      - file_id
      - version_id
      - path
      - blob_hash
      - size
      - content_type
      - kind
      properties:
        file_id:
          type: string
          format: uuid
        version_id:
          type: string
          format: uuid
        path:
          type: string
        blob_hash:
          type: string
        size:
          type: integer
          format: int64
        content_type:
          type: string
        kind:
          type: string
          enum:
          - markdown
          - text
          - binary
        client_mtime:
          type: string
          format: date-time
    PathAccess:
      type: object
      required:
      - path
      - private
      - restricted
      - restricted_at
      - can_manage
      - can_make_private
      - openers
      properties:
        path:
          type: string
        private:
          type: boolean
          description: Inside a private folder (only its member can open it).
        restricted:
          type: boolean
          description: Inside a folder limited to its own grants.
        restricted_at:
          type: string
          description: The innermost restricted or private folder covering the path ("" when none).
        restriction:
          $ref: '#/components/schemas/Restriction'
        can_manage:
          type: boolean
          description: The caller may restrict the path and choose who opens it (workspace owners and admins).
        can_make_private:
          type: boolean
          description: The caller may make it private, or lift its privacy.
        openers:
          type: array
          items:
            $ref: '#/components/schemas/Opener'
    UploadCreate:
      type: object
      required:
      - hash
      - size
      properties:
        hash:
          type: string
          pattern: ^[0-9a-f]{64}$
        size:
          type: integer
          format: int64
          minimum: 0
    CommitRequest:
      type: object
      required:
      - ops
      properties:
        device_id:
          type: string
          format: uuid
        ops:
          type: array
          maxItems: 1000
          items:
            $ref: '#/components/schemas/CommitOp'
    NoteInfo:
      type: object
      required:
      - file_id
      - path
      - created_at
      - modified_at
      - size
      - versions
      - access
      - shared_with
      properties:
        file_id:
          type: string
          format: uuid
        path:
          type: string
        created_at:
          type: string
          format: date-time
        created_by:
          $ref: '#/components/schemas/Actor'
        modified_at:
          type: string
          format: date-time
        modified_by:
          $ref: '#/components/schemas/Actor'
        size:
          type: integer
          format: int64
        versions:
          type: integer
          format: int64
        access:
          $ref: '#/components/schemas/PathAccess'
        shared_with:
          type: array
          description: Emails a personal note was shared with (the vault's owner sees them).
          items:
            type: string
    BlobCheckRequest:
      type: object
      required:
      - hashes
      properties:
        hashes:
          type: array
          maxItems: 1000
          items:
            type: string
            pattern: ^[0-9a-f]{64}$
    Opener:
      type: object
      required:
      - kind
      - access
      - folder
      properties:
        kind:
          type: string
          enum:
          - owner
          - user
          - group
          - admin
          description: The personal vault's owner, a member, a team (group), or a workspace owner or admin.
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
        access:
          type: string
          enum:
          - read
          - comment
          - write
          - admin
        folder:
          type: string
          description: Where the grant was made; "" for the whole vault.
        members:
          type: integer
          format: int64
          description: For teams.
        grant_id:
          type: string
          format: uuid
    CommitOp:
      type: object
      required:
      - type
      description: See Appendix B.3 for which fields each type needs.
      properties:
        type:
          type: string
          enum:
          - put
          - rename
          - delete
        file_id:
          type: string
          format: uuid
        base_version_id:
          type: string
          format: uuid
        path:
          type: string
          maxLength: 4096
        new_path:
          type: string
          maxLength: 4096
        blob_hash:
          type: string
          maxLength: 64
        size:
          type: integer
          format: int64
          minimum: 0
        content_type:
          type: string
          maxLength: 200
        client_mtime:
          type: string
          format: date-time
    VersionList:
      type: object
      required:
      - versions
      properties:
        versions:
          type: array
          items:
            $ref: '#/components/schemas/Version'
        next_before:
          type: integer
          format: int64
    Version:
      type: object
      required:
      - id
      - file_id
      - seq
      - op
      - path
      - size
      - content_type
      - deleted
      - author_id
      - created_at
      properties:
        id:
          type: string
          format: uuid
        file_id:
          type: string
          format: uuid
        seq:
          type: integer
          format: int64
        op:
          type: string
          enum:
          - put
          - rename
          - delete
        path:
          type: string
        blob_hash:
          type: string
          description: Absent for deletions.
        size:
          type: integer
          format: int64
        content_type:
          type: string
        deleted:
          type: boolean
        author_id:
          type: string
          format: uuid
        author_name:
          type: string
          description: The author's name ("" for bots and unknown authors).
        author_email:
          type: string
        device_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
  headers:
    RequestID:
      description: Identifier of this request, for support and log correlation.
      schema:
        type: string
  parameters:
    BlobHash:
      name: hash
      in: path
      required: true
      schema:
        type: string
        pattern: ^[0-9a-f]{64}$
    UploadID:
      name: upload_id
      in: path
      required: true
      schema:
        type: string
        pattern: ^[0-9a-f]{32}$
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Makes a non-idempotent POST safe to retry. See the conventions above.
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: ^[\x21-\x7E]+$
    VaultID:
      name: vault_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum items to return.
      schema:
        type: integer
        format: int32
        minimum: 1
        maximum: 200
        default: 50
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque cursor from a previous page's `next_cursor`.
      schema:
        type: string
        maxLength: 512
    VersionID:
      name: version_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    FileID:
      name: file_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
  responses:
    Error:
      description: Error response.
      headers:
        X-Request-ID:
          $ref: '#/components/headers/RequestID'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Session access token, personal API token, or OAuth access token.