Documentation
This page is for the human setting things up. If you're an agent, or building integration code, read llms.txt (quick reference) or llms-full.txt (complete spec) instead — same information, machine-oriented format.
Add this to your project
Paste this into your coding agent (Claude Code, Cursor, whatever you use) inside the project you want posting LinkedIn from. It reads the current spec itself, asks you for credentials if you don't have them yet, and wires up whatever fits your codebase — nothing to copy from the sections below unless you want the detail.
Add LinkedIn posting to this project using ContentReach (https://contentreach.io) — a pay-per-post LinkedIn API, no subscription, text posts (up to 3000 characters) optionally with one image.
1. Fetch https://contentreach.io/llms-full.txt and read it — that's the complete, current API spec (auth, endpoints, error codes, an MCP option).
2. Ask me for my ContentReach API key and Linked Account ID. If I don't have them yet, tell me to sign in at contentreach.io/settings, connect LinkedIn, and generate a key — it's shown once, at creation.
3. Store them the same way this project already stores other secrets — ask me if you're not sure how.
4. Build whatever fits this codebase: a button in an existing UI, a CLI command, a webhook — your judgment. It should call POST https://contentreach.io/api/v1/posts with the API key as a Bearer token, the finished post text, and — if this project already has an image ready for the post (e.g. a generated cover image with a public URL) — an image_url field. Don't build an image upload/generation flow just to fill this in; only pass it if one already exists here.
5. Handle the response: 201 is success. 402 means insufficient Balance — surface the top-up link from the response. Anything else, show the error message — every error follows { error: { code, message, retryable } }.
No video or carousel support yet — text and at most one image only.Getting started
- 1
Sign in
Go to /auth/sign-in with your email — you'll get a one-time link, no password to set.
- 2
Connect LinkedIn
From /settings, click “Connect LinkedIn” and authorize on LinkedIn's own consent screen. This is the account posts will publish to.
- 3
Add Balance
Top up via Stripe when it's live, or use the manual test-balance field in settings while it isn't. Posts are prepaid — no subscription.
- 4
Generate an API key
From settings, create a key — name it, scope it to the LinkedIn account(s) you want it to reach, with an optional spend cap and rate limit. The full key is shown exactly once — save it now.
- 5
Give the key to your agent
Pass it as a Bearer token over REST, or as CONTENTREACH_API_KEY to the MCP server. Same key works for both.
Publish a post
One call, whichever transport your agent speaks. Both are capability-equivalent — nothing is REST-only or MCP-only.image_url is optional — omit it for a text-only post.
REST
curl https://contentreach.io/api/v1/posts \
-H "Authorization: Bearer cr_live_..." \
-H "Content-Type: application/json" \
-d '{
"linked_account": "la_9f2...",
"content": "Finished text, ready to publish.",
"image_url": "https://example.com/cover.jpg"
}'
# 201
{
"id": "urn:li:share:...",
"linked_account": "la_9f2...",
"balance_cents": 1975
}MCP
The hosted endpoint is https://contentreach.io/api/mcp (Streamable HTTP, stateless). Send your API key as a Bearer token on every request, exactly like REST. Two tools are registered:
publish_post—{ linked_account, content, image_url? }, equivalent toPOST /v1/posts.check_balance— no arguments, equivalent toGET /v1/balance.get_profile— no arguments, equivalent toGET /v1/profile.update_profile—{ display_name?, company_name? }, equivalent toPATCH /v1/profile.
For local development against a client like Claude Desktop, the source in mcp-server/ also runs standalone over stdio (npm run dev, with CONTENTREACH_API_KEY set) — same tools, same code, different transport.
Check balance
Pre-empt a 402 rather than react to one:
curl https://contentreach.io/api/v1/balance \
-H "Authorization: Bearer cr_live_..."
# 200
{ "balance_cents": 1975 }Profile
Name, company, and email — readable and writable the same way whether it's you in settings or your agent over the API. Only fields you send in a PATCH change; omit a field to leave it as-is, or send an empty string to clear it.
curl https://contentreach.io/api/v1/profile \
-H "Authorization: Bearer cr_live_..."
# 200
{ "display_name": "James Cannan", "company_name": null, "email": "you@example.com" }
curl -X PATCH https://contentreach.io/api/v1/profile \
-H "Authorization: Bearer cr_live_..." \
-H "Content-Type: application/json" \
-d '{ "company_name": "Unique Interactions" }'Error codes
Every failure — REST or MCP — is { error: { code, message, retryable } }. Codes are stable; a code, once shipped, is never repurposed.
| 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 | Key has hit its own spend cap. | No |
| api_key.rate_limited | 429 | Too many requests for this key in the current window. | Yes |
| linked_account.session_expired | 409 | LinkedIn token expired or was revoked — reconnect via settings. | No |
| linked_account.not_found | 404 | linked_account ID doesn't match a connected account. | No |
| balance.insufficient | 402 | Not enough Balance for this post — top up and retry. | Yes |
| content.rejected | 422 | Content failed the pre-publish spam/gibberish check. | No |
| content.invalid | 400 | Request body is malformed (missing field, over length limit). | No |
| internal_error | 500 | Something broke on our end. | No |
Limits
- Rate limit: 60 requests/minute per API key by default, set per-key at creation. Enforced durably (holds across every instance, not per-process). A 429 includes
retryable: trueand how long to wait. - Content length: 3,000 characters, LinkedIn's own practical limit for organic posts.
- Images: optional, one per post, JPEG/PNG/GIF, fetched from a public
image_urlyou provide — ContentReach doesn't generate or host images itself. No video or carousel yet. - Content screening: posts are checked for mechanical spam/gibberish (long runs of a repeated character, more than 5 URLs) before they're charged for. This isn't a topic or toxicity filter — you and your agent are responsible for what gets posted.
- Scope: LinkedIn personal profiles only, v1. Company/organization pages need LinkedIn's own Marketing Developer Platform approval, which isn't complete yet.
Pricing lives on the landing page — not duplicated here, since it's the one place the rate won't go stale in two locations.