# MyMagazine API v1

Base: the origin hosting this skill. JSON request/response bodies unless specified. Credentials use `Authorization: Bearer TOKEN`. Errors are `{ "error": { "code": "...", "message": "..." } }`, sometimes with validation details. Do not log bearer tokens or reader URLs in shared logs.

## Capabilities and creation

`GET /v1/capabilities`: limits, profiles, backend, schema, sample and skill URLs. `instructionsUrl` is the complete HTML guide at `/start`; `instructionsTextUrl` is its plain-text counterpart at `/start.txt`. `publishRestricted` is `false`: anyone can create an issue. The displayed profile is experimental until its qualification flag is enabled by the operator.

`POST /v1/issues`, with `{title, description?}`. No authorization header, invitation code, or account is required. The existing per-IP limit is 20 creation attempts per hour. Returns 201 `{id, ownerToken, readToken, issueUrl, manageUrl}`. New issues have sharing disabled; `issueUrl` is a reserved reading URL, not the user handoff. `manageUrl` is the clean private review page, requiring browser review access or existing owner access. Create a private review link below. Store immediately. Tokens are returned once. The service stores their hashes. Legacy upload browsers store owner credentials locally. The normal agent-to-user handoff uses a separate scoped, HttpOnly browser cookie; it never copies the owner token into the user’s browser.

## Authenticate again for an existing issue

Read `site`, `id`, and `ownerToken` from the saved private editing file. Send `GET /v1/issues/{id}` to that same site with `Authorization: Bearer OWNER_TOKEN`. This verifies access and returns the current revision. Use that revision ID and the same bearer token for a new upload. A fresh agent session needs no cookies or publishing code. The Python helper performs these steps each time it runs with the same `--state` file; a browser-exported editing file also works.

Owner tokens do not have a scheduled expiry. Expired preview URLs can be refreshed by fetching owner status; an expired upload requires a new upload declaration. An unauthorized request must not trigger creation of a replacement magazine. Reader tokens, retired invitation codes, and other issues' owner tokens grant no editing access. If the editing file is lost, recover it from a browser that still has owner access or a private backup; there is no account-based credential recovery yet.

## Private user review handoff

`POST /v1/issues/{id}/review-links`, owner bearer only, returns 201 `{reviewUrl,expiresAt}`. The URL is `/review/{id}#access=ONE_USE_TOKEN`, expires after 24 hours, and is for the user alone. This endpoint does not upload, approve, share, or order. Create a fresh link for a different browser or an expired/consumed handoff. Never grant access using a reading token. The Python helper creates a link after uploading; `--review-link --site SITE --state PRIVATE_FILE` creates one without an upload. `--status` remains strictly read-only.

The page POSTs `{issueId,token}` to `/v1/review-access/redeem`, requiring the same application Origin. The token is consumed atomically and establishes a host-only HttpOnly, SameSite=Strict cookie (Secure on HTTPS), scoped to this magazine for 30 days. Only token hashes are stored. The URL fragment is removed from browser history. Repeating redemption in the same session is safe; another browser cannot reuse it. Agents must not open or redeem links intended for their users.

Browser review cookies authorize GET `/v1/issues/{id}`, edition `/identity`, `/accept-warnings`, `/approve`, `/share-link`, `/sharing`, and `/checkout`. Cookie-authenticated writes require the same application Origin. They do not authorize uploads, source ZIP downloads, review retries, creating other review links, or any other magazine. Owner bearer access retains those authoring capabilities. Reading tokens authorize neither role. Exact-proof, warning, and purchase gates remain enforced for both authorized roles.

## Uploads and revisions

`GET /v1/issues/{id}`, owner bearer or browser review access: state, current revision, latest prepared edition of that revision, owner proof URLs, report, `printReadiness`, revision history, purchase availability, `access` (`owner`, `review`, or `reader`), and `sharingEnabled`. Fetch this when the user asks to look at issues; copied feedback is not required. `revisionId` on owner/review status is always the current upload revision, even while the page displays the previous proof. `revisionNumber` identifies the displayed proof, `latestRevisionNumber` identifies the current revision, and `pendingRevision` is null or `{revisionNumber,state,stage,updatedAt}`. It reports a queued/running/interrupted review or a newer edition blocked by incomplete or failed checks, without exposing its private findings to readers. While processing, the previous proof can remain visible with an explicit progress notice. Signed owner previews expire; fetch status again for fresh links. The helper's `--status --site SITE --state PRIVATE_FILE` reads without uploading; `--wait` also polls ongoing review.

