> ## 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.

# Brand and QR resolver

> Create a brand and a product, read the Digital Link its QR code encodes, and control where a scan goes.

This guide takes a brand from nothing to a scannable product: create the brand
and its product, read the GS1 Digital Link its QR code encodes, attach a typed
link, and see where a phone and a machine reader each end up when they scan it.

Every sample uses the [Python SDK](/sdk/python) with a client built once,
from an API key issued to your organization. The scans in steps 7 and 8 use a
plain HTTP client standing in for a phone:

```python theme={null}
closient = ClosientClient(api_key="csb_...")
phone = httpx.Client()
```

The samples import:

```python theme={null}
import httpx
from closient import ClosientClient, ClosientValidationError
from closient.api.brands import create_brand
from closient.api.product_links import create_product_link
from closient.api.products import create_product, get_claim_verification
from closient.api.resolver import create_resolution_rule
from closient.models import BrandCreateIn, DestinationTypeEnum, ProductCreatePayload, ProductLinkCreate, ProductLinkCreateLinkType, ResolutionRuleCreate, ResolverScopeTypeEnum
```

<Info>
  Every request and response on this page was recorded from a real run of this journey against the Closient API. The journey runs every night and fails if any exchange differs from what is shown here, so the examples cannot silently go stale. Source: `tools/api-conformance/api_conformance/journeys/brand_resolver.py`.
</Info>

## 1. Create the brand

A brand belongs to an organization. Your API key is scoped to one, so the
`organization_id` is the one that key was issued for.

```python theme={null}
def register_brand(closient: ClosientClient, organization_id: str) -> str:
    brand = create_brand.sync(
        client=closient.generated,
        body=BrandCreateIn(organization_id=organization_id, name="Driftwood"),
    )
    return brand.id
```

<Accordion title="POST /brands/api/v1/brands → 201">
  Request headers:

  ```http theme={null}
  accept: */*
  content-type: application/json
  ```

  Request body:

  ```json theme={null}
  {
    "description": "",
    "name": "Driftwood",
    "organization_id": "<organization_id>",
    "website": ""
  }
  ```

  Response body:

  ```json theme={null}
  {
    "created": "2026-01-01T00:00:00Z",
    "description": "",
    "facebook": "",
    "has_social_links": false,
    "id": "<brand_id>",
    "instagram": "",
    "linkedin": "",
    "logo_url": null,
    "metadata": {},
    "name": "Driftwood",
    "organization_id": "<organization_id>",
    "pinterest": "",
    "threads": "",
    "tiktok": "",
    "twitter": "",
    "website": "",
    "youtube": ""
  }
  ```
</Accordion>

## 2. Create a product and read its Digital Link

Creating a product under your organization claims its GTIN for you. The
response carries `digital_link`: the exact URL a QR code for this product
encodes. Print that, or hand it to any QR generator. If your organization
has a verified custom resolver hostname with an active certificate, the
link uses it. `POST /qr/` builds a QR image for you, but it always encodes
the platform resolver domain, so with a custom hostname print
`digital_link` instead.

```python theme={null}
def add_product(closient: ClosientClient, organization_id: str, gtin: str) -> tuple[str, str]:
    product = create_product.sync(
        client=closient.generated,
        body=ProductCreatePayload(gtin=gtin, organization_id=organization_id, product_name="Driftwood Smoked Sea Salt"),
    )
    assert product.is_claimed
    return product.id, product.digital_link
```

