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

# V1 → V2 migration: agent runbook

> Workflow for Claude Code (or any coding agent) to execute the v1 → v2 port directly against your integration repository.

<Info>
  This page is the **agent prompt** that complements the [V1 → V2 migration reference](/docs/v2/migration-reference). Drop a coding agent into your integration repo, point it at this URL, and the agent ports your code phase-by-phase with human checkpoints. Want the human-readable port reference instead? [Start here](/docs/v2/migration-reference).
</Info>

## Invoking the agent

[Claude Code](https://claude.ai/code) is the reference implementation. From your integration repo:

```bash theme={null}
cd your-zinc-integration-repo
claude

# Then in the Claude prompt:
> Read https://docs.zinc.com/v2/migration-agent-runbook and execute the v1 → v2 migration on this repository.
```

Other agents work too — the runbook is plain text and language-agnostic. Just paste the full content of this page as the prompt.

***

## Prime directives

1. **Never silently drop functionality.** Anything without a 1-to-1 v2 mapping gets a flag in `MIGRATION_GAPS.md` and a `TODO(zinc-migration)` comment at the call site.
2. **Commit per phase.** Each numbered phase below is a separate commit, so the human can review small diffs.
3. **Surface uncertainty.** If you can't tell whether a string is a product ID or a URL, whether `max_price` is in cents or dollars, whether a status branch should map to `order_placed` or `in_progress` — flag it instead of guessing.
4. **Do not invent credentials or secrets.** API keys, webhook secrets, retailer credentials, and the v2 base URL come from the human via env vars or the dashboard.
5. **The live docs are the source of truth.** Where this runbook and the live docs disagree, the live docs win — v2 evolves and your training data may be stale.

***

## Webhook Wire Reference

v2 delivers all events to a **single webhook URL** configured per account in the
dashboard. The agent must not invent this URL, the secret, or the header names —
they are fixed by the API and reproduced here so Phase 6 does not depend on
fetching live docs.

### Delivery

* Transport: HTTP `POST` to the account's configured `webhook_url`.
* `Content-Type: application/json`
* Headers on every delivery:
  * `X-Webhook-Event` — the event name (e.g. `order.failed`)
  * `X-Webhook-Signature` — hex-encoded HMAC-SHA256 of the **raw request body**

### Signature verification

The signature is `HMAC-SHA256(secret, raw_body)`, hex-encoded. The secret is the
account's **webhook signing secret** (prefix `zn_whsec_`), obtained from the
dashboard — never generated by the agent. Compute the HMAC over the exact bytes
received, before any JSON re-serialization, and compare timing-safely.

```python theme={null}
import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
```

### Payload envelope

```json theme={null}
{
  "event": "order.failed",
  "order_id": "<uuid>",
  "return_id": null,
  "status": "<current status>",
  "timestamp": "<ISO 8601>",
  "data": {}
}
```

* `return_id` is populated **only** on `return.*` events; it is `null` for order events.
* `status` is the order status for `order.*` events and the return status for `return.*` events.
* `data` carries event-specific fields. For `order.failed` it includes `error_type` and `error`.

### Complete event list

Order lifecycle:
`order.started`, `order.placed`, `order.failed`, `order.tracking_received`,
`order.delivered`, `order.cancelled`

Return lifecycle:
`return.created`, `return.approved`, `return.denied`, `return.credited`

When mapping v1 event names in Phase 6, map only to names in this list. Any v1
event with no equivalent here goes to `MIGRATION_GAPS.md` rather than a guessed name.

***

## Phase 0 — Ground in the current docs

Before writing any code, fetch the live v2 documentation. Do not rely on what you remember about the Zinc API; v2 ships changes frequently.

Fetch at minimum:

* `https://www.zinc.com/docs/llms.txt` — the full documentation index
* `https://www.zinc.com/docs/versions/latest.json` — the OpenAPI spec, **the source of truth for field names and types**
* This page and the [migration reference](/docs/v2/migration-reference)
* Endpoint pages for any endpoints this codebase uses. At minimum:
  * `https://www.zinc.com/docs/v2/api-reference/introduction/authentication.md`
  * `https://www.zinc.com/docs/v2/api-reference/orders/create-order.md`
  * `https://www.zinc.com/docs/v2/api-reference/orders/get-order.md`
  * `https://www.zinc.com/docs/v2/api-reference/orders/cancel-order.md`
  * `https://www.zinc.com/docs/v2/api-reference/introduction/webhooks.md`
  * `https://www.zinc.com/docs/v2/api-reference/introduction/error-handling.md`
  * `https://www.zinc.com/docs/v2/api-reference/introduction/idempotency.md`
  * `https://www.zinc.com/docs/v2/api-reference/introduction/sandbox.md`
  * `https://www.zinc.com/docs/v2/api-reference/returns/create-return.md` (plus get-return, list-returns)
  * `https://www.zinc.com/docs/v2/api-reference/products/get-product.md`, `get-product-offers.md`, `search.md`

If the codebase uses managed accounts, MPP, or cross-retailer search, read those pages too — they're linked from the index.

***

## Phase 1 — Repo setup

Read-only confirmation. Stop and ask the human if any of these are unclear:

* Identify the repo's language(s) (Python / Node / Ruby / Go / PHP / etc.) and HTTP client patterns (`requests`, `axios`, `fetch`, raw `curl`, an SDK).
* Identify the existing test harness — you'll run it as validation in Phase 8.
* Create a new branch `zinc-v2-migration`.

***

## Phase 2 — Discovery → `MIGRATION_INVENTORY.md`

Search the repo for every v1 touchpoint. Inventory includes code paths, config, **and persistence layer** — not just HTTP call sites.

**Code patterns:**

* URL strings containing `api.zinc.io` (any path)
* URL strings containing `/v0/`, `/v1/`, or `/v2/` near an HTTP call
* Headers / variables named `client_token`, `ZINC_CLIENT_TOKEN`, `zinc_token`, `Authorization: Basic …`
* Imports of any v1 SDK: `zinc`, `@zinc/api`, `zinc-api`, `zincapi`, etc.
* `request_id` polling loops — v1 returned `{"request_id": "..."}` from order creation and required polling `GET /v1/orders/{request_id}`; while in flight, that endpoint returned the `request_processing` error code rather than a real status
* Webhook handler routes whose payloads or paths reference v1 event names: `order_placed`, `order_failed`, `tracking_obtained`, `tracking_updated`, `status_updated`, `case_updated`, `return_placed`, `return_failed`, `request_succeeded`, `request_failed`

**v1 request fields to grep for** (many have no v2 equivalent and need flagging):

`retailer`, `product_id`, `seller_selection_criteria`, `shipping_method`, `retailer_credentials`, `payment_method`, `billing_address`, `is_gift`, `gift_message`, `client_notes`, `addax`, `webhooks` (v1 accepted per-request webhook URLs in the order body), `max_price`

**v1 error / status codes to grep for:**

`_type: "error"`, `request_processing`, `aborted_request`, `invalid_login`, `max_price_exceeded`

**Persistence:**

* Database columns or models storing `request_id`, v1 status strings (e.g. `pending`, `submitted`, `placed`), or v1 error codes — these may need a schema migration to hold v2's UUID order IDs and the new status enum.
* Migration shape: keep v1 columns readable during the transition window (existing in-flight v1 orders shouldn't break); add v2 columns alongside.

**Config / secrets:**

* Environment variables, `.env*` files, secret-manager templates, CI configs holding Zinc client tokens.

**Configuration:**

* Any per-environment config (Stripe-style keys, Slack channels, etc.) that points to Zinc — note which environments exist so the agent doesn't accidentally mix test and prod keys later.

Write `MIGRATION_INVENTORY.md` at the repo root:

```markdown theme={null}
# Zinc v1 → v2 Migration Inventory

Generated by Claude Code on <ISO date>. Do not edit by hand — re-run
discovery to refresh.

## API call sites

| File | Line | v1 endpoint | Phase | Status |
|------|------|-------------|-------|--------|
| src/orders.py | 42 | POST /v1/orders | 4 | pending |
| src/returns.py | 17 | POST /v1/returns | 4 | pending |

## Webhook handlers

| File | Line | v1 event(s) handled | Status |
|------|------|---------------------|--------|
| src/webhooks/order_placed.py | 5 | order_placed | pending |

## v1 credentials in code (potential leak — review)

| File | Line | Pattern matched |
|------|------|-----------------|
| config/.env.example | 3 | ZINC_CLIENT_TOKEN=… |
```

**Commit:** `chore(zinc): inventory v1 call sites for v2 migration`.

If the inventory contains zero rows, **stop** — there are no v1 calls to migrate; surface that to the human.

***

## Phase 3 — Auth and base URL

One commit, repo-wide:

<Steps>
  <Step title="Add or rename env vars">
    Store the key in the customer's environment under a local name such as `ZINC_API_KEY` (the `zn_live_*` / `zn_test_*` bearer token) and add `ZINC_BASE_URL` (the v2 production host; default to a placeholder and flag for human confirmation in `MIGRATION_PREREQS.md`). These env var names are a convention for the customer's repo, not values the API defines. The key itself is **minted in the dashboard**, never invented by the agent.
  </Step>

  <Step title="Remove the old auth config">
    Strip `ZINC_CLIENT_TOKEN` and any basic-auth password vars from `.env.example`, `config.{py,js,...}`, CI configs, and secret-manager templates. Add a deprecation comment.
  </Step>

  <Step title="Rewrite every HTTP-client construction site">
    `Authorization: Bearer ${ZINC_API_KEY}` instead of `Authorization: Basic <base64(token)>`.
  </Step>

  <Step title="Strip URL prefixes">
    Drop `/v0`, `/v1`, `/v2` — v2 has no version prefix.
  </Step>
</Steps>

<Warning>
  **Do not** rewrite request bodies in this phase. Endpoint paths beyond the prefix change still match in URL structure for most resources; bodies move in Phase 5.
</Warning>

<Warning>
  **Two keys, never mixed.** Sandbox and production share the **same base URL** — there is no separate sandbox host. Provision a `zn_test_*` key for dev / staging / CI and a `zn_live_*` key for production only; the key prefix alone selects the environment (`zn_test_*` routes to the sandbox, `zn_live_*` to production). Add a config-time assertion that prevents `zn_live_*` from ever loading in non-prod environments, and vice-versa — and use timing-safe comparison for webhook signatures.
</Warning>

**Commit:** `feat(zinc): switch to v2 bearer auth and base URL`.

***

## Phase 4 — Pre-flight checklist → `MIGRATION_PREREQS.md`

Write `MIGRATION_PREREQS.md` with the human-required steps from the [pre-flight checklist](/docs/v2/migration-reference#pre-flight-checklist) in the reference. Tailor it to what discovery found.

```markdown theme={null}
# Zinc v2 Migration — Manual Pre-flight

The agent has scaffolded the code migration. Before any v2 calls will
succeed, you (a human) need to:

- [ ] Sign in to the v2 dashboard at https://app.zinc.com and create
      an account via Stytch magic-link.
- [ ] Confirm the v2 production base URL and set ZINC_BASE_URL.
- [ ] Mint a `zn_live_*` API key and set ZINC_API_KEY.
- [ ] Mint a `zn_test_*` API key for CI and set ZINC_TEST_API_KEY.
- [ ] Top up your v2 wallet. The agent counted N order placements in
      your repo; estimate budget accordingly.
- [ ] Register your webhook URL via POST /users. The agent identified
      these existing v1 webhook URLs — pick one or consolidate:
        - https://yours.example.com/wh/placed
        - https://yours.example.com/wh/tracking
- [ ] Copy the account's webhook signing secret (prefix `zn_whsec_`)
      from the dashboard into the customer's environment (e.g.
      ZINC_WEBHOOK_SECRET). This is the HMAC key for verifying
      X-Webhook-Signature. It is account-scoped and dashboard-issued —
      the agent must not generate it.
- [ ] Create managed accounts for these (retailer, email) pairs the
      agent found in your code:
        - amazon / buyer1@example.com
        - amazon / buyer2@example.com
        - walmart / buyer1@example.com
- [ ] Record the returned `short_id` for each; set them in config
      (the agent left placeholders: ZINC_MANAGED_ACCOUNT_AMAZON_BUYER1, …).
```

**Commit:** `docs(zinc): manual pre-flight checklist for v2 migration`.

***

## Phase 5 — Endpoint-by-endpoint port

For each row in `MIGRATION_INVENTORY.md`, apply the transformation from the [endpoint mapping](/docs/v2/migration-reference#endpoint-mapping) and [request shape diffs](/docs/v2/migration-reference#request-shape-diffs) in the reference. Group by resource and commit per resource:

* `feat(zinc): port order placement to v2 /orders`
* `feat(zinc): port returns to v2 /returns`
* `feat(zinc): port retailer credentials to managed-accounts model`
* `feat(zinc): port tracking reads to embedded order tracking`

### Per-call-site checklist

For each call site:

<Steps>
  <Step title="URL change">
    Apply the mapping from the [reference's endpoint mapping](/docs/v2/migration-reference#endpoint-mapping). If the mapping is *no equivalent*:

    * Leave the calling code intact.
    * Add `# TODO(zinc-migration): no v2 equivalent for {endpoint} — see MIGRATION_GAPS.md`.
    * Append a row to `MIGRATION_GAPS.md`. Continue.
  </Step>

  <Step title="Request body transformation">
    Apply the field-by-field rules from [Request shape diffs](/docs/v2/migration-reference#request-shape-diffs).
  </Step>

  <Step title="Response body transformation">
    Apply [Response shape diffs](/docs/v2/migration-reference#response-shape-diffs) everywhere downstream code reads the response. Search the codebase for accesses to `request_id`, `merchant_order_ids`, `_status`, `price_components`, etc., and rewrite.
  </Step>

  <Step title="Status string branches">
    If you find `if status == "placed"` or similar, rewrite to the v2 enum value (`"order_placed"`). Centralise into a helper if there are >3 sites.
  </Step>

  <Step title="Update the inventory row">
    Set status to `done` and link the commit SHA.
  </Step>
</Steps>

### Hard rules

<AccordionGroup>
  <Accordion title="Never inline credentials">
    If you find a literal email/password/TOTP in the codebase that was previously passed to v1's `retailer_credentials`, replace it with a config reference (e.g. `config.ZINC_MANAGED_ACCOUNT_<NAME>`) and add a row to `MIGRATION_PREREQS.md`. Do not move secrets between files.
  </Accordion>

  <Accordion title="Confirm max_price units">
    v2 requires integer cents. If the customer's code multiplies or divides by 100 before sending to v1, that math probably stays. If it doesn't, flag the call site for the human to confirm.
  </Accordion>

  <Accordion title="Don't auto-construct product URLs you're unsure about">
    If a `product_id` is being used and you can't confidently identify the retailer (e.g. from a sibling `retailer: "amazon"` argument), flag it.
  </Accordion>

  <Accordion title="Idempotency keys carry over">
    `idempotency_key` is still supported on v2 `/orders`; pass it through. Drop it for endpoints where v1 had it and v2 doesn't (returns).
  </Accordion>
</AccordionGroup>

***

## Phase 6 — Webhook handlers

<Steps>
  <Step title="Find every v1 webhook handler route">
    Typically one route per event type.
  </Step>

  <Step title="Consolidate into a single handler">
    Match the existing URL the customer is most likely to register in v2 (the most common destination in the v1 `webhooks` objects).
  </Step>

  <Step title="Insert HMAC-SHA256 signature verification">
    Implement signature verification per the [Webhook Wire Reference](#webhook-wire-reference) above. Use `ZINC_WEBHOOK_SECRET` as the HMAC key, compute over the raw request body, and reject with 401 if the `X-Webhook-Signature` header doesn't match. Use `hmac.compare_digest` (or the language equivalent) to avoid timing leaks.
  </Step>

  <Step title="Map v1 event names to v2 event names">
    Use the [event name mapping](/docs/v2/migration-reference#event-name-mapping). If the customer's handler relied on `status_updated` / `case_updated` / `request_succeeded` / `request_failed` / `return_failed`, add `TODO(zinc-migration)` + a `MIGRATION_GAPS.md` row.
  </Step>

  <Step title="Delete or comment out per-event handlers">
    Now merged into the dispatcher.
  </Step>
</Steps>

**Commit:** `feat(zinc): consolidate webhook handlers for v2 single-URL model`.

***

## Phase 7 — Gaps review → `MIGRATION_GAPS.md`

By the end of Phase 6 you should have a populated `MIGRATION_GAPS.md`. Ensure each row has:

* File and line of the original v1 call
* The v1 endpoint or event involved
* A one-sentence description of what the customer was doing with it
* Your **suggested workaround** if any (e.g., "store `merchant_order_id` locally and look it up; v2 does not expose `insert_merchant_order_id`"), or `none — discuss with Zinc` if you have no good suggestion

**Commit:** `docs(zinc): document migration gaps for human review`.

<Warning>
  Stop the migration here and surface `MIGRATION_GAPS.md` to the human. Do not proceed to Phase 8 until the human has acknowledged the gaps file — otherwise validation will run against a half-migrated integration and lie about success.
</Warning>

***

## Phase 8 — Validation

Drive scenarios with the **dedicated test product URLs** rather than placing real orders. Fetch the live catalog from `GET /orders/test-products` and exercise at minimum:

| Test product                                               | What it exercises                                                                                                            |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `https://zinc.com/shop/products/test-success`              | Full happy path through `order_placed`, tracking numbers, and price components                                               |
| `https://zinc.com/shop/products/test-invalid-address`      | **Synchronous** failure — rejected at order-creation time                                                                    |
| `https://zinc.com/shop/products/test-url-unreachable`      | Synchronous failure                                                                                                          |
| `https://zinc.com/shop/products/test-insufficient-funds`   | Synchronous failure                                                                                                          |
| `https://zinc.com/shop/products/test-out-of-stock`         | **Asynchronous** failure — order creates successfully, then fails during processing and arrives as an `order.failed` webhook |
| `https://zinc.com/shop/products/test-price-exceeded`       | Asynchronous failure                                                                                                         |
| `https://zinc.com/shop/products/test-invalid-variant`      | Asynchronous failure                                                                                                         |
| `https://zinc.com/shop/products/test-shipping-unavailable` | Asynchronous failure                                                                                                         |

<Warning>
  **The sync-vs-async failure split is the most common code path you'll get wrong.** Some errors are returned by the API immediately (4xx with a code/message); others come back asynchronously as `order.failed` webhooks after the order was successfully created. The application must handle both — code that only inspects the create-order response will silently drop the async failures.
</Warning>

<Steps>
  <Step title="Confirm pre-flight is complete">
    All checkboxes in `MIGRATION_PREREQS.md` are ticked, env vars are set in `.env.test` or equivalent.
  </Step>

  <Step title="Run the customer's test suite against the v2 sandbox">
    Use `ZINC_TEST_API_KEY` and `ZINC_BASE_URL`. Sandbox is the same base URL as production — the `zn_test_*` key prefix alone determines test mode.
  </Step>

  <Step title="Exercise the full lifecycle with the test products">
    Create → receive webhooks → tracking on `test-success` → every synchronous and asynchronous failure scenario → (if used) cancel and return flows. Confirm webhook signature verification passes on real sandbox deliveries, not just unit-test fixtures.
  </Step>

  <Step title="Tail webhook-receiver logs during the test run">
    Confirm signature verification passes and event names map correctly.
  </Step>

  <Step title="Write MIGRATION_REPORT.md">
    Summary including:

    * Number of call sites migrated
    * Number of gap rows requiring human decisions
    * Test suite pass/fail counts
    * Outstanding TODOs
  </Step>
</Steps>

<Note>
  **Sandbox limitations** — test mode skips wallet balance, URL reachability, and address validation checks so you can test without funding real wallets or hitting real retailers. Plan a small set of real production orders as the final cutover check before declaring the migration done.
</Note>

**Commit:** `docs(zinc): migration report`.

***

## What to do when stuck

<AccordionGroup>
  <Accordion title="The reference disagrees with the customer's understanding">
    Trust the reference (it's generated against v2's live OpenAPI schema). File an issue against the reference page with the specific discrepancy.
  </Accordion>

  <Accordion title="Customer code references an endpoint not in the guide">
    Add a row to `MIGRATION_GAPS.md` with `endpoint not documented in migration guide`. Do not guess.
  </Accordion>

  <Accordion title="Repeated identical edits across files">
    Consider whether the customer has an internal SDK wrapper you can edit once instead of N times. Look in `lib/`, `clients/`, `services/zinc*` before mass-editing.
  </Accordion>

  <Accordion title="Tests fail in ways unrelated to the migration">
    Stop and report — don't try to fix unrelated test infrastructure.
  </Accordion>
</AccordionGroup>

***

## Output files summary

By the end of the migration, the customer's repo has these new top-level files:

| File                     | Purpose                                                    |
| ------------------------ | ---------------------------------------------------------- |
| `MIGRATION_INVENTORY.md` | Every v1 call site, status per phase                       |
| `MIGRATION_PREREQS.md`   | Human checklist for dashboard / wallet / credentials setup |
| `MIGRATION_GAPS.md`      | Endpoints with no v2 equivalent + suggested workarounds    |
| `MIGRATION_REPORT.md`    | Final summary                                              |

And the code is ported on a branch named `zinc-v2-migration`, ready for the customer's normal PR review process.
