---
title: "Report confirmed local writes (sync receipt)"
method: POST
path: "/api/v1/library/sync-receipts"
tags: ["Library"]
---

# Report confirmed local writes (sync receipt)

`POST /api/v1/library/sync-receipts`

## Request

- Base URL: `https://skillrepo.dev`
- URL: `https://skillrepo.dev/api/v1/library/sync-receipts`
- Auth: HTTP bearer

Posted by `skillrepo update` AFTER it confirms skills were written to disk. Records a delivery event + stamps subscriber sync state from the (owner, name, version) pairs actually written. A confirmed disk write — not a fetch response — is the single source of truth for sync status (#1832).

Entries that don't resolve to a skill the caller can access, or to a known version label, are silently dropped (stale local copy or bogus payload); the `recorded` count reflects only what was stored. An empty `skills` array is accepted and records nothing.

**Per-repo attestation**: an optional `repo` block reports which skillset the syncing repo declares and the exact (skill, version) set it is serving, with skipped-extras and counts-only violation drift signals. Recorded only for accounts with skillsets enabled; silently ignored otherwise. Additive — older CLIs omit it and get byte-identical behavior.

Rate-limit bucket: `cli-crud`.

## Request body

- SyncReceiptBody: Posted by `skillrepo update` after a successful local write. The server records a delivery event + stamps subscriber sync state ONLY from receipts — a confirmed disk write is the single source of truth for sync status (#1832).
  - `syncedAt` string, date-time, required: Client timestamp of the sync that produced these writes. Used as the delivery event time and the per-UTC-day dedup key. Rejected (400) if more than 5 minutes in the future: a receipt records a write that already happened, and an unbounded future timestamp could otherwise game the fleet-view freshness buckets or burn a future dedup slot. The CLI sends the server's own response `syncedAt`, so a legitimate receipt is always at/near now.
  - `skills` SyncReceiptEntry[], required: Skills written to disk on this sync. An empty array is accepted and records nothing (no-op).
    - `owner` string, required
    - `name` string, required
    - `version` string, required: The version LABEL the CLI actually wrote to disk (e.g. "1.4"), not necessarily the skill's current version. The server resolves it against the unique (skill, label) index and records the delivery at exactly that version.
  - `repo` SyncReceiptRepoBlock: Optional per-repo attestation on a sync receipt: which skillset this repo declares and the exact (skill, version) set it is serving. Requires a plan with skillsets enabled for the account; silently ignored otherwise — a receipt is telemetry ingestion and never fails into a governance error.
    - `name` string, required: The declaration's self-declared repo name — the same value the skillset-scoped sync sends as its `repo` param, under the same rules (max 200 chars, no control characters). Two checkouts declaring one name merge server-side into one repo state.
    - `skillset` string, required: The declared `owner/name` skillset pointer, verbatim from `skillrepo.json`.
    - `frozen` boolean: True for headless/CI runs syncing from a recorded (frozen) resolution. Recorded, but excluded from conformance metrics.
    - `resolved` SyncReceiptRepoResolvedEntry[], required: The FULL set this repo is serving on this sync — re-asserted on every sync, including 304 re-assertions, so the server snapshot always reflects the repo's current state.
      - `owner` string, required
      - `name` string, required
      - `version` string, required
      - `written` boolean, required: Whether this entry was actually written to disk on this sync. `false` means served in the resolved set but not written — e.g. a locally-modified skill directory the CLI refused to overwrite.
    - `skippedExtras` object[]: Declared `extra` entries the sync response reported as skipped, echoed back so the fleet view can show declared-but-unhonored extras. `reason` is the stable machine axis (`extras_disabled` or `not_in_library` today); presentation messages never cross this wire.
      - `skill` string, required
      - `reason` string, required
    - `violations` object[]: Counts-only summary of foreign content detected in managed skill directories (category `unmanaged` today). Only counts and categories cross the wire — names of unmanaged content never leave the machine.
      - `category` string, required
      - `count` integer, required

## Response `200`

Receipt processed.

- SyncReceiptResponse: Result of recording a sync receipt.
  - `recorded` integer, required: How many (skill, version) pairs resolved to an accessible skill and a real version label and were recorded. Entries that don't resolve are silently dropped and excluded from this count.

## Other responses

- `400`: Invalid request — malformed JSON, missing required fields, or failed schema validation. Body matches `ErrorEnvelope`.
- `401`: Missing or invalid authentication. For routes that require `bearerAccessKey` or `bearerCatalogKey`.
- `403`: Authenticated, but the key lacks the required scope or the account is suspended. Use `code` to distinguish: `insufficient_scope`, `account_suspended`, `plan_limit`.
- `429`: Rate limit exceeded on the route's bucket. Includes `X-RateLimit-*` headers indicating the limit, remaining, and reset time.

---

[API](https://skmtc.dev/skillrepo/apis/skillrepo-api.md) · [All operations](https://skmtc.dev/skillrepo/apis/skillrepo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/skillrepo/apis/skillrepo-api/revisions/b62bac246918?raw)
