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

# Serial Verification (gs1:verificationService)

> Verify serialized GTIN scans against the Closient resolver — tiered, rate-limited, enumeration-safe.

<Note>
  **Experimental (v1):** This linkType response shape is published as a v1 experimental contract. The auth-tier split, response fields, and `verificationStatus` enum may change before the first external integration freezes the shape. Closient will announce a stable v1.0 once an external scanner or 2D-cert tool integrates against the response.
</Note>

The `gs1:verificationService` linkType returns the verification status of a serialized trade item — a **GTIN + AI 21 serial** pair scanned against the Closient resolver. Two tiers are exposed:

| Tier        | Caller                                      | Auth        | Rate limit                | Payload                                          |
| ----------- | ------------------------------------------- | ----------- | ------------------------- | ------------------------------------------------ |
| **Minimal** | Consumer scan (browser, native scanner)     | none        | 60 / hour / IP            | 5 fields, no scan history                        |
| **Full**    | Brand dashboard, retailer POS, 2D-cert tool | `X-API-Key` | per-key (default 300/min) | scan history, anomaly entries, prior-event hints |

## Endpoint

```
GET https://www.closient.com/resolver/api/v1/verify/01/{gtin}/21/{serial}
```

Path parameters mirror the GS1 Digital Link AI ordering used elsewhere in the resolver (`01` = GTIN, `21` = serial).

| Parameter | Format                          | Notes                                                        |
| --------- | ------------------------------- | ------------------------------------------------------------ |
| `gtin`    | 8–14 digit GTIN                 | GTIN-8/12/13/14; non-digit input returns `422 invalid_gtin`. |
| `serial`  | 1–20 printable ASCII characters | AI 21. Out-of-range characters return `422 invalid_serial`.  |

## Authentication

Authenticated calls supply an organization API key in the `X-API-Key` header. The same key shape used elsewhere in the Closient API is accepted (`csb_*`).

```http theme={null}
GET /resolver/api/v1/verify/01/09521101530018/21/SN001 HTTP/1.1
Host: www.closient.com
X-API-Key: csb_<body>_<checksum>
```

Calls without `X-API-Key` (or with an unknown key) fall through to the minimal-tier response and are subject to the IP rate limit.

## Unauthenticated response (consumer)

```json theme={null}
{
  "verificationStatus": "authentic",
  "gtin": "09521101530018",
  "serialNumber": "SN001",
  "verifiedAt": "2026-05-09T20:00:00Z",
  "recommendation": "proceed"
}
```

| Field                | Type               | Description                                                                         |
| -------------------- | ------------------ | ----------------------------------------------------------------------------------- |
| `verificationStatus` | enum               | `authentic`, `suspect`, `counterfeit_suspected`, `serialization_error`              |
| `gtin`               | string             | Echoed GTIN from the request path                                                   |
| `serialNumber`       | string             | Echoed AI 21 serial from the request path                                           |
| `verifiedAt`         | ISO 8601 timestamp | When this response was generated                                                    |
| `recommendation`     | enum               | `proceed` for authentic / unverified / unknown serials, `flag_for_review` otherwise |

### Enumeration protection

Serials we have never seen return the **same shape** as known-authentic serials (`verificationStatus: "authentic"`, `recommendation: "proceed"`). Consumer-facing scanners cannot use this endpoint to probe whether a particular serial is in the Closient database. Brands needing the truthful "we have not seen this serial" signal must authenticate — the auth-tier response returns `scanHistory: null` for never-seen serials.

### Rate limit

Unauthenticated calls are limited to **60 requests per hour per client IP**. The 61st call returns `429 Too Many Requests` with a `Retry-After` header indicating the seconds remaining in the bucket.

## Authenticated response (brand / retailer)

```json theme={null}
{
  "verificationStatus": "suspect",
  "gtin": "09521101530018",
  "serialNumber": "DUPE001",
  "verifiedAt": "2026-05-09T20:00:00Z",
  "scanHistory": {
    "totalScans": 5,
    "retailerScans": 2,
    "consumerScans": 3,
    "distinctRetailLocations": 2,
    "firstSeen": "2026-04-01T10:00:00Z"
  },
  "anomalies": [
    {
      "type": "duplicate_retail_scan",
      "description": "Serial previously scanned at POS at a different retail location.",
      "priorEvent": {
        "location": "49.2827,-123.1207",
        "retailer": "RetailerA",
        "scannedAt": "2026-04-10T14:30:00Z"
      }
    }
  ],
  "recommendation": "flag_for_review"
}
```