`POST /v1/issues/{id}/uploads`, owner bearer:

```json
{
  "parentRevisionId": null,
  "manifest": {
    "schemaVersion": "1",
    "title": "Example",
    "language": "en",
    "entrypoints": { "print": "index.html" },
    "profileId": "prodigi-a4-portrait"
  },
  "files": [
    {
      "path": "index.html",
      "type": "text/html",
      "size": 1234,
      "sha256": "64-lowercase-hex-characters"
    }
  ]
}
```

`parentRevisionId` must match the current owner response (null initially). The file list excludes `magazine.json`, which is represented by `manifest`. Returns 201 `{uploadId, files:[{path,url,method}], finalizeUrl}`. Uploads expire after 60 minutes. A 409 revision conflict means another upload won; fetch owner status before intentionally creating another revision. Creating a new upload is not an idempotent operation.

`manifest.reader` is optional. Omitted means the standard PDF-based reader, including on existing editions. `{mode:"standard",layout?:"single"|"spread",navigation?:"paged"|"scroll",background?:"#RRGGBB",contents?:[{title,page}]}` customizes it. Defaults are spreads, paged navigation, and a neutral background. Contents have at most 100 entries, titles up to 120 characters, and integer physical page numbers 1–100; entries beyond the actual PDF are omitted from the display. The print entrypoint is required; the web entrypoint is optional. `{mode:"custom"}` explicitly uses the web entrypoint and requires it. All entrypoints still undergo static-package validation. Browser scripts/forms and revision-editing controls remain unsupported. See the skill's reading-experience guidance before choosing custom mode.

Owner, private-review, and reading responses include resolved `reader` settings from the displayed edition. `proofUrl` is the authoritative document used by the standard reader; `pages` are lower-resolution preview images. `contentUrl` is nullable when no web entrypoint was supplied. A custom view uses `contentUrl`. A pending revision does not change the previous edition's reader settings. Capabilities expose supported reader modes/options.

`PUT /v1/uploads/{uploadId}/files/{path}`, owner bearer, raw bytes and declared Content-Type. Repeating the same bytes while the upload is open is safe. Declared path, hash, and size are enforced. No new files may be added after declaration.

`POST /v1/uploads/{uploadId}/finalize`, owner bearer. Checks the entire package, seals immutable source, and queues review. Safe to retry. Returns 202 on first finalization and 200 with the existing state on a repeated call. A 422 includes actionable package diagnostics. Change the package and create a new upload after a validation error; the old declared inventory cannot be edited.

`POST /v1/issues/{id}/retry`, owner bearer, retries a failed job up to the service's attempt limit. It does not remove review findings; content corrections require a new revision.

`GET /v1/issues/{id}/source/{revisionId}`, owner bearer, returns the original ZIP.

## File URL imports

`POST /v1/issues/{id}/imports` is an alternative to declaring an inventory and uploading each file. It downloads a **source ZIP** immediately, validates it, stores immutable source, and queues the same rendering and print review. It does not approve, share, purchase, or place a print order. Discover current settings in `GET /v1/capabilities.fileUrlImport`; `uploadMethods` includes `file-url`.

For a new magazine, prepare the source ZIP and its download URL first, call `POST /v1/issues`, and securely save the returned issue ID and owner token before importing. For revisions, reuse that issue and its saved owner token. Fetch owner status for the current `revisionId` and preserve it as `parentRevisionId` for the entire operation. Initial imports use null. Browser review cookies and reading links cannot authorize imports.

Send `Authorization: Bearer OWNER_TOKEN`, `Content-Type: application/json`, and `Idempotency-Key: A-NEW-RANDOM-UUID`. Save that key alongside the issue's editing state **before** sending the request:

