Optional Postiz publishing backend
Status: offline, mock-tested slice. No Postiz instance, API key or social account has been connected or live-tested by this repository. Nothing here is a receipt, a connection claim or permission to publish.
Postiz is a scheduler that holds an owner’s platform OAuth connections (“channels”, called integrations in its API). ReachWeaver treats it as a backend, not a platform: the account’s platform is still bluesky, linkedin and so on, and ReachWeaver remains the only approval authority. See ADR-0001.
Verified Postiz facts (checked 2026-10-08)
Section titled “Verified Postiz facts (checked 2026-10-08)”| Fact | Value | Source |
|---|---|---|
| Cloud API base | https://api.postiz.com/public/v1 |
OpenAPI servers, API overview |
| Self-hosted API base | https://{your-domain}/api/public/v1 |
same OpenAPI servers entry |
| Auth | Authorization: <api-key> (raw key, no Bearer prefix); OAuth tokens start with pos_ and are sent the same way |
API overview |
| List channels | GET /integrations returns [{id, name, identifier, picture, disabled, profile, customer}] |
List integrations; field mapping confirmed in the public controller, apps/backend/src/public-api/routes/v1/public.integrations.controller.ts in postiz-app (read, not copied) |
| Create | POST /posts with type (now/schedule/draft), date, shortLink, tags, posts[{integration:{id}, value:[{content, image}], settings:{__type}}]; response [{postId, integration}] |
Create post |
type semantics |
now publishes immediately (date ignored), schedule at date, draft stores without scheduling |
Create post |
| Read posts | GET /posts?startDate&endDate[&customer] returns {posts:[{id, content, publishDate, releaseURL, state: QUEUE/PUBLISHED/ERROR/DRAFT, error, integration}]} |
List posts |
| Delete | DELETE /posts/{id} deletes every post in the same group |
Delete post |
| Text-only providers | bluesky, mastodon, telegram, threads need only {"__type": ...}; linkedin/linkedin-page and facebook take optional settings |
provider pages: Bluesky, LinkedIn, Telegram, Facebook, Threads |
| Rate limits | Inconsistent in the docs. The overview says 90 requests/hour (100 on cloud), applied only to the create-post endpoint, one global value per instance (API_LIMIT on self-host). The OpenAPI description embedded in each endpoint page says 30 requests/hour. ReachWeaver relies on neither; its own budget (default 2 attempts/account/24 h) is far lower |
API overview |
| Errors | 400, 401, 403, 404, 413, 429, 5xx documented; “5xx: retry with exponential backoff” | API overview |
| MCP | Exists: https://mcp.postiz.com/mcp (Bearer key) or /mcp/{api-key} in the URL; 13 tools including schedulePostTool |
MCP introduction, MCP setup |
| License | gitroomhq/postiz-app is AGPL-3.0 (GitHub license API and LICENSE file); latest release v2.25.0, 2026-10-02 when first checked. Re-checked 2026-10-15: still AGPL-3.0, latest release v2.25.1 (2026-10-11); gitroomhq/postiz-agent still reports NOASSERTION. See Licensing and credential review |
postiz-app, LICENSE |
Unknown (not verified; nothing depends on it): an idempotency key for POST /posts (none is documented); whether id in GET /posts always equals the postId returned by create (the lookup reports found: false rather than guessing); whether Postiz delivers the content byte-identically on every provider; per-integration OAuth scopes or a “refresh needed” flag (not exposed by GET /integrations); cloud pricing and plan quotas (the overview says plans tier on channel and posts-per-month quotas only); whether a separate commercial/enterprise edition changes the public API (the repository contains an enterprise.controller.ts, but its scope was not examined). Self-hosted deployments that use a different reverse-proxy path than /api are not covered.
MCP is deliberately not used. The documented URL form embeds the API key in the URL, and an MCP tool hands the agent raw publishing authority, which ADR-0001 rejects. The adapter uses the REST API from the local CLI only; the key never enters a Claude prompt.
Licensing. ReachWeaver (Apache-2.0) talks to Postiz over its documented HTTP API and copies no Postiz source code. Running or modifying a Postiz server is the owner’s separate AGPL-3.0 decision. This is not legal advice.
Configuration
Section titled “Configuration”Add an entry to your ignored config/accounts.json. No key in this file:
{ "id": "my-linkedin-via-postiz", "platform": "linkedin", "env_prefix": "REACHWEAVER_LI_POSTIZ", "handle": "the-integration-profile", "backend": "postiz", "postiz_base_url": "https://api.postiz.com", "postiz_integration_id": "cm4ean69r0003w8w1cdomox9n"}backendmust be exactly"postiz". Accounts withoutbackendbehave exactly as before. There is no default backend.postiz_base_urlmust be a public HTTPS origin (policy.require_https_public_server): no path, port, credentials, IP address,localhost,.localor single-label LAN host. A LAN-only self-hosted instance is not supported in this slice.postiz_api_prefixis optional:/public/v1forapi.postiz.com, otherwise/api/public/v1(the documented self-hosted path). Only those two values are accepted. Unknownpostiz_*keys are rejected.postiz_integration_idandhandlemust matchGET /integrationsidandprofile. Runpostiz-checkto read them.- The key is read only from
{env_prefix}_POSTIZ_API_KEY(printable ASCII, no whitespace) at request time, throughaccounts.credential: an environment variable, or the file$CREDENTIALS_DIRECTORY/{env_prefix}_POSTIZ_API_KEYunder systemdLoadCredential=(onboarding).doctorlists the missing variable name, never a value. - Every account needs its own
env_prefix, Postiz accounts included:load_accountsrefuses identical or overlapping prefixes (issue #5). Two ReachWeaver accounts that use the same Postiz API key therefore store it once per prefix;credentials-auditreports that ascredential_value_reused. postiz_workspaceis optional (owner-approved 2026-10-09, proposal). It is a lowercase slug that you choose (a-z,0-9,-, 1–63 characters, for example"acme"), and it is accepted only together with"backend": "postiz". Give the same value to every account whose API key belongs to the same Postiz organization. A 429 on any of them then pauses all of them (see “429” below). It is a label, never the key, a URL or a variable name.
Platforms accepted for the backend, and why the rest are refused (postiz.PROVIDERS/EXCLUDED, also checked against the capability registry at send time):
| ReachWeaver platform | Postiz identifier |
Decision |
|---|---|---|
| bluesky, mastodon, telegram, threads | same name | allowed |
linkedin or linkedin-page |
allowed; the observed identifier is sent as __type |
|
facebook |
allowed (Page post defaults) | |
| — | refused: human-only, moderator-only scheduling | |
| x | — | refused until the #24 cost ceiling and spend ledger exist |
| instagram, pinterest | — | refused: text-only format unavailable |
| youtube, tiktok | — | refused: text format unknown (video-only) |
| devto, hashnode | — | refused: manual-export platforms; Postiz also needs title/tags settings |
The send-time gate is stricter than “text is not unavailable”: it requires registry text api_direct or api_gated and platform status supported or requires-review. unknown, manual_export, blocked and manual-export fail closed. api_gated is accepted only because the gating (app review, OAuth consent) is held by the owner’s Postiz connection, which the identity preflight confirms exists; it is not proof of scope.
LinkedIn: linkedin versus linkedin-page (native and Postiz differ)
Section titled “LinkedIn: linkedin versus linkedin-page (native and Postiz differ)”- Native ReachWeaver adapter (
platform: linkedin, no Postiz): personal member profile only. Organization (company) Pages are refused (LINKEDIN.md): the adapter sends only a member author and neverurn:li:organization:*. - Via Postiz:
PROVIDERS["linkedin"]is("linkedin", "linkedin-page"), so an integration whose Postizidentifierislinkedin-pageis accepted and posts as a company Page, using Postiz’s own LinkedIn connection and scopes. The native Page refusal does not apply on this path. The identity preflight checks only that the integration id exists, is enabled, has an allowed identifier and aprofileequal to the configuredhandle. - The approval gate, dry run, emergency stop, caps and ambiguous-send handling still apply to a Page post. What differs is the author: ReachWeaver cannot verify the Page role or
w_organization_socialscope, because the owner’s Postiz connection holds them. - Decision pending (not changed here): refusing
linkedin-pagewould be a small validation change inpostiz.PROVIDERS, but it contradictstests/test_postiz.py(which sends through alinkedin-pageintegration) and the Postiz column ofdocs/PLATFORM-MATRIX.md, so it needs an owner decision and a test change, not a quiet edit. Until then, if you want the native “personal profile only” guarantee, do not connect a LinkedIn Page integration to an account that usesplatform: linkedin. - Postiz listing both identifiers was verified 2026-10-08 (see above). The Page-posting behaviour is read from this repository’s code and tests, not from a live Postiz instance.
approve (exact text) -> [schedule] -> publish --live + REACHWEAVER_ALLOW_LIVE=1 + REACHWEAVER_ENV=prod (checked before any account is loaded or key is read, issue #5) -> pause check -> opt-out gate (@-mention of a listed handle/domain = BLOCKED, post stays approved) -> text lint -> backend capability gate -> key present/well-formed -> GET /integrations: configured id exists, provider maps to platform, profile == handle, not disabled (read-only; failure = BLOCKED, post stays approved) -> claim_sending (BEGIN IMMEDIATE, rolling budget, pause re-checked) -> POST /posts {type: now, one integration, exact approved text, shortLink false} -> 200 [{postId, integration == configured}] -> needs_review, remote_id = postiz:<postId>, last_error = postiz_accepted_platform_receipt_unverified -> any error after the claim -> needs_review (postiz:<sanitized code>); never retried- Acceptance is not a receipt.
type: nowonly queues a job. The platform may still reject it, Postiz may transform the text, and the native permalink is not in the create response. The post is thereforeneeds_review, notpublished, andreportdoes not count it. A human opens the native post, compares the exact text, and runsreconcile POST_ID --published --remote-id NATIVE_ID --remote-url NATIVE_URL, or--not-published(then delete the Postiz job in Postiz if it exists). - Never Postiz
draft/schedule. ReachWeaver’s outbox owns timing; it sendsnowwhen the approved post is due. A future-dated Postiz job would keep running afterreachweaver pause. Postiz drafts or approvals are never treated as ReachWeaver approval. - Errors are sanitized codes only, for example
postiz_http_401_unauthorized,postiz_http_403_forbidden,postiz_http_429_rate_limited,postiz_http_500,postiz_network_or_timeout_uncertain,postiz_unexpected_create_response_uncertain. Response bodies (which can echo content) and the key never reach output,last_errororevent_log. Redirects are refused, so theAuthorizationheader cannot follow one. - 429 (issue #11). A 429 records a per-account cooldown: a standard
Retry-Afterheader if Postiz sends one (not documented), otherwise 15 minutes, clamped to 60 s–24 h. The account’s other posts areSKIPPEDuntil it ends. A post-claimPOST /posts429 staysneeds_reviewand is classifiedambiguous; it is never retried, whatever the Postiz guidance on 5xx says. The read-onlyGET /integrationspreflight may be retried twice with jitter before the claim. Withoutpostiz_workspacethe cooldown is per ReachWeaver account. With it, the 429 is recorded for the whole workspace:scope=postiz-workspace:<slug>. Every other account that declares the same workspace is thenSKIPPED ... account_provider_cooldown_active ... scope=postiz-workspace:<slug> recorded_by=<account>in the dry run, before its identity preflight, and again insideclaim_sending. The reason: Postiz counts create-post requests per organization. The docs’ 90/h (100 cloud) is one configured value per instance, but the throttle key in the source is the organization id plus_posts(ThrottlerBehindProxyGuard.getTracker, postiz-app commit4c5d42a, read 2026-10-09). An API key orpos_OAuth token resolves to exactly one organization. ReachWeaver does not group per instance, because organizations on one instance have separate counters. - Library callers of
publishers.publish()getpostiz.BackendAccepted, aPublishErrorsubclass, on acceptance, so the generic “PublishError meansneeds_review” handling stays safe.
Commands
Section titled “Commands”reachweaver doctor # offline; shows backend=postiz and the env namereachweaver postiz-check --account ID --online # read-only GET /integrations: id/identifier/profile/disabled + match resultreachweaver postiz-check --account ID --online --post-id POST_ID# read-only GET /posts around send_started_at: backend state and releaseURL (HTTPS only).# Backend-reported, not first-party evidence; it never reconciles.REACHWEAVER_ENV=prod REACHWEAVER_ALLOW_LIVE=1 reachweaver publish --live --post-id POST_ID # owner-supervised, production host onlyconnect-check refuses backend accounts and points to postiz-check.
What the API key can and cannot prove (scope detection)
Section titled “What the API key can and cannot prove (scope detection)”A passing postiz-check --online proves only: the key is accepted, the configured integration id is listed for that key’s organization, its provider identifier maps to the account’s platform, its profile equals the configured handle, and it is not flagged disabled. The result is identity: verified, write_permission: unverified.
It does not prove: that the stored platform OAuth grant still has the write scope, that the platform token is unexpired or unrevoked, that the owner holds a Page/organization role, that profile is unique, or that an accepted post will be delivered. GET /integrations is documented to return no scope field; whether any Postiz version exposes one is unverified. The disabled parser reads only a literal true as disabled; a missing or non-boolean value reads as enabled (pinned by test, not changed here). ReachWeaver therefore never infers scope and keeps write_permission at unverified. Full table, design decision and the live-verification checklist (all unchecked): POSTIZ-SCOPE-DETECTION.md.
Manual fallback when Postiz is unavailable
Section titled “Manual fallback when Postiz is unavailable”The fallback is to inspect the post offline and publish it yourself. Nothing is retried in the background.
- Before the claim (Postiz unreachable, 401/403/5xx on
GET /integrations, a disabled or mismatched integration): the run printsBLOCKEDand the post staysapproved. NoPOST /postswas sent. reachweaver show POST_IDis offline and reads no key. It prints the exact approved text and the approval hash. For a backend account, itsplatform_previewis the Postiz request this account would send:path: postiz,request(POSTURL,type: now,shortLink: false, configuredintegration_id),handle,network: none, plus thereceiptandmanual_fallbackrules. It no longer shows the native Bluesky record key or service for a Postiz-backed Bluesky account, because that account never sends one.- To post by hand, copy the exact text into the platform’s own app. Then run
reachweaver cancel POST_IDso that a laterpublish --livecannot also send it through Postiz. Keep the native URL in your private notes. A cancelled post is not counted as published; ReachWeaver claims no delivery it did not observe. - For Threads and Facebook Pages,
manual-exportbuilds a full creator package (MANUAL-EXPORT.md). For a campaign, usecampaign-export. - After the claim (any
needs_reviewpost): do not cancel and do not resend. First check the Postiz calendar and the native account, thenreconcile(see Flow).
Owner smoke test (staging or production)
Section titled “Owner smoke test (staging or production)”Status: NOT RUN. On 2026-10-15, no Postiz instance, API key, integration or social account had been connected to ReachWeaver. No smoke test has been performed, and there is no receipt in receipts/. Nothing in this repository claims that a post sent through Postiz exists. Only the owner can run this procedure. An agent must not run it, schedule it, or treat a mocked test as a substitute for it.
Scope. One harmless text post to one account the owner controls, sent through one Postiz integration. Prefer Bluesky: text-only, and with a public read API for the receipt. You may use a Postiz Cloud workspace or your own self-hosted instance, and a dedicated test account or your real one. Record which ones you used.
- Offline readiness (safe).
python3 -m unittest discover -s tests -qpasses, andreachweaver doctorshowsbackend=postizand the variable name{env_prefix}_POSTIZ_API_KEY, never a value. - Owner, in Postiz. Connect the channel in Postiz yourself, accepting the platform’s OAuth consent there. Then create an API key. Store it outside Claude, in the environment of your own terminal or as a systemd
LoadCredential=file (see Configuration), and use a separateenv_prefixfor each ReachWeaver account. - Identity (read-only).
reachweaver postiz-check --account ID --onlinemust showidentity: verified, the expectedproviderandwrite_permission: unverified. Write down the integrationid,identifier,profileanddisabledfields actually returned (the scope checklist). - One post.
draft, thenshow POST_ID(check theplatform_previewtarget),check POST_ID,approve POST_IDafter reading the exact text, andpublish --post-id POST_ID(a dry run, no network). - Send (owner present, once).
REACHWEAVER_ENV=prod REACHWEAVER_ALLOW_LIVE=1 reachweaver publish --live --post-id POST_ID. Expected result: exit 3 andREVIEW ... accepted by Postiz backend (postiz:<id>). Keepreachweaver pauseready, and run it if anything looks wrong. - Observe.
reachweaver postiz-check --account ID --online --post-id POST_IDshows the backendstateandreleaseURL. This is backend-reported only. Next, open the native post, compare the text byte for byte, and read its native id from the platform (for Bluesky, the public AppViewapp.bsky.feed.getPosts). - Reconcile.
reachweaver reconcile POST_ID --published --remote-id NATIVE_ID --remote-url NATIVE_URL, or--not-publishedif no native post exists. In that case, also delete the Postiz job in Postiz. - Record evidence privately first. Record the date and time with time zone, the instance (cloud, or self-hosted with its version), the integration identifier, the backend job id, the native id and URL, the observed text equality, and the exit codes. Publish a first-party receipt in
docs/receipts/postiz-<date>.json, modelled on the Bluesky receipt, and update this status line only after the owner decides to. The testPostizIssue56DocTestskeeps this section marked NOT RUN until apostiz*receipt exists. - Teardown (optional). Revoke the Postiz API key, or remove
backendfrom the account entry. Accounts withoutbackenduse the native adapter, as before.
Pass criteria: one POST /posts and one native post, with byte-equal text, reconciled by a human. Failure handling: a needs_review post is never resent. Investigate it with the steps above.
Licensing and credential review (2026-10-15)
Section titled “Licensing and credential review (2026-10-15)”This is a review of facts, not legal advice. Each item lists its source and the date it was checked.
| Item | Finding | Source (retrieved 2026-10-15) |
|---|---|---|
| ReachWeaver licence | Apache-2.0 (LICENSE, NOTICE) |
this repository |
| Postiz server licence | GNU AGPL v3. The GitHub license API reports AGPL-3.0 for LICENSE (blob 24fd2279), and the file opens with “GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007”. The latest release is v2.25.1, 2026-10-11 |
LICENSE, https://api.github.com/repos/gitroomhq/postiz-app/license, https://api.github.com/repos/gitroomhq/postiz-app/releases/latest |
| Postiz agent | gitroomhq/postiz-agent has license NOASSERTION (unknown). ReachWeaver does not use it |
https://api.github.com/repos/gitroomhq/postiz-agent/license |
| Copying | reachweaver/postiz.py is an independent client for the documented HTTP API. No Postiz source code, schema or asset is copied. One controller and one throttler guard were read only to confirm field names and the rate-limit key (see the table above). Interoperating over a network API does not make ReachWeaver a derivative of the AGPL server; this is the project’s reading and has not been reviewed by counsel |
API overview (re-read 2026-10-15: raw Authorization key, 90/h (100 cloud) on create-post only, “5xx: retry with exponential backoff”) |
| Running Postiz | Self-hosting or modifying a Postiz server is the owner’s own AGPL-3.0 decision; its network-use clause applies to that server, not to ReachWeaver | AGPL-3.0 section 13 in the LICENSE above |
| Postiz Cloud terms | The terms page exists and showed “Last updated: October 15, 2026” when fetched. That date matches the retrieval day, so it may be generated when the page loads (unverified). The operators are Gitroom Limited and Gitroom LLC; Gitroom LLC holds the platform developer registrations. The terms forbid reselling or white-labelling the Service without a written agreement, and scraping or reverse engineering except as an open-source licence allows. A clause-by-clause review has not been done. ReachWeaver accepts no terms on the owner’s behalf. Using Postiz Cloud is the owner’s own contract | Terms, Privacy |
| Credential compartments | One key for each ReachWeaver account, held only in {env_prefix}_POSTIZ_API_KEY (environment or $CREDENTIALS_DIRECTORY), read at request time, never in accounts.json, never in a Claude prompt (MCP is not used, because its URL form embeds the key), and never in output, last_error or event_log. load_accounts refuses overlapping prefixes, credentials-audit reports reuse of the same value, and an account never borrows another account’s key. ReachWeaver never sees platform OAuth tokens: the Postiz connection holds them |
this repository: tests/test_postiz.py (test_each_backend_account_sends_only_its_own_key, test_another_accounts_key_never_substitutes_for_a_missing_one, test_missing_key_and_malformed_key_never_echoed, test_post_claim_failures_need_review_and_hide_key), tests/test_hardening.py::test_postiz_backend_suffix_counted |
| Permission | A Postiz API key is never permission to publish. Each post still needs its own approval, bound to the exact text and destination account (approval_payload_sha256, re-checked inside claim_sending), plus --live and both environment opt-ins |
tests/test_approval_binding.py (test_postiz_run_live_send_blocks_tampered_post and the related tests) |
| Integration binding | The approval also binds the account’s postiz_base_url and postiz_integration_id (a fingerprint under the tag approval/options-1). Re-pointing the account to another server or integration after approval refuses the post before any request, including GET /integrations (approval_send_options_changed_reapproval_required, post stays approved); the send gate also refuses a publish() call with a different account entry. A Postiz post approved with ReachWeaver 0.12.0 or earlier is refused with approval_send_options_missing_reapproval_required until re-approved once. postiz_api_prefix is not bound: it only selects the documented API path on the bound server |
tests/test_send_binding_repeat.py (PostizSendOptionsTests) |
Backend contract
Section titled “Backend contract”publishers.PublishingBackend (a typing.Protocol) declares what every publishing path provides, and publishers.backend_for(account) names the path an account entry selects (no backend key: native; "postiz": this backend; anything else: refused). It is declarative only: publishers.publish and postiz.run_live_send dispatch and gate exactly as before.
| Contract step | Native path (publishers.NATIVE_PATH) |
Postiz (postiz.POSTIZ_BACKEND) |
|---|---|---|
preview(account, content): offline, no credential, refuses text validate_post rejects |
target account, platform and exact text | the same plus the fixed create shape (POST /posts, type: now, shortLink: false, configured integration id) |
create(...): one content-create request, only after a durable claim (sendgate) |
returns the platform’s own receipt | raises BackendAccepted (a PublishError): never a receipt |
lookup_receipt(...): read-only, never reconciles |
None (create already returned the receipt) |
GET /posts state and backend-reported releaseURL |
Reconciliation hand-off (acceptance_status) |
published |
needs_review until a human reconcile with the native id/URL |
tests/test_postiz_contract.py runs the same conformance checks against both paths (native Bluesky mocked at publishers.request_json, Postiz replaying the synthetic fixtures in tests/fixtures/providers/postiz/).
Tests: tests/test_postiz.py (mocked publishers._send_http; payload shape, identity mismatch before claim, 401/403/429/5xx, timeouts, unexpected responses, key redaction, pause, dry run, live opt-in, unapproved posts, no auto-retry, read-only lookup). PostizFixtureTests in the same file replays synthetic fixtures for a create response carrying two jobs (a possible duplicate: needs_review, one POST /posts, nothing retried), two runs racing for one approved post (one claim, one create), a 403 on create shaped like a grant without write scope (needs_review, body and key never shown, not retried) and a disabled integration (BLOCKED before the claim). The fixture bodies are illustrative; none was recorded from Postiz. PostizIssue56Tests adds the remaining issue #56 cases. A pause during the read-only preflight is refused at the claim, so POST /posts is never sent. Backend states QUEUE, ERROR, an unknown state and a missing job are reported read-only, never reconciled, and the error text is not shown. A lookup refuses a post that has no postiz: job id. reconcile stores the observed native id and permalink. Each account sends only its own key. show previews the Postiz request offline. A Postiz outage leaves the post approved for the manual fallback. PostizIssue56DocTests pins the NOT RUN smoke-test status and the dated licensing review.
Issue #56 acceptance evidence (offline, 2026-10-15)
Section titled “Issue #56 acceptance evidence (offline, 2026-10-15)”| Acceptance test | Evidence | State |
|---|---|---|
| Adapter interface plus provider-selection config, opt-in, disabled by default | publishers.PublishingBackend, publishers.backend_for (no backend key: native; unknown: refused); tests/test_postiz_contract.py::ContractShapeTests, tests/test_postiz.py::PostizTests::test_native_accounts_unchanged, test_config_refusals |
met offline |
| Mock tests: account mismatch, 401/403/429/5xx, ambiguous write, duplicate job, paused queue, missing scope | test_identity_mismatches, test_integration_mismatch_refused_before_claim, test_preflight_http_errors_block_before_claim, test_post_claim_failures_need_review_and_hide_key, PostizFixtureTests (two jobs, race, 403 missing scope, disabled), test_pause_respected_cli_and_direct, PostizIssue56Tests::test_pause_during_the_preflight_stops_the_send_at_the_claim, tests/test_rate_limits.py::test_postiz_post_claim_429_cooldown_and_preflight_429 |
met offline |
| Observed remote id and permalink; an unknown outcome goes to review, with no blind retry | test_acceptance_goes_to_needs_review_not_published (postiz:<id>), test_receipt_lookup_is_backend_reported_and_never_reconciles (releaseURL), PostizIssue56Tests::test_reconcile_stores_the_observed_native_id_and_permalink, test_backend_job_states_are_reported_and_never_reconcile |
met offline. The native id and URL come from the owner’s observation, by design |
| Real-account smoke test documented | Owner smoke test | procedure ready, NOT RUN (owner-only) |
| Credential compartments and licensing review, dated and sourced | Licensing and credential review | recorded 2026-10-15. A counsel review of the Cloud terms is still open |
| README, platform matrix, skill publishing reference, offline tests | README “Optional Postiz backend”, PLATFORM-MATRIX.md backend row, .claude/skills/reachweaver/references/publishing.md, the tests above |
met |
| Other publishing flows unchanged | test_native_accounts_unchanged, PostizIssue56Tests::test_native_show_preview_is_unchanged, the full offline suite |
met offline |
