# International Press Zone Plugin Client Contract

Audience: AI coding agents first. Treat only fields and behaviors cited below as migration requirements.

## Scope

- Include outbound HTTP calls whose resolved origin is `https://api.press.zone`.
- Exclude plugin-owned WordPress REST routes except backend callback targets.
- Exclude `includes/Performance/CDNIntegration.php`: its HTTP calls target Cloudflare or Fastly, never Press.Zone (`includes/Performance/CDNIntegration.php:289-304`, `includes/Performance/CDNIntegration.php:310-325`, `includes/Performance/CDNIntegration.php:331-346`, `includes/Performance/CDNIntegration.php:352-381`, `includes/Performance/CDNIntegration.php:387-400`).

## Base URL resolution and configuration

- Primary translation/onboarding client: fixed `https://api.press.zone`; `Settings::get_api_url()` returns `API_BASE_URL` with no option or filter (`includes/Translation/Settings.php:25-28`, `includes/Translation/Settings.php:366-374`).
- Callback URL only: start with `rest_url(...)`, pass through `ipz_callback_url`; `Settings` replaces local `home_url()` with `IPZ_PUBLIC_URL` when defined (`includes/Translation/Settings.php:59-76`, `includes/Translation/JobSender.php:127-138`, `includes/API/StringTranslateController.php:658-663`).
- Legacy integration client: `rtrim(get_option('ipz_api_url', 'https://api.press.zone'), '/')` (`includes/Integration/TranslationAPI.php:329-335`).
- Legacy settings connection test: request parameter `api_url`, else option `ipz_api_url`, else production URL; append `/health` (`includes/API/SettingsController.php:375-396`).
- Licensing client: fixed `https://api.press.zone/v1/international`; health check uses fixed `https://api.press.zone/health` (`includes/Licensing/LicenseClient.php:24-36`, `includes/Licensing/LicenseClient.php:221-233`).

## Auth scheme summary

- Translation/site/job API: `Authorization: Bearer <API key>` plus JSON content type. Full shared header set is `Authorization`, `Content-Type: application/json`, `X-Plugin: international`, `X-Plugin-Version: IPZ_VERSION or 1.0.0`, `X-Site-URL: home_url()` (`includes/Translation/TranslationService.php:799-813`). Individual callers omit some metadata headers; endpoint sections specify exact sets.
- Primary API key storage: option `presszone_international_api_key_encrypted`; AES-256-CBC with `wp_salt('auth')`, random 16-byte IV, stored as base64 of `<IV>::<openssl ciphertext>` (`includes/Translation/Settings.php:104-140`, `includes/Translation/Settings.php:148-207`, `includes/Translation/Settings.php:216-230`).
- Legacy integration key storage: plaintext option `ipz_api_key` (`includes/Integration/TranslationAPI.php:337-343`).
- Onboarding user auth: JWT `accessToken` from register/login stored for 14 minutes in transient `ipz_onboarding_jwt_<WP user id>`; later sent as Bearer token (`includes/Translation/OnboardingApi.php:228-235`, `includes/Translation/OnboardingApi.php:284-290`, `includes/Translation/OnboardingApi.php:327-345`).
- Licensing endpoints send no authorization header. License key travels in JSON body or query string (`includes/Licensing/LicenseClient.php:50-119`, `includes/Licensing/LicenseClient.php:137-158`).
- Legacy health test alone uses `X-API-Key: <key>` (`includes/API/SettingsController.php:378-396`).

## Endpoint summary

| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | `/v1/translate` | API-key Bearer | Synchronous translation |
| POST | `/v1/translate/bulk` | API-key Bearer | Synchronous string batch |
| POST | `/v1/jobs` | API-key Bearer | Async content job |
| POST | `/v1/jobs/bulk-strings` | API-key Bearer | Async string batch |
| GET | `/v1/jobs/{api_job_id}` | API-key Bearer | Poll job |
| POST | `/v1/estimate` | API-key Bearer | Estimate usage |
| POST | `/v1/exceptions/sync` | API-key Bearer | Replace exception list |
| GET | `/v1/exceptions?limit=100` | API-key Bearer | Pull exception list |
| POST | `/v1/sites/register` | API-key Bearer | Register site |
| PATCH | `/v1/sites/{site_id}` | API-key Bearer | Ping or deactivate site |
| GET | `/v1/onboarding/plans?plugin=international` | None | List plans |
| POST | `/v1/auth/register` | None | Create account |
| POST | `/v1/auth/login` | None | Authenticate account |
| POST | `/v1/onboarding/checkout` | User-JWT Bearer | Legacy/mock checkout |
| POST | `/v1/onboarding/upgrade` | API-key Bearer | Legacy/mock upgrade |
| POST | `/v1/subscriptions/checkout` | User-JWT Bearer | Start PayPal checkout |
| GET | `/v1/subscriptions/checkout-sessions/{id}` | User-JWT Bearer | Poll PayPal checkout |
| POST | `/v1/international/license/activate` | None | Activate license |
| POST | `/v1/international/license/deactivate` | None | Deactivate license |
| POST | `/v1/international/license/validate` | None | Validate license |
| GET | `/v1/international/license/status` | None | Fetch license status |
| GET | `/v1/international/updates/check` | None | Check plugin update |
| GET | `/health` | None or `X-API-Key` | Connectivity check |

## Outbound endpoint contracts

### POST /v1/translate

