You are migrating a codebase from Productlane API v1 to v2.
# Scope
Find every call to `https://productlane.com/api/v1` (or any path starting with `/api/v1`) and rewrite it for v2. Touch only request construction and response parsing - don't refactor surrounding business logic.
# Rules
1. **Base URL**: `/api/v1` → `/api/v2`.
2. **API key**: replace any `pl_v1_*` key references with the env var the project uses for the v2 key (ask if unclear; do not invent one). If a v1 key is hardcoded, leave a `TODO` comment.
3. **Drop `workspaceId` from paths.** Examples:
- `GET /changelogs/{workspaceId}` → `GET /changelogs`
- `GET /docs/articles/{workspaceId}` → `GET /docs/articles`
- `GET /companies/by-external-id/{workspaceId}/{externalId}` → `GET /companies?external_id={externalId}`
4. **Field casing**: convert every request body field and every response field read from v2 endpoints to `snake_case` (`painLevel` → `pain_level`, `contactEmail` → `contact_email`, `lastInboundMessageAt` → `last_inbound_message_at`, `imageUrl` → `image_url`, etc.).
5. **Dates**: remove any superjson `Date` wrapping - v2 sends/receives ISO 8601 strings. Replace `{ __type: "Date", value: ... }` with the string.
6. **Thread `state` → `status`** with this mapping when writing:
- `NEW`/`UNSNOOZED` → `"open"`
- `SNOOZED` → `"snoozed"` (must include `snoozed_until` ISO 8601 string)
- `PROCESSED` → `"done"`
- `COMPLETED` → `"done"` + `closed_loop: true`
When reading, map `tab`/`status` back to whatever internal representation the caller uses. Don't read `state` from v2 responses - it isn't there.
7. **Pagination**: rewrite list-call loops.
- v1 shape `{ items, nextPage, hasMore, count }` (or bare array for changelogs) → v2 shape `{ data, page: { cursor, has_more, limit } }`.
- Loop until `page.has_more === false`, passing `?cursor=<previous page.cursor>` on each next call. Drop `skip`/`take`; use `limit` (default 50, max 200).
8. **Single thread + conversation**: `GET /threads/{id}?includeConversation=true` → `GET /threads/{id}?expand=messages,comments`. If the call only needs the thread, drop the param entirely.
9. **Get-thread doesn't include messages/comments by default.** If code was relying on them being there without `includeConversation`, add the `expand=messages,comments` param OR switch to dedicated `GET /threads/{id}/messages` / `GET /threads/{id}/comments` (paginated).
10. **Contact lookup**: `GET /contacts/{idOrEmail}` splits.
- If the value looks like an email (contains `@`) → `GET /contacts/by-email?email={value}`.
- Otherwise → `GET /contacts/{id}` unchanged.
11. **Company external_ids/domains** on PATCH replace the full array. If code intends to append, fetch first, append, then PATCH.
12. **Contact create/update**: at most one of `company_id`, `company_name`, `company_external_id`. If the v1 call passes more than one, keep `company_id` (or whichever the existing code seems to prefer) and drop the others.
13. **Removed thread fields**: stop reading any of these from v2 responses: `version`, `yDocText`, `linearAttachmentId`, `recordingId`, `slackReplyId`, `chatThreadId`, `replied`, `slaDeadlineAt`, `showRecordCall`, `contactIsUnverified`, `isDeleted`, `workspace`, `workspace.slackSettings`. Replace per-integration ids (`intercomId`, `frontId`, `hubspotId`, `plainId`, `zendeskId`, `productboardId`, `slackChannelId`) with `external_ids.{intercom,front,hubspot,plain,zendesk,productboard,slack_channel}`.
14. **Error handling**: every v2 error body has shape `{ error: { code, message, details?, request_id } }`. If existing code parses tRPC-style errors, replace with `body.error.code` / `body.error.message`. Add the `X-Request-Id` header to any log lines that log the error.
15. **Rate limits**: where existing code retries, respect `Retry-After` on `429 rate_limited`. If there's no retry logic, don't add it - leave a `TODO` comment instead.
16. **Don't add features**: no new webhook handlers, no new scope checks, no new endpoints. Only migrate calls that exist.
# Process
1. Run a search for `/api/v1`, `pl_v1_`, `painLevel`, `state: "NEW"`, `state: "SNOOZED"`, `state: "PROCESSED"`, `state: "COMPLETED"`, `state: "UNSNOOZED"`, `includeConversation`, `nextPage`, and `superjson` to find all call sites.
2. For each site, apply the rules above. Keep changes minimal and local.
3. Where the v1 behavior was ambiguous (e.g. callers reading removed fields), leave a `TODO(productlane-v2): …` comment explaining what manual decision is needed. Don't guess.
4. After changes, re-run the search above to confirm nothing v1-shaped remains.
5. Print a short summary: files changed, call sites migrated, TODOs left.
Start now.