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

# Scan a card

> Identify a card from a photo.

`POST /v1/scan` identifies a trading card from a photo of it. Send a cropped, roughly
straight-on photo of a single card. You get back the closest catalog matches, each with a
confidence score.

## Send a file

Send `multipart/form-data` with the photo in an `image` field (max 10 MB):

```bash theme={null}
curl -X POST https://api.cromos.so/v1/scan \
  -H "Authorization: Bearer $API_KEY" \
  -F "image=@card.jpg"
```

## Send a URL

Send `application/json` with a publicly reachable `https` URL (max 10 MB, no redirects):

```bash theme={null}
curl -X POST https://api.cromos.so/v1/scan \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://images.example.com/card.jpg"}'
```

## The response

```json theme={null}
{
  "data": {
    "analysis": { "game": "pokemon" },
    "matches": [
      {
        "score": 0.91,
        "card": {
          "id": "base1-4",
          "name": "Charizard",
          "category": "Pokemon",
          "rarity": "Rare",
          "number": "4",
          "image": {
            "low": "https://assets.cromos.so/cards/base1-4/low.webp",
            "high": "https://assets.cromos.so/cards/base1-4/high.webp"
          },
          "expansion": { "id": "base1", "name": "Base Set" }
        }
      }
    ]
  },
  "error": null
}
```

* `score` is a similarity from 0 to 1, higher is better. Matches are best-first.
* A confident scan returns a single match. A close race returns up to five, so show them and let
  the user pick.
* **An empty `matches` (with `"game": null`) is a valid 200**, not an error. Nothing in the
  catalog cleared the confidence floor. Prompt for a retake: closer, straighter, less glare.
* Photos are not stored.

## Errors

| Status | Code                | Meaning                                                                                |
| ------ | ------------------- | -------------------------------------------------------------------------------------- |
| 400    | `bad_request`       | Body is not one of the two supported shapes                                            |
| 400    | `unsupported_image` | Bytes are not a decodable image                                                        |
| 400    | `url_not_allowed`   | URL is not `https`, or resolves to a non-public address                                |
| 400    | `url_fetch_failed`  | URL could not be fetched, redirected, timed out after 10 seconds, or exceeded the size |
| 413    | `payload_too_large` | Image larger than 10 MB                                                                |
| 503    | `scan_unavailable`  | Scanning is not configured on this deployment                                          |


## OpenAPI

````yaml openapi.json POST /v1/scan
openapi: 3.0.0
info:
  title: Cromos API
  version: 1.0.0
servers:
  - url: https://api.cromos.so
security:
  - bearerAuth: []