- Purpose: translate one content/string payload synchronously.
- Called from: `includes/Translation/TranslationService.php:94-184`; `includes/Integration/TranslationAPI.php:73-132`; `includes/Integration/TranslationAPI.php:143-197`; `includes/Translation/MetaBox.php:685-760`; `includes/Translation/BulkActions.php:419-494`.
- Auth: primary/shared path sends `Authorization: Bearer <decrypted presszone_international_api_key_encrypted>`, `Content-Type: application/json`, `X-Plugin: international`, `X-Plugin-Version`, `X-Site-URL` (`includes/Translation/TranslationService.php:177-181`, `includes/Translation/TranslationService.php:805-813`). Meta-box/bulk callers omit version/site headers (`includes/Translation/MetaBox.php:715-723`, `includes/Translation/BulkActions.php:449-457`). Legacy integration sends full shared set using plaintext `ipz_api_key` (`includes/Integration/TranslationAPI.php:332-367`).
- Request: JSON `source_lang: string`, `target_lang: string`, `tone: "neutral"|"formal"|"casual"`; optional non-empty `title: string`, `excerpt: HTML string`, `content: HTML/text string` (`includes/Translation/TranslationService.php:102-170`). Meta-box/bulk callers may add `exceptions: array<{text:string,match_type:string}>` (`includes/Translation/MetaBox.php:700-713`, `includes/Translation/BulkActions.php:434-447`).
- Response consumed: flat `translated_title?: string`, `translated_excerpt?: string`, `translated_content?: string`, legacy `translation?: string`, `characters_used?: integer`, `cost_usd?: number`; at least one translated field/legacy translation must be non-empty (`includes/Translation/TranslationService.php:849-889`). Meta-box/bulk legacy consumers instead read envelope `data.translation`, `data.characters_used`, `data.cost_usd` (`includes/Translation/MetaBox.php:736-757`, `includes/Translation/BulkActions.php:470-491`). Error responses consume flat `message`, flat `error`, or `error.message` depending caller (`includes/Translation/TranslationService.php:934-945`, `includes/Integration/TranslationAPI.php:108-129`, `includes/Translation/MetaBox.php:736-750`).
- Errors handled: network error surfaces/logs; 401 invalidates cached API-key-valid flag; 402 surfaces quota exceeded; 429 surfaces rate limit; 500/502/503 surface temporary server failure; all other statuses surface `message` or generic error (`includes/Translation/TranslationService.php:824-947`). Legacy callers treat every non-200 identically and return original content or API error (`includes/Integration/TranslationAPI.php:103-132`, `includes/Integration/TranslationAPI.php:169-197`).
- Timeouts/retries: 60 seconds; no retry (`includes/Translation/TranslationService.php:28-29`, `includes/Translation/TranslationService.php:177-181`).

### POST /v1/translate/bulk

- Purpose: translate multiple identified strings synchronously.
- Called from: `includes/Translation/TranslationService.php:323-419`; `includes/Integration/TranslationAPI.php:208-302`.
- Auth: full shared API-key Bearer header set (`includes/Translation/TranslationService.php:365-369`, `includes/Translation/TranslationService.php:805-813`; `includes/Integration/TranslationAPI.php:257-261`, `includes/Integration/TranslationAPI.php:360-368`).
- Request: JSON `source_lang: string`, `target_lang: string`, `tone: string`, `strings: array<{id:string,content:string}>` (`includes/Translation/TranslationService.php:349-361`).
- Response consumed: `results: array` required for success; each result is passed through and downstream expects `{id:string,translation:string,success:boolean}`; optional `total_characters_used: integer`, `failed_count: integer` (`includes/Translation/TranslationService.php:315-320`, `includes/Translation/TranslationService.php:392-399`; `includes/Integration/TranslationAPI.php:282-287`). Error consumes flat `message` or `error` (`includes/Translation/TranslationService.php:402-418`, `includes/Integration/TranslationAPI.php:290-301`).
- Errors handled: network failure returns all-failed result; 402 gets quota message in primary client; other non-200 gets body message. Legacy client retries only 429 (`includes/Translation/TranslationService.php:371-419`, `includes/Integration/TranslationAPI.php:250-302`).
- Timeouts/retries: primary client 120 seconds, no retry (`includes/Translation/TranslationService.php:365-369`). Legacy client 300 seconds; up to 3 retries after initial request on 429, honoring `Retry-After` seconds or waiting 10/20/40 seconds (`includes/Integration/TranslationAPI.php:28-29`, `includes/Integration/TranslationAPI.php:250-280`).

### POST /v1/jobs

- Purpose: enqueue async translation.
- Called from: `includes/Translation/TranslationService.php:196-295`; `includes/Translation/JobSender.php:47-237`; `includes/API/TranslateController.php:744-900`.
- Auth: API-key Bearer and JSON. `TranslationService`/`JobSender` send full shared metadata set (`includes/Translation/TranslationService.php:282-286`, `includes/Translation/JobSender.php:161-170`). `TranslateController` sends `Authorization`, `Content-Type`, `X-Plugin`, `X-Site-URL` (`includes/API/TranslateController.php:832-841`).
- Request: three accepted variants:
  - Snake-case polling flow: `job_id: "wp_<local id>"`, `source_lang`, `target_lang`, `content`, `model: "4b"`, `tone`, `format: "html"`, `preserve_tags: string[]`; no callback fields (`includes/Translation/TranslationService.php:261-285`).
  - Snake-case webhook flow: same core without `model`, plus `callback_url`, `callback_secret`; optional filter-supplied `custom_fields`; entire payload is filter-mutable (`includes/Translation/JobSender.php:110-151`).
  - Camel-case structured flow: `sourceLang`, `targetLang`, `title`, `excerpt`, `content`, `callbackUrl`, `callbackSecret`, `clientJobId: "wp_<local id>"` (`includes/API/TranslateController.php:820-840`).