```json
{
  "url": "https://files.oaiusercontent.com/your-file?temporary-download-parameters",
  "parentRevisionId": null
}
```

The URL must be a directly downloadable HTTPS file on an exact host in `allowedHosts`, with no login, URL credentials, fragment, or custom port. OpenAI's `files.oaiusercontent.com` and the site's public hostname are enabled by default; additional storage hosts require operator configuration. Redirects are checked at every step, with at most three redirects. The service never forwards your owner bearer or cookies to the file host. It does not retain the download URL. Treat signed URLs as private temporary credentials; do not publish them in the magazine or logs.

Downloads have a 25-second deadline and a 32 MiB compressed limit. ZIPs must contain `magazine.json` and the HTML/CSS/local assets at the root or inside one common folder. Normal stored/DEFLATE ZIPs are supported, including streaming ZIPs. ZIP64, split/encrypted archives, symlinks, path traversal, duplicate paths, and unsafe expansion are rejected. Existing file limits apply, and the total uncompressed ZIP (including the manifest and metadata) must fit 32 MiB. A PDF alone is not accepted. Server package validation returns the same actionable `PACKAGE_INVALID` diagnostics as ordinary uploads.

First success returns **202** `{issueId,revisionId,state,replayed:false,statusUrl,reviewLinksUrl}` after the source has been stored and review queued. Poll `statusUrl` with the owner bearer to inspect progress and findings. POST to `reviewLinksUrl` with that bearer to get the user's private `reviewUrl`; deliver it without redeeming it. An import response means review was queued, not that the magazine passed review or is ready to print.

Retries are scoped to the issue and require its owner bearer:

- Preserve the original key and parent after a lost response, download failure, or timeout. A committed import returns **200** with the same revision and `replayed:true`, even if the download URL has expired or later revisions exist. It never re-downloads or creates another revision. The reported `state` is that imported revision's current state.
- `409 IMPORT_IN_PROGRESS` means an attempt is running; respect `Retry-After` (five seconds) and retry the same operation. An interrupted attempt becomes reclaimable after two minutes. `IMPORT_LEASE_EXPIRED` also permits retrying the same operation.
- `IMPORT_DOWNLOAD_FAILED` or `IMPORT_DOWNLOAD_TIMEOUT` permits obtaining a fresh URL and retrying with the **same key and parent**. URL changes are allowed because temporary links expire. Until an import succeeds, corrected packages may also use that key. Once it succeeds, a new file requires a new key and the current parent.
- `409 REVISION_CONFLICT` means a newer upload won. Fetch owner status and decide whether your changes still belong on top of it; use a new key and current parent only when intentionally creating another revision. Never silently replace newer work. `IMPORT_KEY_REUSED` means the key was paired with a different parent; recover using the original request or choose a new key for a deliberate new revision.
- `IMPORT_URL_DENIED` means the URL host or shape is unsupported. Use an approved host or the ordinary manifest/PUT upload. Do not try alternate encodings to bypass this boundary. `IMPORT_TOO_LARGE` / `IMPORT_ZIP_INVALID` require fixing the file. Imports allow 40 requests per IP per hour, including retries; back off on 429.

