Instagram single-image posts (owner-approved, disabled)
Status: owner-approved 2026-10-09; disabled until a live receipt. The mock-tested adapter exists, but it is unreachable. The capability registry keeps Instagram image at api_gated with reachweaver_adapter: "none", and instagram is not in policy.SUPPORTED_LIVE. publish --live and connect-check --online therefore refuse Instagram before any credential read or request. No Meta app, Instagram account or live post exists. Until the owner completes the steps in the security review proposal, use manual-export --platform instagram (MANUAL-EXPORT.md). Owner preparation steps: INSTAGRAM-SETUP.md.
What the adapter would send
Section titled “What the adapter would send”One JPEG image with a caption and alt text, to the authenticated professional (Business or Creator) account’s own feed, through the Instagram API with Instagram Login (graph.instagram.com, version v25.0 as in the official examples). It never sends carousels, video, reels, stories, user tags, locations, collaborators or product tags.
| Step | Request (official) | Notes |
|---|---|---|
| Identity | GET /me?fields=user_id,username,account_type |
user_id must equal ig_user_id, username must equal handle, account_type must be Business or Media_Creator. Before the claim. |
| Quota | GET /{ig_user_id}/content_publishing_limit?fields=quota_usage,config |
Read-only, before the claim. Refused when quota_usage >= quota_total. |
| Image check | GET <your image URL> |
Must still hash to the approved SHA-256. Before the write marker. |
| Container | POST /{ig_user_id}/media {image_url, caption, alt_text} |
JSON with Authorization: Bearer. The write marker is set just before this. |
| Status | GET /{container}?fields=status_code |
Once a minute, at most 5 checks (official recommendation). |
| Publish | POST /{ig_user_id}/media_publish {creation_id} |
Returns the media id (the receipt). |
| Receipt | GET /{media}?fields=id,permalink,username,media_type |
A mismatching username or media type goes to needs_review. |
Image hosting: you host the JPEG
Section titled “Image hosting: you host the JPEG”Instagram does not accept an upload here: “We will cURL your image using the passed in URL so it must be on a public server”. ReachWeaver hosts nothing. Put the JPEG on an HTTPS host you control (your site, a static bucket with public read) at a plain path:
https://, a public DNS name (no IP address,localhostor.local), port 443;- no user name or password, no query string (signed or tokenized URLs would store a secret in the database and in Meta’s fetch) and no fragment;
- printable ASCII only (percent-encode anything else); no redirects (give the final URL);
- JPEG only (MPO/JPS are not supported by Instagram and are not detected by ReachWeaver), at most 8,000,000 bytes, aspect ratio from 4:5 to 1.91:1. Widths outside 320 to 1440 are scaled by Instagram, not refused.
reachweaver draft --account my-instagram --text "Release notes are out. Link in bio."reachweaver media-attach POST_ID --url https://img.example.org/2026/release.jpg \ --alt "Bar chart: monthly downloads rose from 10 to 40" --onlinereachweaver show POST_ID # url, sha256, size, dimensions; nothing uploadedcurl -s https://img.example.org/2026/release.jpg | sha256sum # optional: compare with sha256media-attach makes one read-only GET with no credentials. It refuses addresses that resolve to private networks, and records the SHA-256, size and dimensions. Alt text is required (at most 1,000 characters) and is checked against the opt-out list. Approval then binds the caption, the image SHA-256, the MIME type, the alt text and the URL. Do not change or remove the image until the post is published. A changed image blocks the send (approval_media_changed_reapproval_required) until you restore the original bytes or re-attach and re-approve (unapprove, media-detach, media-attach, show, approve). media-detach POST_ID works on drafts only.
Account configuration (ignored config/accounts.json, no secrets)
Section titled “Account configuration (ignored config/accounts.json, no secrets)”{"id": "my-instagram", "platform": "instagram", "env_prefix": "REACHWEAVER_IG", "handle": "your_username", "ig_user_id": "17841400000000000", "authorization_expires_at": "2026-12-01T00:00:00+00:00"}ig_user_id is the user_id from GET /me (a string of digits). Without it the account stays manual-only metadata, as before. The token is read only from REACHWEAVER_IG_ACCESS_TOKEN (environment or $CREDENTIALS_DIRECTORY). Dashboard tokens are valid for 60 days; record authorization_expires_at so doctor and health (and so ops-summary and the dashboard) warn 7 days ahead, and an expired token is refused offline.
Documented limits (read 2026-10-09)
Section titled “Documented limits (read 2026-10-09)”Caption: 2,200 characters, 30 hashtags and 20 @ tags (ReachWeaver counts emails as @ tags, which only tightens the check). Containers expire after 24 hours, and at most 400 containers may be created per rolling 24 hours. The content-publishing guide says “100 API-published posts within a 24-hour moving period”, while its carousel section and the endpoint reference (quota_total “currently 50”) say 50. The live quota is authoritative, and ReachWeaver’s own budget (at most 10 per 24 h, tighten-only caps) applies first.
Sources: content publishing, IG User Media, content_publishing_limit, get started with Instagram Login, IG Media.
Not verified: the HTTP statuses Meta uses for an expired token, a missing permission or throttling; whether Authorization: Bearer works for GET (GETs use the documented access_token query parameter); the permalink format; whether the IG User Media limits (that reference still lists only Facebook Login requirements, re-checked 2026-10-15) apply unchanged to Instagram Login; whether Standard Access limits single-image publishing in practice.
Re-checked 2026-10-12 against the content publishing guide (one page, read-only): it still states none of the error statuses for an expired token, missing permission or throttling (it only names the rate-limited endpoint, media_publish), shows Authorization: Bearer only in POST examples, documents no permalink format, and does not say when app review or Advanced Access is required for accounts outside app roles. It does say “Extended JPEG formats such as MPO and JPS are not supported” (the outcome of submitting one is not described), and it still gives both 100 and 50 posts per 24 hours without reconciling them. Nothing above changes: those items stay unverified and fail closed, and the live content_publishing_limit stays authoritative. The IG User Media and Instagram Login pages were not re-read in this pass.
Re-checked 2026-10-15 with curl, every quote matched with grep against the fetched page (the docs/instagram-platform/... links now redirect to documentation/instagram-platform/...). Scopes, the Instagram Login versus Facebook Login comparison and the eligibility findings are in INSTAGRAM-SETUP.md.
- IG User Media: unchanged image rules, “Format: JPEG”, “File size: 8 MB maximum.”, “Aspect ratio: Must be within a 4:5 to 1.91:1 range”, widths 320 to 1440 scaled; caption “Maximum 2200 characters, 30 hashtags, and 20 @ tags.”;
alt_text“Alternative text, up to 1000 character, for an image.” (image posts only); “Containers expire after 24 hours”; “An Instagram account can only create 400 containers within a rolling 24 hour period”. Its Requirements table still lists only the Facebook Login permissions (instagram_basic,instagram_content_publish,pages_read_engagement) and Page tasks, so the Instagram Login question above stays open. ReachWeaver applies these limits to Instagram Login anyway, which can only make it stricter. - Content publishing: its Requirements table names, for Instagram Login, the Instagram User access token, host
graph.instagram.com, Business Login for Instagram, and the permissionsinstagram_business_basicandinstagram_business_content_publish, with Standard or Advanced Access. It still says both “Instagram accounts are limited to 100 API-published posts within a 24-hour moving period.” and, in the carousel section, “Accounts are limited to 50 published posts within a 24-hour period.” The content_publishing_limit reference still givesquota_total“(currently50)”. The live quota stays authoritative. - Instagram Login and get started: no Facebook Page needed;
account_type“Can beBusinessorMedia_Creator.”; dashboard tokens are valid for 60 days, login-flow tokens for 1 hour. The identity check and the 60-dayauthorization_expires_atguidance above match. - Overview: Standard Access is enough for your own professional account; Advanced Access, for accounts you do not own or manage, “requires App Review and Business Verification”. App calls are budgeted at “Calls within 24 hours = 4800 * Number of Impressions” per app and user pair.
Acceptance evidence for issue #50 (offline, mock-tested)
Section titled “Acceptance evidence for issue #50 (offline, mock-tested)”Every row is an offline test with mocked HTTP and synthetic fixtures; none is a live receipt. tests/test_instagram.py::IssueFiftyEvidenceTests::test_every_cited_test_exists fails if a test named here is removed or renamed.
| Criterion | Evidence |
|---|---|
| Setup guide maps professional-account prerequisites, scopes and eligibility | INSTAGRAM-SETUP.md: checklist, scopes, the dated Instagram Login versus Facebook Login comparison (token lifetimes, publishing permissions, chosen path), eligibility, and the manual path for personal or unsupported accounts. Pinned by tests/test_instagram.py::IssueFiftyEvidenceTests::test_setup_guide_maps_prerequisites_scopes_and_eligibility |
| Fixture: unsupported consumer account | tests/test_meta_provider_fixtures.py::InstagramFixtureTests::test_consumer_account_is_refused_before_any_write, tests/test_instagram.py::FlippedLibraryTests::test_wrong_account_or_personal_account_refused_before_any_write, tests/test_instagram.py::FlippedCliTests::test_wrong_or_consumer_account_blocked_before_claim |
| Fixture: missing permissions | tests/test_meta_provider_fixtures.py::InstagramFixtureTests::test_missing_permission_and_app_review_required_are_pre_write, tests/test_meta_provider_fixtures.py::InstagramFixtureTests::test_connection_check_reports_missing_permission_without_remote_text, tests/test_instagram.py::FlippedLibraryTests::test_missing_permission_hints_at_identity_and_quota_are_pre_write |
| Fixture: bad media | tests/test_meta_provider_fixtures.py::InstagramFixtureTests::test_bad_media_rejected_at_container_is_uncertain_and_never_retried, tests/test_instagram.py::RemoteMediaTests::test_image_rules, tests/test_instagram.py::RemoteMediaTests::test_url_rules, tests/test_instagram.py::RemoteMediaTests::test_download_refusals |
| Fixture: asynchronous processing | tests/test_meta_provider_fixtures.py::InstagramFixtureTests::test_async_container_processing_from_fixtures, tests/test_instagram.py::FlippedLibraryTests::test_in_progress_polls_once_per_minute_then_publishes, tests/test_instagram.py::FlippedLibraryTests::test_container_never_ready_is_uncertain_after_five_checks, tests/test_instagram.py::FlippedLibraryTests::test_container_error_or_already_published_is_uncertain |
| Account selection | tests/test_instagram.py::FlippedCliTests::test_post_is_sent_only_through_its_own_account, tests/test_instagram.py::FlippedCliTests::test_wrong_or_consumer_account_blocked_before_claim, tests/test_instagram.py::ConfigAndCredentialTests::test_invalid_configs_rejected |
| Approval | tests/test_instagram.py::BindingTests::test_media1_payload_binds_bytes_and_url_and_tamper_blocks_claim, tests/test_instagram.py::FlippedCliTests::test_unapproved_draft_dry_run_and_missing_live_gate_send_nothing, tests/test_instagram.py::FlippedCliTests::test_image_changed_after_approval_blocks_before_claim |
| Publish receipt | tests/test_instagram.py::FlippedCliTests::test_end_to_end_receipt_stored, tests/test_instagram.py::FlippedLibraryTests::test_receipt_contradicting_the_send_is_uncertain, tests/test_instagram.py::FlippedLibraryTests::test_permalink_unavailable_or_unrecognized_keeps_media_id |
| Failure recovery | tests/test_instagram.py::FlippedCliTests::test_post_write_failure_goes_to_needs_review_and_is_never_resent, tests/test_instagram.py::FlippedCliTests::test_needs_review_is_closed_by_reconcile_and_never_resent, tests/test_instagram.py::FlippedCliTests::test_preflight_429_records_cooldown_and_keeps_post_approved, tests/test_instagram.py::FlippedLibraryTests::test_emergency_pause_stops_before_media_publish, tests/test_instagram.py::FlippedLibraryTests::test_emergency_pause_after_container_finished_stops_before_media_publish |
| Duplicate safety | tests/test_instagram.py::FlippedCliTests::test_published_post_is_never_resent, tests/test_instagram.py::FlippedLibraryTests::test_post_write_http_errors_are_uncertain_and_never_retried |
| Stays disabled until the owner’s go | tests/test_instagram.py::UnreachableTests::test_bundled_registry_keeps_instagram_gated, tests/test_instagram.py::UnreachableTests::test_cli_live_publish_skips_instagram_and_keeps_post_approved |
| Manual-export fallback | tests/test_manual_package.py::DedicatedPackagesUnchanged::test_library_output_matches_golden_bytes (the Instagram package says any account you control can post it) |
| Media features and verification date in the matrix | Instagram row of PLATFORM-MATRIX.md and the registry’s rate_limits.accessed (2026-10-15), kept equal by tests/test_instagram.py::IssueFiftyEvidenceTests::test_matrix_row_carries_the_registry_verification_date |
Owner-only, not provable offline: the Meta app, the dashboard token, the supervised first post and its receipt, and the reviewed registry and policy.SUPPORTED_LIVE change that would enable the adapter.
Owner pilot runbook: INSTAGRAM-PILOT.md.