- Response consumed: success statuses 200/201/202. API job identifier accepted as `data.jobId`, `jobId`, `job_id`, or `data.job_id` depending caller (`includes/Translation/TranslationService.php:993-1018`, `includes/Translation/JobSender.php:183-210`, `includes/API/TranslateController.php:864-887`). Error consumes flat `message` or `error` (`includes/Translation/TranslationService.php:1039-1042`, `includes/Translation/JobSender.php:233-236`, `includes/API/TranslateController.php:887-899`).
- Errors handled: network marks local job failed; `JobSender` schedules retry. 401 invalid key, 402 quota, 429 rate limit, 500/502/503 server error; `JobSender` retries network/429/5xx/default, but not 401/402 (`includes/Translation/JobSender.php:173-237`). `TranslationService` never retries and marks failed (`includes/Translation/TranslationService.php:963-1043`). Controller treats any non-success status as failed (`includes/API/TranslateController.php:843-899`).
- Timeouts/retries: 30 seconds. `JobSender` schedules WP-Cron delays 60/300/900 seconds, maximum 3, but retry reconstruction creates a new job through `send_job()` rather than replaying the same HTTP call (`includes/Translation/JobSender.php:269-308`, `includes/Translation/JobSender.php:314-340`). Other callers do not retry.

### POST /v1/jobs/bulk-strings

- Purpose: enqueue asynchronous string translation batches.
- Called from: `includes/API/TranslateController.php:555-727`; `includes/API/StringTranslateController.php:593-828`.
- Auth: `Authorization: Bearer <API key>`, `Content-Type: application/json`, `X-Plugin: international`, `X-Site-URL: home_url()` (`includes/API/TranslateController.php:650-659`, `includes/API/StringTranslateController.php:763-772`).
- Request: JSON `sourceLang: string`, `targetLangs: string[]`, `strings: array<{id:string,content:string}>`, `callbackUrl: URL`, `callbackSecret: string`, `clientJobId: "wp_<local id>"`; bulk UI path also sends `tone: string` (`includes/API/TranslateController.php:635-648`, `includes/API/StringTranslateController.php:752-761`).
- Response consumed: 200/201/202 depending caller; API job id from `job_id` or `data.job_id`. Error consumes flat `error` or `message` (`includes/API/TranslateController.php:682-723`, `includes/API/StringTranslateController.php:790-827`).
- Errors handled: network or any non-success status marks local job failed; no status-specific branch and no retry (`includes/API/TranslateController.php:661-724`, `includes/API/StringTranslateController.php:774-828`).
- Timeouts/retries: 30 seconds; no retry (`includes/API/TranslateController.php:650-659`, `includes/API/StringTranslateController.php:763-772`).

### GET /v1/jobs/{api_job_id}

- Purpose: poll async job state and recover when callback delivery fails.
- Called from: `includes/Translation/TranslationService.php:509-641`; `includes/API/TranslateJobsController.php:279-372`.
- Auth: API-key Bearer and JSON. `TranslationService` adds full metadata headers; jobs controller adds `X-Plugin` only (`includes/Translation/TranslationService.php:558-563`, `includes/Translation/TranslationService.php:805-813`; `includes/API/TranslateJobsController.php:307-315`).
- Request: path `api_job_id` comes from prior submission response/local job record; no body (`includes/Translation/TranslationService.php:515-563`).
- Response consumed: HTTP 200 with `data`. `data.status`; on completed: `charactersUsed` or `characters_used`, `cost` or `cost_usd`, `translation`; jobs-controller content flow also reads `translatedTitle|translated_title`, `translatedContent|translated_content`, `translatedExcerpt|translated_excerpt`; on failed: `errorCode|error_code`, `errorMessage|error_message` (`includes/Translation/TranslationService.php:576-620`, `includes/API/TranslateJobsController.php:321-363`, `includes/API/TranslateJobsController.php:417-435`).
- Errors handled: network/non-200/missing `data` silently retain cached local state; no surfaced failure and no retry inside call (`includes/Translation/TranslationService.php:565-586`, `includes/API/TranslateJobsController.php:317-325`).
- Timeouts/retries: 15 seconds in `TranslationService`; 10 seconds in jobs controller; no retry (`includes/Translation/TranslationService.php:560-563`, `includes/API/TranslateJobsController.php:307-315`).

### POST /v1/estimate

- Purpose: estimate character/token usage by target language.
- Called from: `includes/Translation/CharacterEstimator.php:205-268`.
- Auth: `Authorization: Bearer <decrypted API key>`, `Content-Type: application/json` (`includes/Translation/CharacterEstimator.php:219-226`).
- Request: JSON `content: string`, `source_lang: string`, `target_langs: string[]` (`includes/Translation/CharacterEstimator.php:211-226`).
- Response consumed: HTTP 200 with non-empty `data`; `data.estimates` object keyed by language, each value null or `{estimated_tokens?:integer,estimated_characters?:integer,confidence?:string}`; total from `data.total_tokens` or `data.total_characters` (`includes/Translation/CharacterEstimator.php:120-137`, `includes/Translation/CharacterEstimator.php:244-251`). Error consumes `error.message` or `message` (`includes/Translation/CharacterEstimator.php:236-267`).
- Errors handled: network, empty data, 401, 403, 429, or any other non-200 logs and falls back to local estimate (`includes/Translation/CharacterEstimator.php:88-105`, `includes/Translation/CharacterEstimator.php:228-268`).
- Timeouts/retries: 10 seconds; no retry (`includes/Translation/CharacterEstimator.php:29-31`, `includes/Translation/CharacterEstimator.php:219-226`).