The auth-tier response is a strict superset of the unauth-tier response — every consumer field is preserved verbatim.

### `scanHistory`

| Field                     | Type               | Description                                        |
| ------------------------- | ------------------ | -------------------------------------------------- |
| `totalScans`              | int ≥ 0            | All scan-context counters summed                   |
| `retailerScans`           | int ≥ 0            | Authenticated retailer-system scans                |
| `consumerScans`           | int ≥ 0            | Browser / unauthenticated scans                    |
| `distinctRetailLocations` | int ≥ 0            | Unique POS locations that have scanned this serial |
| `firstSeen`               | ISO 8601 timestamp | Earliest observed scan                             |

`scanHistory` is **`null`** when the serial has never been observed by the resolver (auth tier only — the unauth tier omits the field entirely).

### `anomalies`

Empty array when no rule has fired. Phase 1 emits a single rule:

| `type`                  | Trigger                                             | `priorEvent`                                   |
| ----------------------- | --------------------------------------------------- | ---------------------------------------------- |
| `duplicate_retail_scan` | `retailerScans ≥ 2 AND distinctRetailLocations ≥ 2` | Most recent retailer scan from a different POS |

Each anomaly entry carries:

| Field                  | Type               | Description                                                                                        |
| ---------------------- | ------------------ | -------------------------------------------------------------------------------------------------- |
| `type`                 | string             | Stable rule identifier                                                                             |
| `description`          | string             | Human-readable explanation                                                                         |
| `priorEvent.location`  | string             | `lat,lon` coordinates (4 decimal places), `GLN:<gln>` when only a GLN is captured, or empty string |
| `priorEvent.retailer`  | string             | Retailer organization name when the prior scan was authenticated, empty otherwise                  |
| `priorEvent.scannedAt` | ISO 8601 timestamp | Prior event time (UTC)                                                                             |

Phase 3 (tracked in C-2292) layers on the remaining rules — excessive scans, geographic impossibility, post-sale re-entry, serialization errors, mass-scan — each with its own `type` value.

## Status values

| Value                   | Meaning                                        | Default `recommendation` |
| ----------------------- | ---------------------------------------------- | ------------------------ |
| `authentic`             | No negative signals; treat as genuine          | `proceed`                |
| `suspect`               | At least one anomaly rule has fired            | `flag_for_review`        |
| `counterfeit_suspected` | Operator-confirmed counterfeit (Phase 3+)      | `flag_for_review`        |
| `serialization_error`   | Brand serialization scheme violated (Phase 3+) | `flag_for_review`        |

The `unverified` internal state is **never returned to consumers** — it collapses to `authentic` for the public contract until Phase 3 ships positive-authentication rules.

## Errors

| Status | Body                                           | When                                       |
| ------ | ---------------------------------------------- | ------------------------------------------ |
| 200    | Verification payload                           | Always for valid path parameters           |
| 422    | `{"error": "invalid_gtin", "detail": "..."}`   | GTIN is not 8–14 digits                    |
| 422    | `{"error": "invalid_serial", "detail": "..."}` | Serial not 1–20 printable ASCII characters |
| 429    | Rate-limit envelope, `Retry-After` header      | Anonymous IP exceeded 60/hour              |

## Stability guarantees

This linkType is **experimental v1**. Until at least one external scanner / 2D-cert tool pins to the contract, the following may change with notice in the changelog:

* Field names (`verificationStatus`, `recommendation`, etc.)
* The `verificationStatus` enum set
* The auth-tier split (which fields are unauth vs auth)
* The `recommendation` mapping table
* The shape of `anomalies` entries

Stable in all v1 revisions:

* The path layout (`/resolver/api/v1/verify/01/<gtin>/21/<serial>`)
* The `X-API-Key` auth scheme
* The 60/hour anonymous rate limit
* The enumeration-protection guarantee (unknown serial = known-authentic shape)

When the contract freezes, Closient will announce v1.0 in the API changelog.

## Related linkTypes

* [GS1 Digital Link Resolution](/guides/digital-link-resolution) — generic resolver behaviour
* [EPCIS Events](/guides/epcis-events) — the long-form event log this verification view summarizes
