# ContentReach — complete reference > This is the full machine-oriented spec. For the short version, see /llms.txt. For a > human setting up an account (not the calling agent), see /docs. ## What this is ContentReach posts finished content to LinkedIn on behalf of an already-authorized account. It does not generate, edit, approve, or store post history — the calling agent or its operator's own platform is the system of record. Direct LinkedIn integration (ContentReach's own registered LinkedIn Developer app), not a third-party unified social API. Scope (v1): LinkedIn personal profiles only. No company/organization pages (blocked on LinkedIn's own Marketing Developer Platform partner approval, not yet complete). No scheduling — every publish is immediate. No platforms other than LinkedIn. ## Auth Every request — REST or MCP — carries `Authorization: Bearer cr_live_...`. A key is issued by the account operator (a human, via the settings UI, never by an agent itself) and is permanently scoped at issue-time to one or more specific Linked Accounts, plus an optional spend cap in cents. A key that isn't scoped to the Linked Account a request targets returns `api_key.out_of_scope` (403) — this can't be worked around by anyone downstream of the key issuer. ## REST endpoints ### POST /v1/posts Publish a post immediately. Request body: ``` { "linked_account": "la_...", // required, string — the Linked Account's public_id "content": "..." // required, string, 1-3000 chars — the finished post text } ``` Success — 201: ``` { "id": "urn:li:share:...", // LinkedIn's own post URN "linked_account": "la_...", "balance_cents": 1975 // Operator's remaining Balance after this post } ``` Insufficient balance — 402: ``` { "error": { "code": "balance.insufficient", "message": "...", "retryable": true }, "balance_cents": 15, "topup": { "checkout_url": "/settings/balance" }, "x402": { ... } // present only if the Operator has x402 configured — // an agent with a compatible wallet can pay and retry // with no human in the loop } ``` Any other error — see Error codes below, same `{ error: {...} }` envelope, appropriate HTTP status per the table. Billing note: the post's price is deducted from Balance atomically with the LinkedIn publish attempt. If the LinkedIn call itself fails after Balance was already spent, the charge is refunded automatically — you are never billed for a post that didn't go out. A `content.rejected` or `content.invalid` rejection happens before any spend, so those never touch Balance either. ### GET /v1/balance No body. Returns `{ "balance_cents": 1975 }`. Lets an agent pre-empt a 402 rather than react to one — check before a batch of posts if you want to fail fast on insufficient funds instead of mid-batch. ## MCP Hosted endpoint: `https://contentreach.io/api/mcp` — Streamable HTTP transport, stateless (no session persists between calls; every request is independent, matching serverless hosting — don't rely on connection-scoped state). Same Bearer API key as REST, on every request. Local/stdio (for pointing a desktop MCP client at this during development): clone the repo, `npm run dev` inside `mcp-server/` with `CONTENTREACH_API_KEY` set. Same tool definitions, different transport — nothing is transport-specific. Tools registered: - `publish_post` — input `{ linked_account: string, content: string }`. Equivalent to `POST /v1/posts`, same success/error shapes, JSON-stringified in the tool result's text content. - `check_balance` — no input. Equivalent to `GET /v1/balance`. `revoke_key` is defined in source but intentionally not registered as a callable tool yet — whether an agent's own key should be able to manage keys at all is still an open product question. Key issuance and revocation are settings-UI-only (human) actions for now. ## Error codes Every error, both transports, one shape: `{ "error": { "code": "...", "message": "...", "retryable": boolean } }`. A code, once shipped, is never silently repurposed — a new failure mode gets a new code. | code | HTTP | meaning | retryable | |------------------------------------|------|--------------------------------------------------------------------------|-----------| | api_key.missing | 401 | No API key on the request. | no | | api_key.invalid | 401 | Key is invalid, revoked, or unknown. | no | | api_key.out_of_scope | 403 | Key isn't scoped to the requested Linked Account. | no | | api_key.spend_cap_exceeded | 402 | This key's own spend cap would be exceeded by this post. | no | | api_key.rate_limited | 429 | Too many requests for this key in the current 60s window (default: 60). | yes | | linked_account.session_expired | 409 | LinkedIn token expired or was revoked. Re-auth via settings — no agent-side fix exists. | no | | linked_account.not_found | 404 | `linked_account` doesn't match a connected account. | no | | balance.insufficient | 402 | Not enough Balance. Response includes `topup` and, if configured, `x402`.| yes | | content.rejected | 422 | Content failed the pre-publish spam/gibberish screen. | no | | content.invalid | 400 | Malformed request — missing field, empty content, or over 3000 chars. | no | | internal_error | 500 | Something broke on our end. | no | Note on `linked_account.session_expired`: LinkedIn's Consumer-tier OAuth issues no refresh token — an access token is valid for a fixed window and then must be re-authorized by the Operator through the settings UI. There is no agent-side or API-side path to silently renew it; retrying the same request will not help. ## Limits - Rate limit: 60 requests/minute per API key, default. Enforced durably server-side (a Postgres-backed counter, not per-process) — holds correctly regardless of which serverless instance handles a given request. - Content length: 3000 characters (LinkedIn's practical limit for organic posts). - Content screening: mechanical spam/gibberish heuristics only (a character repeated 30+ times in a row, more than 5 URLs in one post) — not a topic or toxicity filter. The Operator and calling agent are responsible for what's published. ## Pricing Pay-as-you-go per post, no subscription. Exact current rate: https://contentreach.io/#pricing — not duplicated here since rates may change without a corresponding update to this file. ## Full docs Human-facing walkthrough (account setup, not agent integration): https://contentreach.io/docs