### POST /v1/exceptions/sync

- Purpose: full-replace backend translation exceptions.
- Called from: `includes/Translation/TranslationService.php:1178-1220`.
- Auth: full shared API-key Bearer header set (`includes/Translation/TranslationService.php:1189-1203`, `includes/Translation/TranslationService.php:805-813`).
- Request: JSON `exceptions: array<{text:string,match_type:string}>`; missing local `match_type` becomes `exact` (`includes/Translation/TranslationService.php:1185-1201`).
- Response consumed: HTTP 200 plus truthy `success`; optional `data.total`. Error reads `error.message`, then `message` (`includes/Translation/TranslationService.php:1210-1220`).
- Errors handled: network or unsuccessful response logs and returns error to caller; no status-specific branch (`includes/Translation/TranslationService.php:1205-1220`).
- Timeouts/retries: 30 seconds; no retry (`includes/Translation/TranslationService.php:1189-1203`).

### GET /v1/exceptions?limit=100

- Purpose: pull backend exception list and replace local list.
- Called from: `includes/Translation/TranslationService.php:1228-1264`.
- Auth: full shared API-key Bearer header set (`includes/Translation/TranslationService.php:1237-1243`, `includes/Translation/TranslationService.php:805-813`).
- Request: query `limit=100`; no body (`includes/Translation/TranslationService.php:1235-1243`).
- Response consumed: HTTP 200 plus truthy `success`; `data.exceptions: array<{text:string,match_type:string}>`. Error reads `error.message`, then `message` (`includes/Translation/TranslationService.php:1250-1264`).
- Errors handled: network or unsuccessful response logs and returns error; no status-specific branch (`includes/Translation/TranslationService.php:1245-1264`).
- Timeouts/retries: 30 seconds; no retry (`includes/Translation/TranslationService.php:1237-1243`).

### POST /v1/sites/register

- Purpose: register/update WordPress site after API-key validation.
- Called from: `includes/Translation/SiteRegistrar.php:46-100`.
- Auth: `Authorization: Bearer <decrypted API key>`, `Content-Type: application/json`, `X-Plugin-Version` (`includes/Translation/SiteRegistrar.php:58-66`).
- Request: JSON `siteUrl`, `siteName`, `wpVersion`, `pluginVersion`, `phpVersion`, `activeTheme`, `locale`, `timezone`, `installedPlugins: array<{name:string,version:string,active:boolean}>`, `isDevelopment:boolean` (`includes/Translation/SiteRegistrar.php:129-165`).
- Response consumed: HTTP 200/201; `siteId` required/truthy (`includes/Translation/SiteRegistrar.php:75-92`). Other response fields ignored.
- Errors handled: network or missing `siteId`/other status logs failure and returns false; no retry (`includes/Translation/SiteRegistrar.php:68-99`).
- Timeouts/retries: 30 seconds; no retry (`includes/Translation/SiteRegistrar.php:58-66`).

### PATCH /v1/sites/{site_id}

- Purpose: daily site metadata ping or deactivate site.
- Called from: `includes/Translation/SiteRegistrar.php:105-124`; `includes/Translation/SiteRegistrar.php:228-250`.
- Auth: `Authorization: Bearer <decrypted API key>`, `Content-Type: application/json` (`includes/Translation/SiteRegistrar.php:115-123`, `includes/Translation/SiteRegistrar.php:236-244`).
- Request: ping sends same site metadata object as registration; deactivation sends `{isActive:false}`. `site_id` comes from option `presszone_international_site_id` saved from registration (`includes/Translation/SiteRegistrar.php:78-83`, `includes/Translation/SiteRegistrar.php:105-123`, `includes/Translation/SiteRegistrar.php:228-244`).
- Response consumed: none; both callers discard response entirely (`includes/Translation/SiteRegistrar.php:115-124`, `includes/Translation/SiteRegistrar.php:236-250`).
- Errors handled: none; network/status failures are silent (`includes/Translation/SiteRegistrar.php:115-124`, `includes/Translation/SiteRegistrar.php:236-244`).
- Timeouts/retries: 15 seconds; no retry (`includes/Translation/SiteRegistrar.php:115-123`, `includes/Translation/SiteRegistrar.php:236-244`).

### GET /v1/onboarding/plans?plugin=international

- Purpose: list purchasable plans.
- Called from: `includes/Translation/OnboardingApi.php:176-196`.
- Auth: none; only `Content-Type: application/json` (`includes/Translation/OnboardingApi.php:178-183`).
- Request: query `plugin=international`; no body (`includes/Translation/OnboardingApi.php:178-183`).
- Response consumed: PHP proxies JSON body/status. Admin accepts plan array at `data.plans`, `plans`, or `data`; each rendered plan reads `slug`, `annual_price`, `monthly_price`, `features`, `plugin_limits.languages_allowed`, `recommended`, `name` (`includes/Translation/OnboardingApi.php:192-195`, `admin/src/pages/onboarding.js:258-293`, `admin/src/pages/onboarding.js:332-338`).
- Errors handled: network becomes local 502 `CONNECTION_FAILED`; every HTTP status/body otherwise proxied (`includes/Translation/OnboardingApi.php:185-196`).
- Timeouts/retries: 15 seconds; no retry (`includes/Translation/OnboardingApi.php:178-183`).