For a configured ChatGPT GPT Action, its `openaiFileIdRefs` runtime objects contain `download_link`; a tool adapter can pass that value as `url`. ChatGPT plugin file inputs use `download_url`. Obtain the link from the actual file tool; do not invent it from a file ID or use a conversation's `sandbox:` download link. OpenAI documents GPT Action links as valid for five minutes, so submit promptly. The generic endpoint does not itself install an Action/plugin or make POST available in an ordinary web-reading chat. See [OpenAI's file-transfer documentation](https://developers.openai.com/api/docs/actions/sending-files).

## Review, release, and print approval

Build states: `awaiting_upload`, `queued`, `reviewing`, `ready`, `needs_review`, `needs_changes`, `failed`. Build `ready` means a prepared reading edition. `printReadiness` is the current print decision: `{status,label,message,errors,warnings,acceptedWarnings,canApprove}`. `warnings` counts unresolved warnings. Status is `blocked` (red: errors or incomplete/failed review), `attention` (yellow: warnings need correction or acceptance), `ready` (green: eligible for exact-proof approval), or `reviewing`. `report.printReady` remains the technical-check result; it does not account for warning acceptance or proof approval. `purchase.available` additionally checks proof approval and provider configuration.

Reports include deterministic and AI diagnostics, page coverage, page count, renderer, and duration. Errors prevent print readiness. Missing/incomplete AI review prevents release. Review is strictly read-only: every uploaded file must match its sealed hash. The legacy `changes` field is empty for new reviews; older reports may retain historical changes. The user and their agent make corrections and upload a new revision.

`GET /v1/issues/{id}/editions/{editionId}/identity`, owner bearer or browser review access, returns `{editionId,pdfHash,approvedAt}`.

Each owner diagnostic has an edition-scoped `id` such as `finding-1`, plus `severity`, `source`, `message`, and optional `page`, `path`, `element`, and `suggestion`. Accepted warnings also have `acceptedAt` and `acceptedBy` (`user` or `agent`, the declaring client rather than a separately authenticated identity). Reader metadata exposes aggregate readiness and deterministic findings but omits private AI findings; use the owner endpoint for the full report.

`POST /v1/issues/{id}/editions/{editionId}/accept-warnings`, owner bearer or browser review access:

```json
{
  "pdfHash": "64-lowercase-hex-characters",
  "diagnosticIds": ["finding-1", "finding-3"],
  "acceptedBy": "agent"
}
```

Fetch the hash from `/identity` and warning IDs from the latest owner report. Acceptance is additive and idempotent; retries preserve the first acceptance timestamp. Returns the refreshed owner view. Invalid/error IDs reject the entire request with 422 `WARNING_REQUIRED`; wrong hashes or superseded editions return 409. Acceptance leaves the report immutable, does not waive errors or incomplete review, does not approve the proof, and never transfers to another edition. Agents may accept when the user chooses that outcome or delegates judgment. “Fix the issues” alone does not authorize waiving them.

`POST /v1/issues/{id}/editions/{editionId}/approve`, owner bearer or browser review access, `{pdfHash}`. Only for the current edition with `printReadiness.canApprove:true`, after the user's proof approval. Unresolved warnings return 409 `WARNINGS_UNRESOLVED`. Records exact-proof approval for printing only. It does not change the reading edition or enable sharing.

`POST /v1/issues/{id}/share-link`, owner bearer or browser review access: enables sharing and returns `{issueUrl}` for the currently released edition. Available once a reading edition exists. This creates a read-only alias without invalidating existing reading links.

`POST /v1/issues/{id}/sharing`, owner bearer or browser review access, `{enabled:false}` revokes reader access, including previously released content URLs. `{enabled:true}` restores the same link. Downloads already obtained cannot be revoked. The owner's short-lived preview remains accessible until expiry.

## Reading and owner purchasing

`GET /v1/read/{readToken}`: latest passing reading edition metadata, rendered HTML URL, PDF URL, page-image URLs, purchase availability. The same URL automatically advances after each complete passing review; print approval and warning acceptance are separate. While newer work is processing or blocked, it keeps the previous passing edition with `pendingRevision`. The page polls and updates in place. Late completion of an older revision cannot move the reader backward. `readingRevisionPolicy` in capabilities is `latest-passing-review`. A link holder has no editing authority. Unlisted does not mean authenticated/private; no directory or public search index is provided.

The purchase button stays visible on reading and owner pages, alongside readiness. The buy button opens checkout directly. Operator-issued payment codes authorize a limited order without private review access, as described below. Ordinary card checkout still requires private review access. Seeing the buy button grants no purchasing authority. `POST /v1/issues/{id}/checkout`, with that issue’s owner bearer token or browser review cookie, accepts `{editionId,copies,recipient}`. Recipient: `{name,email,line1,line2,townOrCity,stateOrCounty,postalOrZipCode,countryCode}`. The active delivery countries and 20–50-page retail limit are in `GET /v1/capabilities.purchasePolicy`; US delivery is enabled initially. `shippingMethod` and client prices are rejected. The retail price is $20 per copy including shipping. Reading access never grants purchasing authority. Anonymous callers, reader tokens, and another issue’s owner token cannot start checkout. The former `POST /v1/read/{readToken}/checkout` route returns 403. Never send a client price or asset URL. The service quotes the selected approved edition and redirects to Stripe. Checkout remains disabled until provider keys, matching environments, and qualified print settings are present.

Payment is recognized only by a signed Stripe webhook, not by visiting the success page. The service binds fulfillment to the stored PDF hash and a permanent Prodigi idempotency key. The order lookup link contains a separate capability and displays status without delivery details.

## Common errors

- 401: missing or incorrect owner/publishing credential.
- 403: origin restriction, revoked link, or expired signed preview.
- 404: unavailable issue, edition, or file. Do not try to enumerate identifiers.
- 409: stale revision, closed upload, expired review lease, or unapproved edition.
- 413 / 422: resource or package validation failure; inspect diagnostics.
- 429: rate limit; wait rather than creating alternate identities.
- 503: provider or publishing configuration incomplete. Do not claim the upload was printed or paid.

## Payment-code checkout

Payment codes are supplied by the site operator and stored hashed. They are scoped to an issue and a printer environment (`live` or `sandbox`), with expiry, revocation, quantity/estimated-cost limits, and an atomic use counter. They grant no source, editing, private browser session, or global review authority. Reading access alone still cannot submit an order. `purchase.paymentCodeAvailable` advertises the option. Customer card checkout may remain disabled while controlled sample-code orders are enabled.

Both endpoints below require the application Origin. Never put a payment code in a URL or published content.

- `POST /v1/issues/{id}/code-checkout/quote`: `{paymentCode,editionId,copies,recipient}`. The recipient has the same schema as ordinary checkout. Returns `{token,expiresAt,environment,editionId,pdfHash,revisionNumber,proofUrl,warnings,price,copies}`. The quote expires after 15 minutes. It checks the newest revision, complete passing review, provider configuration, code scope and limits, but does not consume the code or create an order. Unresolved warnings are returned for explicit acceptance. `price` contains only retail terms: `{unitAmountCents:2000,amountCents,discountCents,dueCents:0,currency:"USD",shippingIncluded:true}`. A code covers the retail total. The provider quote, including any quoted tax, stays internal. Page, destination, and cost limits apply even with a code, and unrelated provider issues still block.
- `POST /v1/code-checkout/{token}/submit`: `{pdfHash,approveProof:true,confirmOrder:true,acceptedWarningIds:[...]}`. Confirms the quoted PDF and all returned warning IDs for this order. Returns 202 `{orderUrl}`. A new revision, expired quote, changed or consumed code, unaccepted warning, or a fresh quote exceeding the cost ceiling blocks submission. Drafts from before the $20 pricing policy return `CHECKOUT_UPDATED` without consuming the code; get a new quote. Finalization atomically consumes the code and creates one `authorized` order, never a fake Stripe payment. Repeating the same quote token returns that same order even after code consumption or quote expiry. Competing quotes cannot exceed the use limit.

`GET /v1/orders/{token}` returns `{reference,status,copies,createdAt,paymentMethod,paymentCollected,environment,price,requiresAttention,dispatchEstimate,shipments}`. `reference` is the stable MyMagazine `MM-…` customer reference. `status` is `processing`, `received`, `printing`, `shipped`, `partially_shipped`, `cancelled`, or `needs_attention`. No provider reference, internal progress fields, recipient details, or wholesale costs are returned. `price` is null for historical orders without a retail snapshot. A `dispatchEstimate` contains a 4–6 business-day range with `basis:"usual_production_time"`; the public Prodigi API does not expose an order-specific estimated dispatch field. Shipments expose the actual `dispatchedAt` and an HTTPS `trackingUrl` when available. Do not interpret “shipped” as “delivered.”

Code authorization never reports payment received. The printer environment and approved PDF are frozen in the order; later uploads or configuration changes cannot redirect a retry. Production retries preserve the provider idempotency key and stop for operator attention after the attempt limit. Fresh retail orders are checked against the current internal fulfillment budget before each provider submission; a budget hold never creates a replacement order.
