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.
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 scoped to the LinkedIn account(s) you want it to reach, with an optional spend cap. 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.
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."
}'
# 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 }, equivalent toPOST /v1/posts.check_balance— no arguments, equivalent toGET /v1/balance.
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 }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, 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.
- 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.