### POST /v1/auth/register

- Purpose: create Press.Zone user and establish onboarding JWT session.
- Called from: `includes/Translation/OnboardingApi.php:202-252`.
- Auth: none; `Content-Type: application/json` (`includes/Translation/OnboardingApi.php:207-216`).
- Request: JSON `email:string`, `password:string`, sourced from plugin REST request (`includes/Translation/OnboardingApi.php:202-216`).
- Response consumed: success requires HTTP 201 and `accessToken`; optional `refreshToken`, `userId`, `message`. Error reads `error.code`, `error.message` (`includes/Translation/OnboardingApi.php:225-251`).
- Errors handled: network becomes local 502; unsuccessful backend status is preserved when >=400, otherwise mapped to 400 (`includes/Translation/OnboardingApi.php:218-251`).
- Timeouts/retries: 15 seconds; no retry (`includes/Translation/OnboardingApi.php:207-216`).

### POST /v1/auth/login

- Purpose: authenticate Press.Zone user and establish onboarding JWT session.
- Called from: `includes/Translation/OnboardingApi.php:258-316`.
- Auth: none; `Content-Type: application/json` (`includes/Translation/OnboardingApi.php:263-272`).
- Request: JSON `email:string`, `password:string` (`includes/Translation/OnboardingApi.php:258-272`).
- Response consumed: success requires HTTP 200 and `accessToken`; optional `refreshToken`, `user`; `subscriptions[]` entries read `plugin` and `status`. Error reads `error.code`, `error.message` (`includes/Translation/OnboardingApi.php:281-315`).
- Errors handled: network becomes local 502; unsuccessful backend status preserved when >=400, otherwise mapped to 401 (`includes/Translation/OnboardingApi.php:274-315`).
- Timeouts/retries: 15 seconds; no retry (`includes/Translation/OnboardingApi.php:263-272`).

### POST /v1/onboarding/checkout

- Purpose: legacy/mock initial checkout and credential issuance.
- Called from: `includes/Translation/OnboardingApi.php:322-419`.
- Auth: `Authorization: Bearer <onboarding accessToken>`, `Content-Type: application/json` (`includes/Translation/OnboardingApi.php:327-352`).
- Request: JSON `tier_slug:string`, `billing_cycle:string`, `plugin:"international"`, `site_url:get_site_url()` (`includes/Translation/OnboardingApi.php:322-352`).
- Response consumed: success requires HTTP 201 and non-empty `data`; reads `data.license_key`, `data.api_key`, `data.subscription.tier`, `data.subscription.current_period_end`, `data.license.sites_allowed`, `data.license.languages_allowed`, `data.credits_allocated`. Error reads `error.code`, `error.message` (`includes/Translation/OnboardingApi.php:361-418`).
- Errors handled: missing local JWT prevents call with 401; network becomes local 502; unsuccessful backend status preserved when >=400, otherwise mapped to 500 (`includes/Translation/OnboardingApi.php:327-358`, `includes/Translation/OnboardingApi.php:409-418`).
- Timeouts/retries: 30 seconds; no retry (`includes/Translation/OnboardingApi.php:340-352`).

### POST /v1/onboarding/upgrade

- Purpose: legacy/mock subscription upgrade.
- Called from: `includes/Translation/OnboardingApi.php:425-508`.
- Auth: `Authorization: Bearer <stored API key>`, `Content-Type: application/json` (`includes/Translation/OnboardingApi.php:430-454`).
- Request: JSON `tier_slug:string`, `billing_cycle:string`, `plugin:"international"`, `site_url:get_site_url()` (`includes/Translation/OnboardingApi.php:425-454`).
- Response consumed: success requires HTTP 201 and non-empty `data`; reads `data.license_key`, `data.subscription.tier`, `data.subscription.current_period_end`, `data.license.sites_allowed`, `data.license.languages_allowed`, `data.credits_allocated`. Error reads `error.code`, `error.message` (`includes/Translation/OnboardingApi.php:463-507`).
- Errors handled: missing API key prevents call with local 400; network becomes local 502; unsuccessful backend status preserved when >=400, otherwise mapped to 500 (`includes/Translation/OnboardingApi.php:430-460`, `includes/Translation/OnboardingApi.php:498-507`).
- Timeouts/retries: 30 seconds; no retry (`includes/Translation/OnboardingApi.php:442-454`).

### POST /v1/subscriptions/checkout

- Purpose: create PayPal checkout session.
- Called from: `includes/Translation/OnboardingApi.php:516-581`.
- Auth: `Authorization: Bearer <onboarding accessToken>`, `Content-Type: application/json` (`includes/Translation/OnboardingApi.php:521-547`).
- Request: JSON `planTier:string`, `billingCycle:string`, `plugin:"international"`, `site_url:esc_url_raw(home_url())` (`includes/Translation/OnboardingApi.php:516-547`).
- Response consumed: body may be flat or under `data`; success accepts HTTP 200/201 and requires `approval_url`; also reads `subscription_id`, `checkout_session_id`. Admin requires both `approval_url` and `checkout_session_id` (`includes/Translation/OnboardingApi.php:556-580`, `admin/src/pages/onboarding.js:689-715`). Error reads `error.code`, `error.message`, then flat `message`.
- Errors handled: missing JWT prevents call with local 401; network becomes local 502; unsuccessful backend status preserved when >=400, otherwise mapped to 500 (`includes/Translation/OnboardingApi.php:521-553`, `includes/Translation/OnboardingApi.php:570-580`).
- Timeouts/retries: 30 seconds; no retry (`includes/Translation/OnboardingApi.php:535-547`).