paths:
  /v1/scan:
    post:
      tags:
        - Scanning
      summary: Scan a card
      description: >-
        Identifies a trading card from a photo. Accepts either
        multipart/form-data with an `image` field or JSON with a public https
        `url`, up to 10 MB. Returns the closest catalog matches with a
        confidence score each. An empty `matches` array is a valid 200, not an
        error: nothing cleared the confidence floor.
      operationId: scanCard
      requestBody:
        required: false
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                image:
                  type: string
                  format: binary
              required:
                - image
          application/json:
            schema:
              $ref: '#/components/schemas/ScanUrlBody'
      responses:
        '200':
          description: >-
            Identification result. Empty matches with a null game is a valid
            outcome: nothing cleared the confidence floor.
          headers:
            ETag:
              description: >-
                A strong validator over the response body. Send it back as
                `If-None-Match` to get a `304 Not Modified` when nothing has
                changed.
              schema:
                type: string
            RateLimit:
              description: >-
                Remaining budget in the current window, RFC 9239 draft-7 form:
                `limit=300, remaining=284, reset=41`. `reset` is seconds until
                the window rolls.
              schema:
                type: string
            RateLimit-Policy:
              description: >-
                The policy the budget above is drawn from. `300;w=60` is 300
                requests a minute.
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ScanResponse'
                  error:
                    type: object
                    nullable: true
                required:
                  - data
                  - error
        '304':
          description: >-
            Not modified. The `If-None-Match` you sent still matches, and there
            is no body.
          headers:
            ETag:
              description: >-
                A strong validator over the response body. Send it back as
                `If-None-Match` to get a `304 Not Modified` when nothing has
                changed.
              schema:
                type: string
            RateLimit:
              description: >-
                Remaining budget in the current window, RFC 9239 draft-7 form:
                `limit=300, remaining=284, reset=41`. `reset` is seconds until
                the window rolls.
              schema:
                type: string
            RateLimit-Policy:
              description: >-
                The policy the budget above is drawn from. `300;w=60` is 300
                requests a minute.
              schema:
                type: string
        '400':
          description: Bad request
          headers:
            ETag:
              description: >-
                A strong validator over the response body. Send it back as
                `If-None-Match` to get a `304 Not Modified` when nothing has
                changed.
              schema:
                type: string
            RateLimit:
              description: >-
                Remaining budget in the current window, RFC 9239 draft-7 form:
                `limit=300, remaining=284, reset=41`. `reset` is seconds until
                the window rolls.
              schema:
                type: string
            RateLimit-Policy:
              description: >-
                The policy the budget above is drawn from. `300;w=60` is 300
                requests a minute.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          headers:
            ETag:
              description: >-
                A strong validator over the response body. Send it back as
                `If-None-Match` to get a `304 Not Modified` when nothing has
                changed.
              schema:
                type: string
            RateLimit:
              description: >-
                Remaining budget in the current window, RFC 9239 draft-7 form:
                `limit=300, remaining=284, reset=41`. `reset` is seconds until
                the window rolls.
              schema:
                type: string
            RateLimit-Policy:
              description: >-
                The policy the budget above is drawn from. `300;w=60` is 300
                requests a minute.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          description: Image larger than 10 MB
          headers:
            ETag:
              description: >-
                A strong validator over the response body. Send it back as
                `If-None-Match` to get a `304 Not Modified` when nothing has
                changed.
              schema:
                type: string
            RateLimit:
              description: >-
                Remaining budget in the current window, RFC 9239 draft-7 form:
                `limit=300, remaining=284, reset=41`. `reset` is seconds until
                the window rolls.
              schema:
                type: string
            RateLimit-Policy:
              description: >-
                The policy the budget above is drawn from. `300;w=60` is 300
                requests a minute.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limited
          headers:
            ETag:
              description: >-
                A strong validator over the response body. Send it back as
                `If-None-Match` to get a `304 Not Modified` when nothing has
                changed.
              schema:
                type: string
            RateLimit:
              description: >-
                Remaining budget in the current window, RFC 9239 draft-7 form:
                `limit=300, remaining=284, reset=41`. `reset` is seconds until
                the window rolls.
              schema:
                type: string
            RateLimit-Policy:
              description: >-
                The policy the budget above is drawn from. `300;w=60` is 300
                requests a minute.
              schema:
                type: string
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal error
          headers:
            ETag:
              description: >-
                A strong validator over the response body. Send it back as
                `If-None-Match` to get a `304 Not Modified` when nothing has
                changed.
              schema:
                type: string
            RateLimit:
              description: >-
                Remaining budget in the current window, RFC 9239 draft-7 form:
                `limit=300, remaining=284, reset=41`. `reset` is seconds until
                the window rolls.
              schema:
                type: string
            RateLimit-Policy:
              description: >-
                The policy the budget above is drawn from. `300;w=60` is 300
                requests a minute.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Scanning not configured on this deployment
          headers:
            ETag:
              description: >-
                A strong validator over the response body. Send it back as
                `If-None-Match` to get a `304 Not Modified` when nothing has
                changed.
              schema:
                type: string
            RateLimit:
              description: >-
                Remaining budget in the current window, RFC 9239 draft-7 form:
                `limit=300, remaining=284, reset=41`. `reset` is seconds until
                the window rolls.
              schema:
                type: string
            RateLimit-Policy:
              description: >-
                The policy the budget above is drawn from. `300;w=60` is 300
                requests a minute.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                data: null
                error:
                  code: scan_unavailable
                  message: scanning is not configured
components:
  schemas:
    ScanUrlBody:
      type: object
      properties:
        url:
          type: string
      required:
        - url
    ScanResponse:
      type: object
      properties:
        analysis:
          type: object
          properties:
            game:
              type: string
              nullable: true
          required:
            - game
        matches:
          type: array
          items:
            $ref: '#/components/schemas/ScanMatch'
      required:
        - analysis
        - matches
    ErrorResponse:
      type: object
      properties:
        data:
          type: object
          nullable: true
        error:
          $ref: '#/components/schemas/ApiError'
      required:
        - data
        - error
    ScanMatch:
      type: object
      properties:
        score:
          type: number
        card:
          $ref: '#/components/schemas/ScanCard'
      required:
        - score
        - card
    ApiError:
      type: object
      properties:
        code:
          type: string
          description: A stable machine-readable token. Switch on this, not on `message`.
        message:
          type: string
          description: A human-readable sentence. Always present, never empty.
      required:
        - code
        - message
    ScanCard:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        category:
          type: string
          nullable: true
        rarity:
          type: string
          nullable: true
        number:
          type: string
          nullable: true
          description: The printed collector number, e.g. `TG01`.
        image:
          $ref: '#/components/schemas/ImageUrls'
        expansion:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
          required:
            - id
            - name
      required:
        - id
        - name
        - category
        - rarity
        - number
        - image
        - expansion
    ImageUrls:
      type: object
      nullable: true
      properties:
        low:
          type: string
          format: uri
          description: Thumbnail-sized rendition. May be the same URL as `high`.
        high:
          type: string
          format: uri
          description: Full-sized rendition. May be the same URL as `low`.
      required:
        - low
        - high
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

````