<Accordion title="POST /products/api/v1/products → 201">
  Request headers:

  ```http theme={null}
  accept: */*
  content-type: application/json
  ```

  Request body:

  ```json theme={null}
  {
    "country_of_origin": "",
    "description": "",
    "gtin": "<gtin>",
    "net_content": "",
    "organization_id": "<organization_id>",
    "product_name": "Driftwood Smoked Sea Salt"
  }
  ```

  Response body:

  ```json theme={null}
  {
    "brand": "Driftwood",
    "claim_verification_status": "unverified",
    "confidence_score": 0.0,
    "country_of_origin": "",
    "data_source": "claimed",
    "date_label_compliance": {
      "ca_ab660": "compliant",
      "federal": "unknown",
      "notes": []
    },
    "depth": null,
    "depth_uom": "",
    "description": "",
    "digital_link": "HTTPS://GTIN.ONE/01/<gtin14>",
    "field_confidence": {},
    "gpc_brick_code": "",
    "gpc_class_code": "",
    "gpc_family_code": "",
    "gpc_segment_code": "",
    "gross_weight": null,
    "gross_weight_uom": "",
    "gtin": "<gtin14>",
    "height": null,
    "height_uom": "",
    "id": "<product_id>",
    "image_url": "",
    "is_claimed": true,
    "length": null,
    "length_uom": "",
    "manufacturer": "",
    "metadata": {},
    "net_content": "",
    "net_content_uom": null,
    "net_content_value": null,
    "net_weight": null,
    "net_weight_uom": "",
    "owner_id": "<organization_id>",
    "product_name": "Driftwood Smoked Sea Salt",
    "sensitivities": [],
    "width": null,
    "width_uom": ""
  }
  ```
</Accordion>

## 3. See what your claim still needs before the resolver will serve your URLs

A new claim starts unverified. While `blocks_resolver_serving` is true, a
scan lands on the product's Closient-hosted page whatever rules you set:
that is what stops someone who claims a GTIN they do not own from
redirecting its shoppers. The `checklist` lists what promotes the claim.

```python theme={null}
def check_claim_verification(closient: ClosientClient, gtin: str) -> None:
    verification = get_claim_verification.sync(client=closient.generated, gtin=gtin)
    assert verification.blocks_resolver_serving
    print(verification.claim_verification_status, [item.to_dict() for item in verification.checklist])
```

<Accordion title="GET /products/api/v1/products/<gtin>/verification → 200">
  Request headers:

  ```http theme={null}
  accept: */*
  ```

  Response body:

  ```json theme={null}
  {
    "blocks_resolver_serving": true,
    "checklist": [
      {
        "action": "Upload GS1 certificate",
        "evidence_type": "gs1_certificate",
        "is_done": false
      },
      {
        "action": "Provide your Verified by GS1 registry record",
        "evidence_type": "gepir_lookup",
        "is_done": false
      }
    ],
    "claim_verification_status": "unverified",
    "domain_verification": {
      "dns_record_name": "",
      "domain": null,
      "http_enabled": false,
      "http_file_url": "",
      "is_available": false,
      "token": "<redacted>"
    },
    "evidence": [],
    "gtin": "<gtin14>",
    "next_status": "verified",
    "promotion_rules": [
      {
        "is_met": false,
        "requirements": [
          {
            "have": 0,
            "missing": 1,
            "required": 1,
            "strength": "strong"
          }
        ]
      },
      {
        "is_met": false,
        "requirements": [
          {
            "have": 0,
            "missing": 2,
            "required": 2,
            "strength": "medium"
          }
        ]
      },
      {
        "is_met": false,
        "requirements": [
          {
            "have": 0,
            "missing": 1,
            "required": 1,
            "strength": "medium"
          },
          {
            "have": 0,
            "missing": 2,
            "required": 2,
            "strength": "weak"
          }
        ]
      }
    ]
  }
  ```
</Accordion>

## 4. Attach a typed link

Links are typed by GS1 link relation. The product's hosted page shows
each type in its own section, with your URL as that section's call to
action, and the resolver advertises the type to machine readers (step 8).

```python theme={null}
def add_recipe_link(closient: ClosientClient, product_id: str) -> str:
    link = create_product_link.sync(
        client=closient.generated,
        body=ProductLinkCreate(
            product_id=product_id,
            link_type=ProductLinkCreateLinkType.GS1RECIPEINFO,
            url="https://driftwood.example/recipes/smoked-sea-salt",
            label="Recipes",
        ),
    )
    return link.id
```

<Accordion title="POST /products/api/v1/product-links → 201">
  Request headers:

  ```http theme={null}
  accept: */*
  content-type: application/json
  ```

  Request body:

  ```json theme={null}
  {
    "description": "",
    "label": "Recipes",
    "link_type": "gs1:recipeInfo",
    "order": 0,
    "product_id": "<product_id>",
    "url": "https://driftwood.example/recipes/smoked-sea-salt",
    "use_ai_substitution": false
  }
  ```

  Response body:

  ```json theme={null}
  {
    "anchor": "recipeInfo",
    "brand_id": null,
    "created": "2026-01-01T00:00:00Z",
    "description": "",
    "display_label": "Recipes",
    "id": "<link_id>",
    "label": "Recipes",
    "link_type": "gs1:recipeInfo",
    "modified": "2026-01-01T00:00:00Z",
    "order": 0,
    "product_id": "<product_id>",
    "url": "https://driftwood.example/recipes/smoked-sea-salt",
    "use_ai_substitution": false
  }
  ```
