> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fanfeed.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Record that a library scan finished

> Call this **once at the end of a completed library scan**, not per batch.

`has_synced` decides whether the next scan is a full library scan or a narrow
incremental one. FanFeed sets it to `true` only when a completed scan examined the
library (`photos_processed` greater than zero, or omitted while media was received
during the scan), and never sets it back to `false`.

That asymmetry is deliberate. If the flag is set after a scan that never ran (say the
user denied photo permission, or the app crashed mid-scan), the account is stuck on
the incremental window permanently and never recovers. Pass an honest
`photos_processed`: the count of assets examined, `0` when the scan did not complete.

Idempotent: calling it twice advances `last_sync_at` twice and is otherwise a no-op.




## OpenAPI

````yaml /api-reference/openapi.yaml post /users/{user_id}/sync-complete
openapi: 3.1.0
info:
  title: FanFeed Partner API
  version: 1.0.0
  description: >
    FanFeed turns a user's camera roll into an attended-event history. Your app
    sends photo

    *metadata*; FanFeed matches it against live-event data and returns the
    events those photos

    were taken at, along with the stats derived from that history.


    Five endpoints cover the whole integration:


    | Endpoint | Purpose |

    |---|---|

    | `POST /users` | Create a FanFeed user, once, when your user opts in |

    | `POST /users/{user_id}/media` | Send photo metadata in batches; get
    matched events back |

    | `POST /users/{user_id}/sync-complete` | Tell FanFeed a library scan
    finished |

    | `GET /users/{user_id}/events` | The user's matched event history |

    | `GET /users/{user_id}/stats` | Profile statistics |
  contact:
    name: FanFeed Engineering
    email: engineering@fanfeed.ai
servers:
  - url: https://api.fanfeed.ai/v1
    description: Production
  - url: https://api-dev.fanfeed.ai/v1
    description: Development
security:
  - PartnerApiKey: []
tags:
  - name: Users
    description: Creating a FanFeed user for one of your users.
  - name: Media
    description: Batch camera-roll metadata ingest and event matching.
  - name: Sync
    description: Recording that a library scan finished.
  - name: Events
    description: A user's matched event history.
  - name: Stats
    description: Profile statistics derived from the event history.
paths:
  /users/{user_id}/sync-complete:
    post:
      tags:
        - Sync
      summary: Record that a library scan finished
      description: >
        Call this **once at the end of a completed library scan**, not per
        batch.


        `has_synced` decides whether the next scan is a full library scan or a
        narrow

        incremental one. FanFeed sets it to `true` only when a completed scan
        examined the

        library (`photos_processed` greater than zero, or omitted while media
        was received

        during the scan), and never sets it back to `false`.


        That asymmetry is deliberate. If the flag is set after a scan that never
        ran (say the

        user denied photo permission, or the app crashed mid-scan), the account
        is stuck on

        the incremental window permanently and never recovers. Pass an honest

        `photos_processed`: the count of assets examined, `0` when the scan did
        not complete.


        Idempotent: calling it twice advances `last_sync_at` twice and is
        otherwise a no-op.
      operationId: completeSync
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SyncCompleteRequest'
      responses:
        '200':
          description: Sync recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncCompleteResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  parameters:
    UserId:
      name: user_id
      in: path
      required: true
      description: The FanFeed user id returned by `POST /users`.
      schema:
        type: string
        format: uuid
        examples:
          - 8f14e45f-ceea-467a-9a1e-2b4d9c3f0a11
  schemas:
    SyncCompleteRequest:
      properties:
        photos_processed:
          description: >
            Assets your scan examined, whether or not you sent them. Pass `0`
            when the scan

            did not complete. Omit it and FanFeed infers the answer from what it
            received

            during the scan.
          examples:
            - 1284
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
      type: object
    SyncCompleteResponse:
      properties:
        has_synced:
          description: >
            Whether this user has ever completed a scan that examined the
            library. Decides

            full vs. incremental on the next run. Never returns to `false` once
            `true`.
          examples:
            - true
          type: boolean
        last_sync_at:
          description: >
            Server-side timestamp of the last time FanFeed received media or a
            `sync-complete`

            for this user, advanced by this call.
          examples:
            - '2026-08-25T18:44:12Z'
          format: date-time
          type: string
      required:
        - has_synced
        - last_sync_at
      type: object
    Error:
      description: The published error envelope.
      properties:
        error:
          description: The ``error`` member of the envelope.
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
              description: Stable, machine-readable error code.
            message:
              description: Human-readable explanation. Not intended for end users.
              examples:
                - Send at most 200 media items per request.
              type: string
            details:
              description: >
                Optional per-field or per-item context. Present only where it
                adds something;

                its shape varies by code.
              anyOf:
                - additionalProperties: true
                  type: object
                - type: 'null'
          required:
            - code
            - message
          type: object
      required:
        - error
      type: object
    ErrorCode:
      description: >
        Stable, machine-readable error code. Branch on this, not on `message`.


        Note that per-item rejections inside a media batch are *not* errors: the
        request

        still returns `200` and the items appear in `rejected[]` with a
        `reason`. An

        `error.code` means the whole request failed.


        The first nine codes are published application errors. The last four are

        framework-level codes that malformed or unsupported requests can trigger
        (for

        example, a non-JSON body returns `unsupported_media_type` and an unknown
        path

        returns `not_found`). Treat any unrecognized code as a retry or fix-once
        failure

        according to the HTTP status.
      examples:
        - invalid_batch_size
      enum:
        - invalid_api_key
        - user_not_found
        - invalid_email
        - empty_batch
        - invalid_batch_size
        - invalid_media_item
        - invalid_cursor
        - rate_limited
        - internal_error
        - unsupported_media_type
        - invalid_request
        - not_found
        - method_not_allowed
      type: string
  responses:
    BadRequest:
      description: The request was malformed or failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: No FanFeed user with that id.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnsupportedMediaType:
      description: Request body must be application/json.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: >
        Rate limited. Nothing is throttled today, but the limiter exists, so
        build for this

        from day one: honor `Retry-After` and back off.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    PartnerApiKey:
      type: apiKey
      in: header
      name: X-PARTNER-API-KEY
      description: >
        Your FanFeed partner API key, issued to your organization.


        FanFeed trusts your authentication: the key identifies you, and the
        `user_id` in the

        path identifies which of your users the request is for. FanFeed performs
        no end-user

        authentication of its own.


        Treat the key as a server-side secret. See the guide for what that
        implies for the

        camera-roll ingest path, which is the one call that naturally originates
        on-device.

````