{
  "api_version": 2,
  "authentication": {
    "credentials": "Create and revoke credentials from your signed-in browser account settings. Secrets are shown once.",
    "header": "Authorization: Bearer <sgg2_ secret>",
    "scope_effect": "read permits ordinary GET/HEAD, including your private publishing drafts, media library, owned-piece summaries and authorized media playback/download. read_write additionally permits the listed POST/PUT/DELETE operations and upload-ticket GET/HEAD, because an upload ticket grants write access. Read credentials are refused on these operations with 401 invalid_token before last-use, parsing, rate or product writes. Unsupported methods retain ordinary authentication-before-405.",
    "scopes": [
      "read",
      "read_write"
    ],
    "sessions": "Browser cookies do not authenticate these routes; an invalid presented token never becomes anonymous."
  },
  "behavior": {
    "answers": "Read /posts/<post_id>/reuse/ for current source choices. Create an ordinary private draft with response_to_swis_id, your final-work center and any explicitly selected node_id/source_post_id placements. Remap slot UUIDs and selected connections yourself; nothing is copied or published automatically. Publication rechecks access and preserves source authorship. A centerless Rally is a complete post whose available ingredients must permit reuse.",
    "caching": "Private/no-store, nosniff. Ordinary HEAD returns no body with GET length metadata. Media grant/local-file HEAD is an empty authorization probe with length zero, not the size/readiness of the GET representation; it performs no signing, derivative preparation or file open. File GET preserves streaming and its own Content-Length.",
    "categories": "Choices list active categories only; existing posts and discovery filters retain retired category IDs.",
    "create_retries": "Persist a client_create_id before sending POST. Retry an uncertain create with that same UUID and the exact original payload; object-key order is immaterial, omitted defaults/source text/array order are not. A matching retry returns the current saved draft, including later edits; changed original content returns 409 create_conflict. This identity lasts while the draft exists; deletion removes it.",
    "deferred": "Comments, credential delegation and account management are outside this replacement API. Production media activation, actual recorder/phone compatibility and live-provider/ASGI/deployment acceptance remain open. Coordinated cutover remains separate from implementation.",
    "discovery": "Same web discovery: current published composition projections only. Private compositions, notes and all retired standalone formats are excluded, including on the owner's profile.",
    "media": "Post/node projections, discovery, previews, publication responses, reuse lookup and exports emit API media routes for native grant_url and download.request_url. These representations contact no provider; explicitly requesting media grants/refresh/finalization can. Canonical post/node URLs, creator source and draft payloads keep their original browser/link meaning. Resolve relative request URLs against the deployment origin.",
    "media_allowances": "GET /api/v2/media/usage/ returns the owner's current finite limits, charged usage and per-kind reservation_status. Pending uploads reserve the full per-file allowance until verified, even for a smaller declared file. Staff media capacity is finite. Exact retries retain their original asset limits; current plan and configuration govern fresh reservations. A downgrade never deletes existing work. Configured allowances do not enable paused uploads or disabled providers.",
    "media_rates": "API and browser share the existing account buckets per 60 seconds: reserve60, refresh12, finalize12, local_upload12 and grant120 (including grant HEAD). Cache failure refuses with 503 media_rate_unavailable. Multiple credentials do not multiply budgets; 429 media_rate_limited has Retry-After:60. Ticket/local-file reads have no extra media bucket; protected GET/HEAD still use the existing account daily read meter, including write-scoped ticket reads.",
    "media_transactions": "Provider and local byte operations run outside the request transaction. Existing short service transactions commit reservation/claims before external work and recheck state before adoption/capability issuance. Withdrawal, hidden content and inactive owners remain authoritative; no new retry worker or durable receipt is added.",
    "other_retries": "Save/preview/delete/unpublish require the current version. Save/unpublish advance it; they have no saved-response replay. After an uncertain result, read the owned draft/list before choosing another action. Deleting a bound editing draft is refused. Preview persists no node or post and contacts no provider.",
    "owner_management": "Lists and single summaries include only the active token owner's objects, including unavailable/withdrawn items; no staff bypass. Node/media lists order by descending creation time and UUID, use an exclusive owned UUID cursor, and finish at next_before:null. Missing or foreign cursors are refused without exposing the other owner's metadata.",
    "pagination": "Nonblank search automatically passes nonmatching candidates and returns at most limit matching results in newest-first order. A search next_before is anchored to the last returned match and is present only when another authorized match was found; continue until null. Without search, including normalized blank q, discovery scans at most limit candidates: an empty items list with a cursor is not the end, and the final full page may require one empty request. Category pages finish at next_after:null. All discovery cursors remain bound to the account and normalized filters, not an individual credential.",
    "piece_permissions": "Setting allow_reuse:false denies new cross-owner attachments without removing existing references; allow_download independently controls new node-based download grants. These changes do not rewrite media assets, sibling nodes or post visibility, or restore withdrawn content. Existing provider-signed links may remain usable until expiry; local file links recheck current authorization. Owner asset-based grants do not use node permission flags.",
    "publication_retries": "Save edits first, then publish that returned version. A current still-published draft with version==published_version accepts published_version-1 or published_version as an exact completed retry, without inserting nodes/posts or advancing version. It returns current authorized projection, never a stored response. Fresh publish/update/republish advances once; updates keep the same post ID. Later saves produce stale_version and intervening unpublish produces post_unpublished for old retries; read the current owned draft and reconcile explicitly.",
    "published_draft": "Publication replaces new content proposals with persisted node_id references in the returned draft. Continue editing that returned payload/version. Nodes are immutable: to change content, submit a new content proposal, optionally naming an owned supersedes node; keeping the old node_id preserves its existing content and authorship.",
    "quota": "When READ_QUOTA_ENABLED is enabled, each protected GET/HEAD consumes one existing account daily read unit, shared across credentials. The fixed public catalog and POST/PUT/DELETE do not consume read units. Draft stock and first-publication post quota use the same account policy as the browser. Save, preview, updates and republishing do not consume another post slot.",
    "search": "Literal matching uses only current authorized title, caption, tags, category label/slug, rendered text and source titles. It does not search private drafts, hidden or withdrawn source, raw Markdown, provider metadata or remote bodies. Each request processes at most 1000 candidates in batches of 50 with a five-second cooperative work budget; this does not cancel an in-flight database query or renderer. A successful response proves a full result page plus another match, or eligible exhaustion. If that cannot be established within the budget, 503 search_capacity_exceeded returns no items or cursor. Narrow the filters or try again; retrying the same over-budget query does not increase capacity.",
    "text_words": "Count nonempty source runs separated only by Unicode code points 0009–000D, 0020, 0085, 00A0, 1680, 2000–200A, 2028, 2029, 202F, 205F, 3000. Normalize CRLF/CR to LF for counting only; never change source. FEFF, 200B and 001C–001F are not separators. Markdown, math, code, URLs and entities count as written, without rendering, decoding or fetching. The 500-word per-piece limit and 50000-code-point/1-MiB/embed limits are independent. An over-limit preview or publication returns 413 text_word_limit with actual, limit:500 and slot_id; the saved draft and existing publication remain intact. Edit the source and save before retrying; no truncation or automatic split.",
    "unicode": "JSON is UTF-8 logical Unicode. Source text is not HTML-unescaped; the html field is explicitly rendered HTML.",
    "upload_retries": "Persist client_request_id and the original reservation payload. Exact retries share the account's existing reservation, return 200 and never reprovision/reset it; changed input returns 409 media_request_conflict. A provider failure may leave a committed initiating reservation even when POST returns 503. Retry the same UUID to inspect it, then refresh video status or withdraw; never allocate a new identity just to repeat an uncertain upload. Null ticket/initiating state does not mean a provider upload was created. A confirmed-unsent or disabled-provider reservation is not reprovisioned by retry.",
    "visibility": "Every read checks current access. Discovery excludes muted authors; direct reads retain the existing direct-link visibility rules. An owner may read an unpublished bound composition snapshot, but exports require publication. Public composition/node reads and exports never include the later private working draft; owner-only draft routes explicitly return that working copy.",
    "withdrawal": "Node withdrawal makes that node unavailable in every placement; it does not withdraw its shared media or sibling nodes. Media withdrawal affects every native node using that asset; affected_nodes counts nodes, not placements. Existing published post rows and private working drafts remain. Provisioned assets enter deleting and retain charged capacity until existing cleanup confirms removal; only proven never-provisioned initiating assets become deleted and release reservations immediately. No provider call or new cleanup dispatch occurs here. For assets not already deleting or deleted, pending_only:true refuses any asset outside initiating, uploading or processing (including failed), or one that already backs a node, with 409 media_cancel_unavailable; already deleting/deleted withdrawals stay idempotent. These operations have no restore endpoint or saved-response receipt; read current summaries after an uncertain result.",
    "write_requests": "Send UTF-8 application/json, at most 1 MiB, with exact fields and no query parameters, except grants accept their listed selector/download query and local image/audio PUT accepts raw bounded bytes. Duplicate fields, non-finite draft JSON and unknown schema fields are rejected. UUIDs are strings; numeric identifiers are positive integers at most 9007199254740991. No cookie or CSRF token substitutes for the Bearer credential; browser management remains session/CSRF protected."
  },
  "endpoints": [
    {
      "authentication": "optional",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/docs/",
      "query": {},
      "response": "{status, api_version, authentication, methods, endpoints, schemas, behavior, errors, examples}"
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/posts/",
      "query": {
        "answer_to": "Current accessible composition parent ID.",
        "author": "Exact username; at most 150 characters.",
        "before": "Opaque next_before from the same account and normalized filters.",
        "category": "Repeat 1–3 distinct positive category IDs; OR within categories.",
        "feed": "latest (default), following, rallies",
        "limit": "1–20 matching results for nonblank search, otherwise candidates; default 20.",
        "q": "Optional literal search across the eligible catalog, NFKC/casefold/whitespace-normalized; at most 200 raw and normalized characters. Blank search uses ordinary discovery pagination.",
        "tag": "Repeat up to 12 distinct normalized tags; AND across tags."
      },
      "response": "{status, items:[{kind,post,edit_url?,matched_fields?}], next_before}"
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/posts/<post_id>/",
      "query": {},
      "response": "{status, post:composition}"
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/nodes/<node_uuid>/",
      "query": {},
      "response": "{status, node:node}"
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/categories/",
      "query": {
        "after": "Positive decimal next_after; ascending category ID.",
        "limit": "1–100, default 100."
      },
      "response": "{status, items:[{id,slug,label}], next_after}"
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/posts/<post_id>/export/",
      "query": {
        "format": "bundle (default), manifest, brief"
      },
      "response": "bundle:{status,manifest,brief}; manifest:JSON attachment; brief:UTF-8 Markdown attachment"
    },
    {
      "authentication": "required",
      "body": {
        "POST": {
          "client_create_id": "Client-generated UUID for this create operation",
          "payload": "draft_payload"
        }
      },
      "methods": [
        "GET",
        "HEAD",
        "POST"
      ],
      "path": "/api/v2/drafts/",
      "query": {
        "GET/HEAD": {
          "before": "Owned draft UUID from next_cursor; exclusive.",
          "limit": "1–100, default 50"
        },
        "POST": {}
      },
      "response": "GET/HEAD:{status,drafts:[{id,title,version,published_swis_id,updated_at}],next_cursor,stock:{used,limit,remaining}}; POST:{status,draft}. Success is 200, including matching create retries."
    },
    {
      "authentication": "required",
      "body": {
        "DELETE": {
          "expected_version": "Current positive integer draft version"
        },
        "PUT": {
          "expected_version": "Current positive integer draft version",
          "payload": "Complete replacement draft_payload"
        }
      },
      "methods": [
        "GET",
        "HEAD",
        "PUT",
        "DELETE"
      ],
      "path": "/api/v2/drafts/<draft_uuid>/",
      "query": {},
      "response": "GET/HEAD/PUT:{status,draft}; DELETE:{status}. Success is 200. Bound drafts cannot be deleted; unpublish their post instead."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {
          "expected_version": "Current positive integer draft version"
        }
      },
      "methods": [
        "POST"
      ],
      "path": "/api/v2/drafts/<draft_uuid>/preview/",
      "query": {},
      "response": "{status,draft,post:private preview with id:null}; 200. Newly proposed originals have transient node IDs that cannot be reused as saved nodes; existing references retain persisted IDs."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {
          "expected_version": "Saved input version; see publication retries"
        }
      },
      "methods": [
        "POST"
      ],
      "path": "/api/v2/drafts/<draft_uuid>/publish/",
      "query": {},
      "response": "{status,draft,post:composition}; 200 for first publish, same-post update/republish, or an exact completed retry."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {
          "expected_version": "Current positive integer draft version"
        }
      },
      "methods": [
        "POST"
      ],
      "path": "/api/v2/drafts/<draft_uuid>/unpublish/",
      "query": {},
      "response": "{status,draft,post:unpublished owner snapshot}; 200. Advances version; repeating the old version returns 409 stale_version."
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/posts/<post_id>/reuse/",
      "query": {},
      "response": "{status,source:{post:composition,reusable_slot_ids:[UUID],can_answer:boolean}}. Advisory current choices; no copy, draft or permission grant is created."
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/owned-nodes/",
      "query": {
        "before": "Owned node UUID from next_before; exclusive.",
        "limit": "1–100, default 50"
      },
      "response": "{status,nodes:[owned_node],next_before}; 200."
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/owned-nodes/<node_uuid>/",
      "query": {},
      "response": "{status,node:owned_node}; 200. Current owner management summary, including unavailable pieces."
    },
    {
      "authentication": "required",
      "body": {
        "PUT": {
          "allow_download": "boolean",
          "allow_reuse": "boolean"
        }
      },
      "methods": [
        "PUT"
      ],
      "path": "/api/v2/owned-nodes/<node_uuid>/permissions/",
      "query": {},
      "response": "{status,id,allow_reuse,allow_download}; 200. Both values required; independent permissions on this node only."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {}
      },
      "methods": [
        "POST"
      ],
      "path": "/api/v2/owned-nodes/<node_uuid>/withdraw/",
      "query": {},
      "response": "{status,id,withdrawn:true,placements}; 200. Withdraw the piece from every placement; repeated withdrawal is accepted."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {
          "client_request_id": "Stable client-generated UUID",
          "payload": "media_reservation"
        }
      },
      "methods": [
        "GET",
        "HEAD",
        "POST"
      ],
      "path": "/api/v2/media/",
      "query": {
        "GET/HEAD": {
          "before": "Owned asset UUID from next_before; exclusive.",
          "limit": "1–100, default 50"
        },
        "POST": {}
      },
      "response": "GET/HEAD:{status,assets:[media_asset],next_before}; 200, all owned lifecycle states. POST:{status,asset:media_asset,upload:upload_ticket|null}; 201 new reservation, 200 matching retry. Only a new reservation attempts provisioning."
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/media/<asset_uuid>/",
      "query": {},
      "response": "{status,asset:media_asset}; 200. Stored owner metadata only; no provider refresh."
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/media/usage/",
      "query": {},
      "response": "{status,usage:media_usage}; 200. Current account usage and advisory reservation status, even when uploads are unavailable."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {
          "pending_only": "Optional boolean, default false"
        }
      },
      "methods": [
        "POST"
      ],
      "path": "/api/v2/media/<asset_uuid>/withdraw/",
      "query": {},
      "response": "{status,asset:media_asset,affected_nodes}; 200. Withdraw the shared asset from all native nodes using it; repeated withdrawal is accepted."
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/media/<asset_uuid>/upload-ticket/",
      "query": {},
      "response": "{status,upload:upload_ticket|null}; 200. Returns stored current owner upload capability without reprovisioning. GET/HEAD requires read_write because a ticket permits writing bytes.",
      "scope": "read_write"
    },
    {
      "authentication": "required",
      "body": {
        "POST": {}
      },
      "methods": [
        "POST"
      ],
      "path": "/api/v2/media/<asset_uuid>/refresh/",
      "query": {},
      "response": "{status,asset:media_asset}; 200. Reconcile video status/uncertain provisioning through the existing provider lookup. No new upload is created; images and audio return their stored status."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {}
      },
      "methods": [
        "POST"
      ],
      "path": "/api/v2/media/<asset_uuid>/finalize/",
      "query": {},
      "response": "{status,asset:media_asset}; 200. Validate uploaded image/audio and adopt verified canonical files. Images are re-encoded; audio removes metadata/artwork while preserving supported encoded audio or PCM samples. An already-ready asset returns its stored snapshot."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {
          "download": "true only"
        }
      },
      "methods": [
        "GET",
        "HEAD",
        "POST"
      ],
      "path": "/api/v2/media/<asset_uuid>/grant/",
      "query": {
        "download": "0 (default) or 1",
        "draft_id": "Optional owned draft UUID for owner preview."
      },
      "response": "GET/POST:media_grant; 200. Owner asset access. POST explicitly prepares a video download if absent. HEAD only checks authorization, returns an empty 200 probe and never signs or contacts the provider."
    },
    {
      "authentication": "required",
      "body": {
        "POST": {
          "download": "true only"
        }
      },
      "methods": [
        "GET",
        "HEAD",
        "POST"
      ],
      "path": "/api/v2/nodes/<node_uuid>/media/",
      "query": {
        "download": "0 (default) or 1",
        "post_id": "Optional positive decimal published containing post ID, at most 9007199254740991; no leading zero."
      },
      "response": "GET/POST:media_grant; 200. Current node and containing publication permissions. HEAD is authorization-only as for the owner grant."
    },
    {
      "authentication": "required",
      "body": {
        "PUT": "Raw image/audio bytes, not JSON or multipart. Use the returned local ticket and Content-Type application/octet-stream; bounded by the asset's reserved max_input_bytes."
      },
      "methods": [
        "PUT"
      ],
      "path": "/api/v2/media/<asset_uuid>/upload/",
      "query": {},
      "response": "{status:'success'}; 200. Local development storage only; bytes remain private/unverified until explicit finalize. Provider uploads go directly to the returned provider ticket instead."
    },
    {
      "authentication": "required",
      "methods": [
        "GET",
        "HEAD"
      ],
      "path": "/api/v2/media/files/<signed_token>/",
      "query": {},
      "response": "GET: authorized local image/audio bytes with media MIME, length and attachment disposition when downloaded. Audio supports one byte range with 206, or 416 for an invalid/unsatisfiable range. HEAD: empty authorization-only probe; no file is opened. Returned signed URL still requires a current Bearer credential for the same account."
    }
  ],
  "errors": {
    "400": [
      "invalid_query",
      "invalid_discovery",
      "invalid_cursor",
      "invalid_export",
      "invalid_draft",
      "invalid_media",
      "rights_confirmation_required",
      "invalid_composition",
      "invalid_answer",
      "reuse_required"
    ],
    "401": [
      "token_required",
      "invalid_token",
      "authentication_required"
    ],
    "403": [
      "active_account_required",
      "account_inactive"
    ],
    "404": [
      "publishing_unavailable",
      "post_not_found",
      "source_unavailable",
      "node_not_found",
      "media_not_found",
      "media_storage_read_failed",
      "draft_not_found"
    ],
    "405": [
      "method_not_allowed"
    ],
    "409": [
      "create_conflict",
      "stale_version",
      "post_unpublished",
      "invalid_binding",
      "immutable_response",
      "draft_stock_exceeded",
      "draft_is_bound",
      "surrounding_limit",
      "source_unavailable",
      "media_not_ready",
      "media_cancel_unavailable",
      "media_kind_unavailable",
      "media_request_conflict",
      "unfinished_upload_limit",
      "media_capacity_exceeded",
      "media_upload_expired",
      "media_upload_unavailable",
      "media_image_invalid",
      "media_audio_invalid",
      "media_storage_read_failed"
    ],
    "413": [
      "draft_too_large",
      "text_word_limit",
      "embed_limit",
      "media_too_large",
      "media_output_too_large",
      "media_image_dimensions",
      "media_audio_duration"
    ],
    "415": [
      "invalid_draft"
    ],
    "429": [
      "read_quota_exceeded",
      "post_quota_exceeded",
      "media_rate_limited"
    ],
    "503": [
      "search_capacity_exceeded",
      "media_rate_unavailable",
      "media_uploads_paused",
      "media_policy_unconfigured",
      "media_provider_unavailable",
      "media_namespace_unconfigured",
      "media_provider_not_sent",
      "media_provider_pending",
      "media_refresh_failed",
      "media_scope_changed",
      "media_identity_conflict",
      "media_status_invalid",
      "media_measurements_invalid",
      "media_delivery_unavailable",
      "media_audio_unavailable",
      "media_storage_unavailable",
      "media_storage_sign_failed",
      "media_storage_read_failed",
      "media_processing_busy",
      "media_temp_capacity",
      "media_storage_timeout",
      "media_processing_limits_invalid",
      "media_storage_io_failed",
      "media_storage_delete_failed",
      "media_transaction_boundary"
    ],
    "envelope": "{status:'error',error:<code>,message:<logical Unicode text>}",
    "notes": {
      "authentication_required": "Race-only 401: the owning account was deleted after successful Bearer resolution but before a product service refreshed it. Missing or invalid Bearer credentials normally return token_required or invalid_token.",
      "media_audio_duration": "413 when decoded audio exceeds the reservation's frozen duration limit. media_audio_unavailable is 503 when the configured decoder/probe is unavailable or cannot start.",
      "media_audio_invalid": "409 when actual decoding rejects a container, codec, stream, truncation, supported channel/rate/depth bounds or canonical verification. Filenames and caller MIME are not validation. Decoder diagnostics and local paths are never returned.",
      "media_operations": "Listed codes are common operational refusals, not an exhaustive list of internal storage validation errors. Malformed stored media coordinates may also yield 409 media_storage_* errors. Most provisioning failures after the claim become media_provider_pending; confirmed-unsent Stream failure can return media_provider_not_sent. Refresh may instead return 200 with failed/uncertain asset state and last_error_code. A 200 response alone does not mean media is ready. Expired/unavailable tickets return 200 upload:null.",
      "media_storage_read_failed": "404 when a local media-file read cannot open the stored derivative file; 409 when a bounded input stream returns non-byte data; 503 when reading the R2 incoming object fails during image/audio finalization.",
      "owner_ids": "Missing or foreign node summaries/cursors return 404 node_not_found; node permission/withdrawal services return 409 source_unavailable. Missing or foreign media IDs/cursors return 404 media_not_found. Malformed query cursor UUIDs return 400 invalid_draft; malformed UUID path segments do not match a route and receive the site's ordinary 404, outside the API envelope.",
      "post_path_ids": "After authentication, /posts/<post_id>/reuse/ returns 400 invalid_draft for zero or IDs above 9007199254740991; /posts/<post_id>/ returns 404 post_not_found for those values. Both integer path converters normalize leading zeros before validation.",
      "search_capacity_exceeded": "503 when search cannot establish the requested matching page and continuation or eligible exhaustion within its work limit. No partial results, cursor or no-match claim is returned. Protected GET/HEAD retains the existing account read charge and quota headers; HEAD has no body. Narrow the filters or try again."
    }
  },
  "examples": {
    "answer_placements": {
      "center": "Your original final-work draft_slot",
      "note": "Use currently permitted node IDs from the source lookup and fresh destination slot IDs.",
      "response_to_swis_id": 123,
      "surrounding": [
        {
          "node_id": "00000000-0000-4000-8000-000000000014",
          "slot_id": "00000000-0000-4000-8000-000000000013",
          "source_post_id": 123
        }
      ]
    },
    "audio_upload": {
      "steps": [
        "Choose an existing MP3, AAC/M4A or supported uncompressed PCM WAV recording. Reserve kind:audio with its actual size_bytes, rights_confirmed:true and a stable UUID.",
        "PUT raw file bytes to upload.url with returned headers; add read_write Bearer only for the same-origin local API URL.",
        "POST {} to /api/v2/media/<asset_uuid>/finalize/; only a ready verified asset may be previewed or published. Keep the same reservation identity when recovering an interrupted upload.",
        "Request a fresh grant on playback or explicit reload; published_audio downloads contain the canonical recording without source metadata/artwork."
      ]
    },
    "cancel_pending_upload": {
      "body": {
        "pending_only": true
      },
      "method": "POST",
      "note": "Cancel only an initiating, uploading or processing asset that does not back a node. Other states, including failed, are refused unless already deleting/deleted, which remain idempotent. Omit pending_only only when intending global asset withdrawal.",
      "path": "/api/v2/media/<owned-asset-uuid>/withdraw/",
      "scope": "read_write"
    },
    "centerless_rally": {
      "center": null,
      "is_rally": true,
      "surrounding": "One or more ordered slots"
    },
    "conflict_recovery": {
      "error": "stale_version",
      "next": "GET /api/v2/drafts/<draft_uuid>/; retain your intended edits, inspect current state and explicitly save against its version.",
      "status": 409
    },
    "create_research_draft": {
      "body": {
        "client_create_id": "00000000-0000-4000-8000-000000000010",
        "payload": {
          "category_ids": [
            1,
            2
          ],
          "center": {
            "content": {
              "body": "A note on the argument: $a=bq+r$.",
              "kind": "text"
            },
            "slot_id": "00000000-0000-4000-8000-000000000011"
          },
          "connections": [
            {
              "from_slot": "00000000-0000-4000-8000-000000000012",
              "label": "Motivated the example",
              "to_slot": "00000000-0000-4000-8000-000000000011"
            }
          ],
          "surrounding": [
            {
              "content": {
                "kind": "url",
                "url": "https://www.youtube.com/watch?v=abcdefghijk&t=62s"
              },
              "slot_id": "00000000-0000-4000-8000-000000000012"
            }
          ],
          "tags": [
            "research",
            "math"
          ],
          "title": "Division with remainder"
        }
      },
      "method": "POST",
      "note": "Use active category IDs from /categories/ and newly generated operation/slot UUIDs.",
      "path": "/api/v2/drafts/",
      "scope": "read_write"
    },
    "image_upload": {
      "steps": [
        "Reserve kind:image with actual size_bytes, rights_confirmed:true and a stable UUID.",
        "PUT raw file bytes to upload.url with returned headers; add read_write Bearer only for the same-origin local API URL.",
        "POST {} to /api/v2/media/<asset_uuid>/finalize/; use the asset only after current state is ready."
      ]
    },
    "mixed_categories": "/api/v2/posts/?category=1&category=2&tag=cross-domain",
    "owned_library": "/api/v2/media/?limit=20",
    "piece_permissions": {
      "body": {
        "allow_download": true,
        "allow_reuse": false
      },
      "method": "PUT",
      "path": "/api/v2/owned-nodes/<owned-node-uuid>/permissions/",
      "scope": "read_write"
    },
    "publish_saved_draft": {
      "body": {
        "expected_version": 1
      },
      "method": "POST",
      "note": "Use the actual returned version; publication is explicit.",
      "path": "/api/v2/drafts/<returned-draft-uuid>/publish/"
    },
    "rallies": "/api/v2/posts/?feed=rallies",
    "request_ingredient_download": {
      "body": {
        "download": true
      },
      "method": "POST",
      "next": "Use a current permitted node/post pair from the export manifest. If status is processing, later GET the same route with download=1 to check. Ready provider links need no Strategic.GG credential; ready local API file links require a same-account read credential.",
      "path": "/api/v2/nodes/<node_uuid>/media/?post_id=123",
      "scope": "read_write"
    },
    "reserve_video": {
      "body": {
        "client_request_id": "00000000-0000-4000-8000-000000000020",
        "payload": {
          "kind": "video",
          "rights_confirmed": true,
          "size_bytes": 1048576
        }
      },
      "method": "POST",
      "next": "Generate and retain your own UUID; use the actual file size. If upload is available, send bytes with a Tus client to its URL, without your Strategic.GG Bearer header. POST refresh on the returned asset until its current state is ready, then use that asset UUID in a draft. Honor current limits and Retry-After; do not blindly allocate on uncertainty.",
      "path": "/api/v2/media/",
      "scope": "read_write"
    },
    "search": "/api/v2/posts/?q=division%20with%20remainder",
    "unavailable_reference": {
      "available": false,
      "slot_id": "00000000-0000-4000-8000-000000000001"
    },
    "url_only_ingredient": {
      "author": {
        "id": 1,
        "username": "creator"
      },
      "availability": "link_only",
      "caption": "Listen to the bridge",
      "content": {
        "start_seconds": 62,
        "url": "https://www.youtube.com/watch?v=abcdefghijk&t=62s"
      },
      "kind": "url",
      "node_id": "00000000-0000-4000-8000-000000000002",
      "slot_id": "00000000-0000-4000-8000-000000000001",
      "title": "A melody",
      "url": "https://strategic.gg/nodes/00000000-0000-4000-8000-000000000002/"
    },
    "withdraw_piece": {
      "body": {},
      "method": "POST",
      "note": "Withdraw this piece from all placements; its shared media remains.",
      "path": "/api/v2/owned-nodes/<owned-node-uuid>/withdraw/",
      "scope": "read_write"
    }
  },
  "methods": [
    "GET",
    "HEAD",
    "POST",
    "PUT",
    "DELETE"
  ],
  "schemas": {
    "composition": {
      "connections": "Ordered {from_slot,to_slot,label}; unavailable endpoints have empty labels.",
      "fields": [
        "id",
        "url",
        "title",
        "caption",
        "tags",
        "categories",
        "author",
        "audience",
        "is_published",
        "created_at",
        "updated_at",
        "is_rally",
        "was_answer",
        "response_to",
        "center",
        "surrounding",
        "connections"
      ],
      "schema_version": 1,
      "slot": "null center or {slot_id,available,caption?,node?}; unavailable slots have only slot_id and available:false."
    },
    "draft": {
      "fields": [
        "id",
        "version",
        "published_version",
        "published_swis_id",
        "payload",
        "created_at",
        "updated_at"
      ],
      "visibility": "Only the owning active account can read its raw working payload, including through read credentials. No staff ownership override. Public projections and exports never include this working copy."
    },
    "draft_content": {
      "audio": "asset_id:UUID proposal in a private draft. Preview/publication requires the owner's ready verified MP3, AAC/M4A or supported PCM WAV; title and permissions use the common fields. No image dimensions, poster or alt_text.",
      "common": "kind, title:text≤200 (default empty), allow_reuse:boolean (default true), allow_download:boolean (default false), optional supersedes:owned node UUID. Each kind accepts only its own fields plus these common fields.",
      "image": "asset_id:UUID proposal in a private draft; optional alt_text≤2000. Preview/publication requires the owner's ready verified image.",
      "swis_ref": "swis_id:positive post ID; fresh publication references require a visible composition target; unchanged saved placements may remain unavailable.",
      "text": "body:text≤50000 Unicode code points; private drafts may exceed 500 source words. Preview/publication allows at most 500 words per text piece, including replacements and reused text. Ordinary Markdown/LaTeX and paragraph cues; no additional HTML escape/unescape pass.",
      "url": "url:HTTP(S)≤8192, without credentials/whitespace; optional nonnegative integer start_seconds/end_seconds (end needs an earlier start). Publication checks provider cues and local-reference rules without fetching the URL.",
      "video": "asset_id:UUID proposal in a private draft. Preview/publication requires the owner's ready video, as verified by the shared media lifecycle."
    },
    "draft_payload": {
      "audience": "PUBLIC (default) or FOLLOWERS. This is proposed publication visibility; drafts are always private.",
      "caption": "Text, maximum 2000 characters; defaults to empty.",
      "category_ids": "0–3 distinct active category IDs in a draft; publication requires 1–3. Existing bound assignments may retain retired IDs.",
      "center": "null (default) or draft_slot; separate from surrounding allowance.",
      "connections": "0–64 {from_slot:UUID,to_slot:UUID,label:text≤500}; endpoints must exist, differ and have distinct ordered pairs. Defaults to [].",
      "response_to_swis_id": "null (default) or Rally post ID. A new answer must have a center and an accessible current published Rally at publication. A bound answer cannot change its parent.",
      "schema_version": "Optional integer 1; defaults to 1.",
      "surrounding": "0–10 draft_slot objects in saved order; defaults to []. Publication applies current Free 4 / Pro 6 / Elite 10 policy and existing downgrade rules.",
      "tags": "0–12 text tags, maximum 40 characters before/after NFKC/casefold/whitespace normalization; normalized tags must be nonempty and distinct.",
      "title": "Text, maximum 200 characters; defaults to empty."
    },
    "draft_slot": {
      "fields": "slot_id:UUID, caption:text≤500 (default empty), and exactly one of node_id:UUID or content:draft_content. Slot UUIDs must be distinct across center and surrounding.",
      "reuse": "Existing node_id may also carry source_post_id. Cross-owner new attachments need current reuse permission and a published PUBLIC source containing the node. FOLLOWERS reading permission alone is insufficient."
    },
    "manifest": {
      "availability": [
        "text_included",
        "link_only",
        "download_available",
        "unavailable"
      ],
      "download": "Optional {request_url,representation,readiness:'unchecked'}; representations are encoded_video, published_image or published_audio. Audio content includes duration_ms. No files, provider URLs or grants.",
      "fields": [
        "schema_version",
        "post",
        "center",
        "surrounding",
        "connections"
      ],
      "schema_version": 1,
      "unavailable": {
        "availability": "unavailable",
        "slot_id": "UUID"
      }
    },
    "media_asset": {
      "fields": [
        "id",
        "kind",
        "state",
        "is_hidden",
        "declared_size_bytes",
        "size_bytes",
        "duration_ms",
        "width",
        "height",
        "reserved_video_seconds",
        "reserved_bytes",
        "upload_expires_at",
        "cleanup_paused",
        "last_error_code",
        "created_at",
        "updated_at"
      ],
      "privacy": "Actual owner only, with no staff override. No provider IDs, object keys, upload tickets, correlation IDs, signed URLs or file bytes."
    },
    "media_grant": {
      "expiry": "Use expires_at; the provider deadline is set five minutes after grant processing starts, before download lookup/preparation. Local API file links require a valid credential for the signed account and recheck access. Provider delivery URLs are bearer capabilities; do not send Strategic.GG credentials to them.",
      "fields": "{kind,status:'ready',expires_at,playback?,iframe?,poster?,download?} or {kind:'video',status:'processing',download:null,expires_at:null}.",
      "status": "These responses use status ready/processing, not success. For video, GET download=1 checks existing derivative state; POST {download:true} prepares it only when absent. Image/audio downloads use their published canonical files without Stream preparation. POST treats download as true even if a download=0 query was supplied."
    },
    "media_reservation": {
      "fields": "Exactly {kind:'video'|'image'|'audio',size_bytes:positive integer≤9007199254740991,rights_confirmed:true}.",
      "limits": "Read /media/usage/ for current account allowances and file/duration/pixel bounds. Declared size must fit the kind's configured input maximum. The locked writer remains authoritative.",
      "rights": "Confirm the right to host the upload and offer its selected permissions. Original/authorized audio admits one mono/stereo program at 8-48 kHz: MP3 Layer III (free-format/VBRI unsupported), AAC/M4A or 8/16-bit integer PCM WAV; use external-platform links and cues for released music references."
    },
    "media_usage": {
      "accounting": "Current account totals conservatively sum the greater reserved or measured usage per asset. Null capacities stay unconfigured, including for staff. Images and audio share retained storage bytes; video seconds are a separate allowance. Missing audio-specific configuration blocks fresh audio only.",
      "fields": [
        "video_seconds",
        "storage_bytes",
        "reserved_video_seconds",
        "reserved_bytes",
        "unfinished_uploads",
        "limits",
        "reservation_status"
      ],
      "limits": [
        "video_seconds",
        "storage_bytes",
        "unfinished_uploads",
        "video_max_seconds",
        "video_upload_bytes_target",
        "image_max_bytes",
        "image_max_pixels",
        "audio_max_seconds",
        "audio_max_bytes"
      ],
      "reservation_status": "{video:{allowed:boolean,reason:string|null},image:{allowed:boolean,reason:string|null},audio:{allowed:boolean,reason:string|null}}. Reasons include media_uploads_paused, media_policy_unconfigured, unfinished_upload_limit and media_capacity_exceeded; allowed has reason:null. Advisory only, not a reservation, upload-resume permission or provider readiness check."
    },
    "node": {
      "audio": "title, size_bytes, duration_ms, grant_url (API media route; owner preview can use the owner asset route). No dimensions, poster or alt_text.",
      "common": [
        "id",
        "kind",
        "author",
        "allow_reuse",
        "allow_download"
      ],
      "image": "title, width, height, size_bytes, alt_text, grant_url (API media route).",
      "swis_ref": "creator_title and target current bounded post card; no recursive expansion.",
      "text": "title, html (viewer-filtered rendered HTML), body only for the actual node owner.",
      "url": "creator_title, url, optional title, start_seconds/end_seconds and embed descriptor.",
      "video": "title, width, height, size_bytes, duration_ms, grant_url (API media route; owner preview can use the owner asset route)."
    },
    "owned_node": {
      "fields": [
        "id",
        "kind",
        "title",
        "created_at",
        "available",
        "withdrawn",
        "is_hidden",
        "allow_reuse",
        "allow_download",
        "placements"
      ],
      "placements": "Count of placement rows, including repeated, hidden and unpublished uses; not a distinct/public post count.",
      "privacy": "Actual owner only, with no staff override. Unavailable content has an empty title. No raw source, destination IDs, draft bodies or media grants."
    },
    "upload_ticket": {
      "fields": [
        "url",
        "method",
        "headers",
        "expires_at"
      ],
      "method": "Stream video uses PATCH with Tus-Resumable:1.0.0 and the Tus upload protocol; image/audio uses PUT with raw bytes. Follow the returned method/headers and expiry.",
      "privacy": "Owner write capability. Never put a Strategic.GG credential in a URL or forward it to provider origins. Only local API upload URLs require the account's read_write Bearer header. Null means no current ticket; read stored status and reconcile."
    }
  },
  "status": "success"
}