</Accordion>

## 5. Try to send scans to your own site

A rule decides where a scan goes. Sending it to a URL of your own needs a
verified claim, so an unverified one is refused with a 422 that says why.

```python theme={null}
def custom_url_rule_is_refused(closient: ClosientClient, organization_id: str, product_id: str) -> None:
    try:
        create_resolution_rule.sync(
            client=closient.generated,
            organization_id=organization_id,
            body=ResolutionRuleCreate(
                scope_type=ResolverScopeTypeEnum.PRODUCT,
                scope_id=product_id,
                order_index=0,
                destination_type=DestinationTypeEnum.CUSTOM_URL,
                custom_url="https://driftwood.example/sea-salt",
            ),
        )
    except ClosientValidationError as error:
        print(error.status_code, error.detail)
    else:
        raise AssertionError("a CUSTOM_URL rule was accepted for an unverified claim")
```

<Accordion title="POST /resolver/api/v1/organizations/<organization_id>/resolution-rules → 422">
  Request headers:

  ```http theme={null}
  accept: */*
  content-type: application/json
  ```

  Request body:

  ```json theme={null}
  {
    "custom_url": "https://driftwood.example/sea-salt",
    "destination_type": "CUSTOM_URL",
    "enabled": true,
    "on_custom_url_dead": "SKIP_RULE",
    "order_index": 0,
    "scope_id": "<product_id>",
    "scope_type": "PRODUCT"
  }
  ```

  Response body:

  ```json theme={null}
  {
    "detail": "GTIN <gtin14> has not been verified, so it cannot be given a redirect destination. Verify ownership of the GS1 prefix to activate custom destinations; until then this product resolves to its Closient-hosted page.",
    "error_code": "unprocessable_entity",
    "retryable": false,
    "status": 422,
    "timestamp": "2026-01-01T00:00:00Z",
    "title": "Unprocessable Entity",
    "type": "https://closient.com/docs/errors/unprocessable_entity"
  }
  ```
</Accordion>

## 6. Send scans to the hosted product page

A `HOSTED_PAGE` rule is always allowed: it sends the scan to the page
Closient hosts for the product, which shows your product data and links.

```python theme={null}
def add_hosted_page_rule(closient: ClosientClient, organization_id: str, product_id: str) -> str:
    rule = create_resolution_rule.sync(
        client=closient.generated,
        organization_id=organization_id,
        body=ResolutionRuleCreate(
            scope_type=ResolverScopeTypeEnum.PRODUCT,
            scope_id=product_id,
            order_index=0,
            destination_type=DestinationTypeEnum.HOSTED_PAGE,
        ),
    )
    return rule.id
```

<Accordion title="POST /resolver/api/v1/organizations/<organization_id>/resolution-rules → 201">
  Request headers:

  ```http theme={null}
  accept: */*
  content-type: application/json
  ```

  Request body:

  ```json theme={null}
  {
    "destination_type": "HOSTED_PAGE",
    "enabled": true,
    "on_custom_url_dead": "SKIP_RULE",
    "order_index": 0,
    "scope_id": "<product_id>",
    "scope_type": "PRODUCT"
  }
  ```

  Response body:

  ```json theme={null}
  {
    "absolute_end_at": null,
    "absolute_start_at": null,
    "annual_end_mmdd": null,
    "annual_start_mmdd": null,
    "created": "2026-01-01T00:00:00Z",
    "custom_url": null,
    "days_of_week": null,
    "destination_type": "HOSTED_PAGE",
    "enabled": true,
    "id": "<rule_id>",
    "link_type": null,
    "location_cities": null,
    "location_countries": null,
    "location_regions": null,
    "metadata": {},
    "modified": "2026-01-01T00:00:00Z",
    "on_custom_url_dead": "SKIP_RULE",
    "order_index": 0,
    "organization_id": "<organization_id>",
    "product_id": "<product_id>",
    "scope_id": "<product_id>",
    "scope_type": "PRODUCT",
    "section_anchor": null,
    "system": false,
    "time_of_day_end": null,
    "time_of_day_start": null,
    "timezone": null
  }
  ```