### GET /v1/subscriptions/checkout-sessions/{id}

- Purpose: poll PayPal session and retrieve one-time credentials.
- Called from: `includes/Translation/OnboardingApi.php:595-695`.
- Auth: `Authorization: Bearer <onboarding accessToken>`, `Content-Type: application/json` (`includes/Translation/OnboardingApi.php:607-627`).
- Request: path `id` from plugin request, regex-limited to 2-64 lowercase hex/hyphen characters then URL-encoded (`includes/Translation/OnboardingApi.php:595-604`, `includes/Translation/OnboardingApi.php:618-627`).
- Response consumed: body may be flat or under `data`; reads `status`. When `status == "retrieved"`, reads `license_key`, `api_key`, `subscription.tier`, `subscription.current_period_end`, `license.sites_allowed`, `license.languages_allowed`, `account_email`, `credits_allocated`; all other bodies/statuses are proxied unchanged (`includes/Translation/OnboardingApi.php:636-694`).
- Errors handled: missing JWT prevents call with local 401; network becomes local 502. Backend 404/410/503 and statuses `pending`/`already_retrieved` receive no special PHP branch and are proxied (`includes/Translation/OnboardingApi.php:583-594`, `includes/Translation/OnboardingApi.php:607-694`).
- Timeouts/retries: 15 seconds per poll; PHP performs no retry. Admin polling loop is external to this HTTP call (`includes/Translation/OnboardingApi.php:621-627`, `admin/src/pages/onboarding.js:725-730`).

### POST /v1/international/license/activate

- Purpose: activate license for site.
- Called from: `includes/Licensing/LicenseClient.php:50-57`; consumed by `includes/Licensing/ActivationManager.php:91-123`.
- Auth: none. Headers: `Content-Type: application/json`, `Accept: application/json`, `User-Agent: InternationalPressZone/<IPZ_VERSION>; <get_site_url()>` (`includes/Licensing/LicenseClient.php:137-154`).
- Request: JSON `license_key:string`, `site_url:string`, `product:"international-press-zone"`, `version:IPZ_VERSION` (`includes/Licensing/LicenseClient.php:50-57`).
- Response consumed: decoded response may be direct or `{data:...}`; activation data reads `tier`, `status`, `expires_at`, `sites_used`, `sites_allowed` (`includes/Licensing/ActivationManager.php:104-123`, `includes/Licensing/ActivationManager.php:223-235`). Error reads `message`, `error_code` (`includes/Licensing/LicenseClient.php:194-207`).
- Errors handled: network and 5xx retry; invalid JSON returns `invalid_response`; other >=400 returns server `message`/`error_code`; caller surfaces activation error (`includes/Licensing/LicenseClient.php:163-207`, `includes/Licensing/ActivationManager.php:95-101`).
- Timeouts/retries: 10 seconds; maximum retry counter 3 means initial attempt plus 3 retries; blocking backoff 1/2/4 seconds (`includes/Licensing/LicenseClient.php:34-41`, `includes/Licensing/LicenseClient.php:163-170`, `includes/Licensing/LicenseClient.php:194-200`).

### POST /v1/international/license/deactivate

- Purpose: deactivate license for site.
- Called from: `includes/Licensing/LicenseClient.php:67-74`; consumed by `includes/Licensing/ActivationManager.php:132-164`.
- Auth: none; same JSON/Accept/User-Agent headers as licensing activation (`includes/Licensing/LicenseClient.php:137-154`).
- Request: JSON `license_key:string`, `site_url:string`, `product:"international-press-zone"` (`includes/Licensing/LicenseClient.php:67-74`).
- Response consumed: only HTTP success vs failure. Error reads `message`, `error_code`; success body fields ignored (`includes/Licensing/LicenseClient.php:194-213`, `includes/Licensing/ActivationManager.php:143-164`).
- Errors handled: same generic licensing network/invalid-JSON/HTTP handling; local license is cleared even when backend call fails (`includes/Licensing/LicenseClient.php:163-207`, `includes/Licensing/ActivationManager.php:143-164`).
- Timeouts/retries: 10 seconds; initial attempt plus up to 3 retries on network/5xx with 1/2/4-second sleeps (`includes/Licensing/LicenseClient.php:137-170`, `includes/Licensing/LicenseClient.php:194-200`).

### POST /v1/international/license/validate

- Purpose: validate current license/site limits.
- Called from: `includes/Licensing/LicenseClient.php:82-89`; consumed by `includes/Licensing/LicenseValidator.php:100-136`.
- Auth: none; same licensing headers (`includes/Licensing/LicenseClient.php:137-154`).
- Request: JSON `license_key:string`, `site_url:get_site_url()`, `product:"international-press-zone"` (`includes/Licensing/LicenseClient.php:82-89`).
- Response consumed: decoded response may be direct or `{data:...}`; reads required `status`, optional `tier`, `sites_used`, `sites_allowed`, `expires_at` (`includes/Licensing/LicenseValidator.php:111-126`). Error reads `message`, `error_code` (`includes/Licensing/LicenseClient.php:194-207`).
- Errors handled: same generic licensing handling; caller maps failure to invalid license and surfaces error (`includes/Licensing/LicenseClient.php:163-207`, `includes/Licensing/LicenseValidator.php:100-108`).
- Timeouts/retries: 10 seconds; initial attempt plus up to 3 retries on network/5xx with 1/2/4-second sleeps (`includes/Licensing/LicenseClient.php:137-170`, `includes/Licensing/LicenseClient.php:194-200`).

