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

# Onboard Retailer

> Walk a retailer through connecting their POS, syncing initial inventory, and publishing their first physical store Location on Closient.

Closient's local-first search depends on accurate, fresh in-store
inventory. This skill takes a retailer Organization from "signed up" to
"discoverable in local search" — POS connected, first batch of
`InStoreOffer` records synced, and at least one `Location` published
with PostGIS coordinates so distance-ranked search can find them.

## When to use

* A retailer just signed up and needs to be onboarded.
* The agent is helping a retailer add their second / third store
  location after the first connect.
* A retailer's existing POS integration broke and the connection needs
  re-establishing.

## Auth

OAuth token (or API key) with `integrations:write`, `offers:write`, and
`locations:write` scopes. The caller must hold `OWNER` or `MANAGER`
role on the retailer `Organization`.

## Flow

### Step 1 — Pick a POS integration

Closient supports Shopify today, with Square, Clover, and Lightspeed
on the roadmap (see **Known gaps**). The agent should ask the
retailer which POS they use; if it's not Shopify, route them to the
support flow rather than mid-onboarding.

### Step 2 — Connect via OAuth

Shopify connect is a **non-API** browser flow today (the OAuth
redirect cannot complete inside an agent-only context):

```
GET /integrations/shopify/install?shop=<retailer-shop>.myshopify.com
```

This redirects to Shopify's OAuth consent screen. After the user
approves, Shopify calls back to:

```
GET /integrations/shopify/callback?code=...&shop=...&state=...
```

Closient persists the access token and provisions an
`InventorySource` row (in the `retailers` app, not `integrations` —
see `apps/integrations/models.py` for the rationale). The agent's
job here is to:

1. Generate the install URL with the user's shop domain.
2. Hand the URL to the user (or surface it as a clickable link).
3. Wait for the user to confirm completion, then verify via Step 3.

### Step 3 — Verify the connection

```
GET /integrations/api/v1/webhooks/endpoints/
```

The provisioned webhook endpoints for the new Shopify connection
should appear in the list. If `count == 0` after the user completed
the OAuth handshake, the connect didn't land — re-issue Step 2 with
a fresh `state` token.

### Step 4 — Sync first batch of inventory

Shopify inventory sync is automatic on connection (background task
in `apps/integrations/shopify/`). The agent should poll for results:

```
GET /retailers/api/v1/organizations/{organization_id}/in-store-offers
```

The first sync typically takes 30-120 seconds for catalogs under 1k
SKUs. The agent should report progress to the user with the count
returned. Manual offer creation (for retailers who want to bootstrap
without a POS) is also supported:

```
POST /retailers/api/v1/organizations/{organization_id}/in-store-offers
Content-Type: application/json

{
  "product_id": "<short_id>",
  "physical_store_id": "<location short_id>",
  "sku": "...",
  "price_amount": 1099,
  "price_currency": "USD",
  "quantity_on_hand": 12,
  "status": "in_stock"
}
```

The `(product_id, physical_store_id, sku)` triple is unique — duplicates
return `422`.

### Step 5 — Publish the first Location

Local search ranks by great-circle distance using PostGIS, so a
Location with accurate `lat`/`lon` is required for the retailer to
appear in nearby-store queries. Today this is done via the dashboard
UI (`/organizations/create` and the org-scoped location form) — there
is no `POST /locations/api/v1/locations` endpoint yet (see **Known
gaps**). The agent should:

1. Direct the user to the dashboard location form.
2. Once published, verify via the public endpoint:

```
GET /locations/api/v1/locations?lat=<store_lat>&lon=<store_lon>&radius=1
```

The retailer's location should appear with `distance_km` near zero.

### Step 6 — Confirm discoverability

```
POST /search/api/v1/search/session
Content-Type: application/json

{
  "query": "<one product the retailer carries>",
  "latitude": <store_lat>,
  "longitude": <store_lon>,
  "radius_km": 5
}
```

The retailer's store should appear in results within a few minutes of
the inventory sync completing. If it doesn't, the most common cause is
zero published `Location` rows for the org — re-verify Step 5.

## Required inputs

* `organization_id` — the retailer's organization.
* `shop_domain` — `<retailer>.myshopify.com` (Shopify-only today).
* One physical store address (city, state/region, postal code) for
  Step 5.

## Optional inputs

* `latitude` / `longitude` — if the retailer already knows them; else
  the dashboard form geocodes from the address.
* Store hours — populated via the dashboard location form.
* Webhook endpoints — provisioned automatically; can be listed via
  `GET /integrations/api/v1/webhooks/endpoints/`.

## Output

* `integration_status` — `connected` once Step 3 verifies; `pending`
  while waiting for OAuth completion.
* `synced_offer_count` — from the count returned by the
  in-store-offers list endpoint in Step 4.
* `first_location_url` — the public location URL once Step 5
  completes (form path: `/marketplace/<org_short_id>/locations/<location_short_id>`).

## Guidance for agents

* **Surface the dashboard handoff explicitly.** Steps 2 (OAuth) and 5
  (Location create) are not fully API-driven today — the user has to
  click a link / fill a form. Be honest about this rather than
  pretending the agent can complete them headlessly.
* **Don't poll faster than 5 seconds** on the in-store-offers list —
  Shopify's catalog sync is rate-limited and we throttle the public
  API at 300 req/min/key. A patient backoff is more user-friendly than
  flooding the API.
* **One Location at minimum, but encourage all of them.** Local search
  ranks by proximity per-location, so a multi-store retailer that only
  publishes one location is invisible to the other neighborhoods.
* **Inventory freshness matters more than completeness.** Surface the
  `last_inventory_sync` field on offers so the user knows their data is
  current — stale inventory ranks lower in search.
* **Webhook secret.** The OAuth flow provisions endpoints with a
  rotating secret. If the retailer needs to verify webhook deliveries
  on their side, the secret is available via `GET
  /integrations/api/v1/webhooks/endpoints/{endpoint_id}` and can be
  rotated via `POST /webhooks/endpoints/{endpoint_id}/rotate-secret/`.

## Known gaps (planned)

* **Shopify-only today.** The original ticket scope mentions Square,
  Clover, and Lightspeed. Models reference these as future
  `InventorySource` types but no OAuth flow exists. Planned per app
  in `apps/integrations/` — track via the integrations roadmap.
* **No headless POS-connect endpoint.** Step 2 requires a browser
  redirect today. A device-code OAuth flow (so agents can complete
  the connect on behalf of a user with a one-time code) is planned.
* **No `POST /locations/api/v1/locations`.** Step 5 is currently a
  dashboard form. A first-class Location create endpoint (with
  PostGIS coordinates, hours, contact info) is planned so this skill
  can be fully API-driven end-to-end.
* **No "is my store discoverable?" health endpoint.** Step 6 uses the
  search endpoint as an indirect probe. A dedicated readiness check
  (POS connected + ≥1 location published + ≥1 in-stock offer)
  would be cleaner.
* **No sync-job status endpoint.** The agent has to poll the offer
  list to infer sync progress. A `GET /integrations/api/v1/jobs/{job_id}`
  is planned.

## Related skills

* `claim-brand` — the parallel flow for brand-owner Organizations.
* `local-product-search` — the surface this onboarding enables —
  what consumers see once the retailer is live.
* `check-product-availability` — how agents probe a specific GTIN
  against the retailer's now-synced offers.
* `brand-retail-footprint` — once a retailer is live, this skill
  (called from a brand owner's side) shows their products being
  carried.