</Accordion>

## 7. Scan the code like a phone

A phone camera opens the Digital Link in a browser, which asks for HTML.
The QR domain only redirects: one hop hands the scan to the resolver,
which redirects it on to the product's hosted page.

```python theme={null}
def scan_like_a_phone(phone: httpx.Client, digital_link: str) -> None:
    page = phone.get(digital_link, headers={"Accept": "text/html"}, follow_redirects=True)
    assert page.status_code == 200
    assert page.url.path.startswith("/pip/")
```

<Accordion title="GET https://gtin.one/01/<gtin14> → 301">
  Request headers:

  ```http theme={null}
  accept: text/html
  ```

  Location: `https://www.closient.com/01/<gtin14>?via=gtin.one`

  Response body:

  ```text theme={null}
  <HTML page>
  ```
</Accordion>

<Accordion title="GET /01/<gtin14>?via=gtin.one → 307">
  Request headers:

  ```http theme={null}
  accept: text/html
  ```

  Location: `https://www.closient.com/pip/<gtin14>/?via=gtin.one`
</Accordion>

<Accordion title="GET /pip/<gtin14>/?via=gtin.one → 200">
  Request headers:

  ```http theme={null}
  accept: text/html
  ```

  Response body:

  ```text theme={null}
  <HTML page>
  ```
</Accordion>

## 8. Ask the same URL for its links

A machine reader asks for `application/linkset+json` instead. The QR
domain sends it to the resolver with the same one hop, and the resolver
answers there, with no further redirect: the product's links keyed by link
relation. Each typed link points at its section of the hosted page
(`#recipeInfo` here), where your URL is the call to action.

```python theme={null}
def scan_like_a_machine(phone: httpx.Client, digital_link: str) -> None:
    response = phone.get(digital_link, headers={"Accept": "application/linkset+json"}, follow_redirects=True)
    assert response.status_code == 200
    (linkset,) = response.json()["linkset"]
    (recipes,) = linkset["https://gs1.org/voc/recipeInfo"]
    assert recipes["href"].endswith("#recipeInfo")
```

<Accordion title="GET https://gtin.one/01/<gtin14> → 301">
  Request headers:

  ```http theme={null}
  accept: application/linkset+json
  ```

  Location: `https://www.closient.com/01/<gtin14>?via=gtin.one`

  Response body:

  ```text theme={null}
  <HTML page>
  ```
</Accordion>

<Accordion title="GET /01/<gtin14>?via=gtin.one → 200">
  Request headers:

  ```http theme={null}
  accept: application/linkset+json
  ```

  Response body:

  ```json theme={null}
  {
    "linkset": [
      {
        "anchor": "https://www.closient.com/01/<gtin14>",
        "https://gs1.org/voc/defaultLink": [
          {
            "href": "https://www.closient.com/pip/<gtin14>/",
            "title": "Driftwood Smoked Sea Salt"
          }
        ],
        "https://gs1.org/voc/masterData": [
          {
            "href": "https://www.closient.com/01/<gtin14>",
            "title": "Master data (JSON)",
            "type": "application/json"
          }
        ],
        "https://gs1.org/voc/pip": [
          {
            "href": "https://www.closient.com/pip/<gtin14>/",
            "title": "Driftwood Smoked Sea Salt"
          }
        ],
        "https://gs1.org/voc/productInfo": [
          {
            "href": "https://www.closient.com/pip/<gtin14>/",
            "title": "Driftwood Smoked Sea Salt"
          }
        ],
        "https://gs1.org/voc/recipeInfo": [
          {
            "href": "https://www.closient.com/pip/<gtin14>/#recipeInfo",
            "title": "Recipes"
          }
        ],
        "itemDescription": "Driftwood Smoked Sea Salt"
      }
    ]
  }
  ```
</Accordion>

## Next

* Verify the claim to unlock `CUSTOM_URL` rules: the `checklist` from step 3
  says what is missing.
* Add more typed links; the linkset grows with them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.