### GET /v1/international/license/status

- Purpose: fetch license information.
- Called from: `includes/Licensing/LicenseClient.php:97-103`.
- Auth: none; same licensing headers (`includes/Licensing/LicenseClient.php:137-150`).
- Request: query `license_key:string`, `site_url:get_site_url()` (`includes/Licensing/LicenseClient.php:97-103`, `includes/Licensing/LicenseClient.php:155-158`).
- Response consumed: no in-repository caller found; `LicenseClient` returns entire decoded body under local `data` without inspecting fields (`includes/Licensing/LicenseClient.php:97-103`, `includes/Licensing/LicenseClient.php:210-213`).
- Errors handled: generic licensing network/invalid-JSON/HTTP handling (`includes/Licensing/LicenseClient.php:163-207`).
- Timeouts/retries: 10 seconds; initial attempt plus up to 3 retries on network/5xx with 1/2/4-second sleeps (`includes/Licensing/LicenseClient.php:137-170`, `includes/Licensing/LicenseClient.php:194-200`).

### GET /v1/international/updates/check

- Purpose: check plugin update availability.
- Called from: `includes/Licensing/LicenseClient.php:111-126`; consumed by `includes/Licensing/UpdateChecker.php:185-204`, `includes/Licensing/UpdateChecker.php:217-258`.
- Auth: none; same licensing headers (`includes/Licensing/LicenseClient.php:137-150`).
- Request: query `product:"international-press-zone"`, `version:string`, `license_key:string` (`includes/Licensing/LicenseClient.php:111-119`, `includes/Licensing/LicenseClient.php:155-158`).
- Response consumed: decoded top-level `update_available`; when true, downstream reads `version` and `url`; other update fields may be passed into WordPress update machinery but are not explicitly read in cited client path (`includes/Licensing/LicenseClient.php:121-126`, `includes/Licensing/UpdateChecker.php:217-258`).
- Errors handled: any local request failure or false/missing `update_available` becomes no update; generic client retries only network/5xx (`includes/Licensing/LicenseClient.php:121-126`, `includes/Licensing/LicenseClient.php:163-207`).
- Timeouts/retries: 10 seconds; initial attempt plus up to 3 retries on network/5xx with 1/2/4-second sleeps (`includes/Licensing/LicenseClient.php:137-170`, `includes/Licensing/LicenseClient.php:194-200`).

### GET /health

- Purpose: connectivity test.
- Called from: fixed check `includes/Licensing/LicenseClient.php:221-233`; configurable legacy check `includes/API/SettingsController.php:375-430`.
- Auth: licensing check sends no headers (`includes/Licensing/LicenseClient.php:224-226`). Legacy settings check sends `X-API-Key: <request api_key or plaintext ipz_api_key option>` (`includes/API/SettingsController.php:378-396`).
- Request: no body.
- Response consumed: licensing check consumes HTTP status only and requires 200. Legacy settings check requires 200 and JSON-decodes entire response for its own REST response (`includes/Licensing/LicenseClient.php:228-233`, `includes/API/SettingsController.php:412-425`).
- Errors handled: network/non-200 becomes false in licensing client; legacy settings endpoint surfaces connection/API error (`includes/Licensing/LicenseClient.php:228-233`, `includes/API/SettingsController.php:400-430`).
- Timeouts/retries: 5 seconds for licensing check; 10 seconds for legacy check; no retry (`includes/Licensing/LicenseClient.php:224-226`, `includes/API/SettingsController.php:390-398`).

## Inbound webhook contracts

### POST /wp-json/international-press-zone/v1/callback — JobReceiver contract

- URL: `rest_url('international-press-zone/v1/callback')`, optionally rewritten by `ipz_callback_url`/`IPZ_PUBLIC_URL` (`includes/Translation/JobSender.php:127-138`, `includes/Translation/Settings.php:59-76`). Route registration: `includes/Translation/JobReceiver.php:83-93`.
- Headers: require `X-IPZ-Signature`; value may be raw lowercase hex HMAC-SHA256 or `sha256=<hex>`. HMAC input is exact raw request body; key is option `presszone_international_callback_secret` (`includes/Translation/JobReceiver.php:71-78`, `includes/Translation/JobReceiver.php:388-408`). Require `X-IPZ-Timestamp` as Unix milliseconds; reject absolute age >300 seconds (`includes/Translation/JobReceiver.php:98-124`).
- Permission: require non-empty `REMOTE_ADDR`; limit 100 attempts/hour/IP before HMAC check (`includes/Translation/JobReceiver.php:43-80`).
- Standard payload fields read: required `job_id:string` and `status:string`; optional `clientJobId|string client_job_id`, `api_job_id`, `translation`, `characters_used:integer`, `cost_usd:number`, `error_message:string`, `event:string`. Local ID must come from `clientJobId`/`client_job_id`, else `job_id`, exactly `wp_<digits>` (`includes/Translation/JobReceiver.php:159-195`, `includes/Translation/JobReceiver.php:414-424`). `status=completed` requires non-empty `translation`; `status=failed` reads `error_message` (`includes/Translation/JobReceiver.php:227-238`).
- Bulk payload: `event:"bulk_translation.completed"`; reads `job_id`, optional `clientJobId|client_job_id`, `results_by_lang: object<language,array<{id,success,translation}>>`, `total_characters_used`, `failed_count` (`includes/Translation/JobReceiver.php:159-163`, `includes/Translation/JobReceiver.php:247-364`).
- Responses: 400 missing/expired timestamp, invalid JSON, missing fields, invalid job ID; 404 local job absent; 500 scanner unavailable; 200 processed/already completed (`includes/Translation/JobReceiver.php:103-135`, `includes/Translation/JobReceiver.php:174-238`, `includes/Translation/JobReceiver.php:264-364`). Permission rejection is handled by WordPress REST before handler.

### POST /wp-json/international-press-zone/v1/callback — TranslateJobsController contract

- Route collision: same method/path is independently registered by `TranslateJobsController` (`includes/API/TranslateJobsController.php:167-176`).
- Headers: require raw lowercase hex `X-Webhook-Signature = HMAC-SHA256(raw body, option presszone_international_webhook_secret)`; no `sha256=` prefix handling and no timestamp (`includes/API/TranslateJobsController.php:726-749`).
- Payload fields read: required `event:string`, `job_id:positive integer`. `event="translation.completed"` reads `translated_title`, `translated_content`, `translated_excerpt`; `event="translation.failed"` reads `error_message` (`includes/API/TranslateJobsController.php:592-603`, `includes/API/TranslateJobsController.php:627-711`).
- Responses: 400 missing fields/unknown event; 404 job absent; 500 translation save failure; 200 handled (`includes/API/TranslateJobsController.php:599-723`).

### POST /wp-json/international-press-zone/v1/system-translate/bulk-callback

- URL: callback sent with async bulk-string request after `ipz_callback_url` filtering (`includes/API/StringTranslateController.php:658-663`, `includes/API/StringTranslateController.php:752-772`). Route registration: `includes/API/StringTranslateController.php:258-264`.
- Headers: require raw lowercase hex `X-TPZ-Signature = HMAC-SHA256(raw body, per-job secret)` (`includes/API/StringTranslateController.php:847-868`).
- Secret lookup: payload `job_id` selects transient `international_press_zone_bulk_callback_secret_<job_id>`; permission check deletes transient after successful verification, making callback one-shot (`includes/API/StringTranslateController.php:847-873`).
- Payload fields read: `job_id:string` for auth lookup; `results_by_lang: object<language,array<{id,success,translation}>>` for processing (`includes/API/StringTranslateController.php:847-856`, `includes/API/StringTranslateController.php:883-910`).
- Response: 200 JSON `{success:true,translated:integer,failed:integer}` after processing (`includes/API/StringTranslateController.php:914-918`). Permission rejection is handled by WordPress REST before handler.

## Uncertainties and code conflicts

- `/wp-json/international-press-zone/v1/callback` has two incompatible handlers, signature headers, secrets, payload schemas, and job-ID types. Runtime winner/order depends controller registration order not resolved in these files. New backend cannot satisfy both contracts with one payload unless plugin registration is consolidated (`includes/Translation/JobReceiver.php:83-93`, `includes/API/TranslateJobsController.php:167-176`).
- `/v1/jobs` has incompatible snake_case and camelCase request dialects plus multiple response job-ID dialects. Preserve all cited variants until plugin callers are unified (`includes/Translation/TranslationService.php:261-285`, `includes/Translation/JobSender.php:127-170`, `includes/API/TranslateController.php:820-868`).
- `/v1/translate` has incompatible flat and `{data:{...}}` success response consumers. Preserve both shapes or normalize plugin callers before migration (`includes/Translation/TranslationService.php:849-889`, `includes/Translation/MetaBox.php:736-757`).
- Primary `Settings` fixes base URL and encrypted key, while `Integration\TranslationAPI` and `API\SettingsController` use legacy mutable URL/plaintext-key options. No code evidence establishes that those option sets are synchronized (`includes/Translation/Settings.php:199-230`, `includes/Translation/Settings.php:366-374`, `includes/Integration/TranslationAPI.php:329-343`, `includes/API/SettingsController.php:378-396`).
- Licensing `GET /license/status` has no in-repository consumer; response field contract is therefore empty beyond valid JSON/success semantics (`includes/Licensing/LicenseClient.php:97-103`, `includes/Licensing/LicenseClient.php:183-213`).
- `UpdateChecker::verifyPackageSignature()` calls private `LicenseClient::request()` and therefore cannot reach undocumented `POST /v1/international/updates/verify` under PHP visibility rules. Excluded from summary/outbound contract because HTTP call is unreachable without code change (`includes/Licensing/UpdateChecker.php:265-293`, `includes/Licensing/LicenseClient.php:137`).
- Licensing retry recursion overwrites original request `$data` with decoded response before retrying a 5xx. A 5xx retry may therefore send the prior response body/query instead of original request. Backend must not depend on this bug; plugin should fix it (`includes/Licensing/LicenseClient.php:137-158`, `includes/Licensing/LicenseClient.php:180-200`).
- Site ping/deactivation discard every response, so no response-body/status contract can be inferred (`includes/Translation/SiteRegistrar.php:105-124`, `includes/Translation/SiteRegistrar.php:228-250`).
