Linkio API for account callers
The contract for a script or an agent that calls Linkio with an account API key. Every list, code, field
and number on this page is checked against the code on every build.
The routes a key can call: http-routes-for-accounts.md. The MCP tools: mcp-tools-for-accounts.md.
Prices and the receipt in detail: self-serve-billing-api.md. A five-minute walk: first-job-in-5-minutes.md.
1. Authenticate
A key starts with acc_. Send it in one of two headers; both are read the same way:
X-API-Key: acc_...
Authorization: Bearer acc_...
Create a key on the API keys page, or with POST /api/account/api-keys from an account sign-in; a key
cannot mint keys (an acc_ key gets 403 owner_only). Body: name (required), scopes (required, from
the list below), clientId (optional: the key sees one brand only), description, expiresAt. The key is
shown once. At most 10 active keys per account; DELETE /api/account/api-keys revokes one (a key holding
client-config:write may revoke its account's keys). No account yet? Section 9 creates one from the API.
Base URL: https://www.linkio.com. Answers are JSON. A missing or revoked key answers 401.
2. Scopes
A scope is namespace:action; the actions are read, write and purchase. A route names the scope it
needs; a key without it answers 403 with error insufficient_scope, code SCOPE_DENIED and
required_scope. An account sign-in holds every scope; a key holds only the ones it was created with.
Two key types exist. A self-serve account's key may hold any of these 34 scopes:
orders:read, orders:write, line-items:read, line-items:write, websites:read, workflows:read,
workflows:write, topic-gen:read, topic-gen:write, article-gen:read, article-gen:write,
billing:read, billing:write, search-queries:read, search-queries:write, bulk-analysis:write,
client-config:read, client-config:write, collaborative-placements:read, citation:read,
citation:write, change-requests:read, change-requests:write, opportunities:read,
opportunities:write, account-campaign:read, account-campaign:write, llm-tracking:read,
llm-tracking:write, asset-bank:read, asset-bank:write, crm:read, crm:write, credits:purchase.
A managed account's key may hold these 11 read scopes: orders:read, line-items:read, workflows:read,
billing:read, citation:read, collaborative-placements:read, client-config:read,
search-queries:read, crm:read, opportunities:read, asset-bank:read. A managed key on a
self-serve-only route (site search, for one) answers 403 ACCOUNT_TYPE_DENIED.
billing:write changes the account's own spending limits and card. The keys page's Full preset sends
every scope on the list, billing:write included; for an agent, create a key with the Read-only preset or
pick the scopes by hand.
| Namespace | What the scope covers |
|---|---|
orders | View and manage guest post orders |
line-items | View and manage individual line items within orders |
websites | Browse available publisher websites |
workflows | View and manage article workflows |
topic-gen | Generate topics and keywords for articles |
article-gen | Generate and manage AI-written articles |
billing | View credit balance and billing history; write changes your own spending limits and card |
search-queries | Import and manage search query research |
bulk-analysis | Run bulk domain analysis |
client-config | View and update client settings, target pages, provider keys and API keys |
collaborative-placements | View collaborative placement opportunities |
citation | View AI citation and ranking data |
change-requests | Reserved, no tool yet |
opportunities | View and manage link exchange opportunities and partner pages |
account-campaign | Your campaigns over HTTP (/api/account/campaigns); no MCP tool yet |
llm-tracking | Track where AI assistants mention and cite your brand (AI Mentions) |
asset-bank | Manage the pages and sites you offer in link exchanges |
crm | View CRM and communication data |
credits | Buy prepaid credit (purchase only) |
3. Refusals
| Status | code | Meaning | What to do |
|---|---|---|---|
401 | No key, or a revoked or expired key | Create a key | |
403 | SCOPE_DENIED | The key lacks required_scope | Create a key with that scope |
403 | ACCOUNT_TYPE_DENIED | A managed key on a self-serve-only route | Nothing: the route is not for managed accounts |
429 | DAILY_READ_CAP | The key passed 5000 reads per key per UTC day | Wait for resetsAt; Retry-After is set |
429 | RATE_LIMIT_EXCEEDED | MCP only: 60 calls per minute with a burst of 20 | Wait Retry-After seconds |
402 | one of six, below | The account cannot fund this start | Read the body: it names the way out |
4. The 402 bodies
A route that spends checks money before it works. A refusal is a 402 with one of three JSON shapes.
The code field tells them apart; error is a plain sentence for a person; estimateCents is what the
start would have held.
Budget: budget_exceeded or budget_paused
{ error, code, scope, feature, apiKeyId, period, limitCents, spentCents, heldCents, estimateCents,
remainingCents, affordableCount?, resetsAt, approvalId: null,
raise: { canSelfRaise, maxSelfServeCents, settingsUrl, api, requestIncreaseApi } }
scope says which limit stopped the start (the account's month, a feature, this key's day or month).
raise.api is PUT /api/account/usage-limits (needs billing:write); when canSelfRaise is false the
budget is at maxSelfServeCents and requestIncreaseApi asks Linkio for more. affordableCount is how
many units the remaining budget would fund, when the start was for several.
Approval: needs_approval
{ error, code, scope, feature, apiKeyId, estimateCents, askOverCents, approvalId, expiresAt, refused?,
approve: { settingsUrl, waitingUrl, api, header, listApi, tool } }
The job costs more than the account's "ask me before any job over" amount (askOverCents). The
decision is the account owner's. A key that holds billing:write may record it: approve.api is
POST /api/account/usage-limits/approvals/<approvalId> with body {"decision":"approve"} or
{"decision":"deny"}; the MCP tool is usage_approvals_decide (approve.tool), marked destructive so a
client asks the person at the keyboard first. Answers: 404 for an unknown or another account's id,
409 when already decided, 410 when expired. A key without billing:write hands the owner
approve.waitingUrl (the Waiting view of the activity page) or approve.settingsUrl; listApi
(GET /api/account/usage-limits/approvals) reads the state. Once approved, repeat the same call;
optionally name the approval in the header X-Linkio-Approval (approve.header), with the
approvalId as its value. The approval is matched to the retry by account, feature, key and jobRef
(the batch, workflow or order the start names), not by the exact estimate, so a retry that costs a little
more still runs. refused is set when a previous decision was a denial.
Card: card_required, payment_failed or credit_line
{ error, code, reason, feature, estimateCents, prepaidCents, cardStatus, unbilledCents, creditLineCents,
pausedAt, card: { settingsUrl, api, statusApi, setupLinkApi, payNowApi } }
Prepaid balance does not cover the start and no card can. card_required: no card (or the card was
removed); payment_failed: the last charge failed and paid work is paused until the card changes;
credit_line: unbilled usage is at creditLineCents and is charged shortly. Adding a card is the
owner's click: card.setupLinkApi (billing:write) returns a Stripe-hosted page to hand to the owner;
card.statusApi reads the card; card.payNowApi (billing:write) charges the unbilled balance now.
The convert hint
When the account holds placement credit (link_building) that the meter cannot spend, the card body
(or a route's older Insufficient credits body) adds convertibleCents and a convert object with
route /api/account/credits/convert, tool credits_convert, rate 1:1, from link_building
and to general. POST /api/account/credits/convert (billing:write) moves that amount into
spendable credit at 1:1.
5. The receipt
GET /api/account/usage/receipt (needs billing:read) answers where the account stands:
accountId, period, asOf, spentCents, heldCents, budgetCents, budgetRemainingCents,
resetsAt, prepaidCents, unbilledCents, cardStatus, cardHeadroomCents, canStartPaidWork,
feesCents. canStartPaidWork is the one bit to read before a paid call.
The same object (without accountId and asOf) comes back without a second call:
- API: on every
2xxanswer of a route marked "receipt" inhttp-routes-for-accounts.md, in the headerX-Linkio-Usage-Receipt(JSON), withX-Linkio-Usage-Receipt-Urlnaming the route above. Only for a key that holdsbilling:read. - MCP: as
usageReceipton every write tool's result.
6. Prices and the estimate
Prices are not on this page. GET /api/pricing/self-serve (public) returns the per-job fees, the
markup on provider cost when a job runs on Linkio's keys, and the publisher price range; the per-job
rules are in self-serve-billing-api.md. Before a paid start,
GET /api/account/usage/estimate?feature=<article|topic_generation|target_page_ai|ai_find_sites|website_search|site_metrics>&count=N
(billing:read; MCP usage_estimate) answers the exact hold the start would take. Every API or MCP
write call carries a per-call fee; reads are free and capped as in section 3.
7. Idempotency and retries
There is no idempotency key: no route an account can call reads an Idempotency-Key header. A call that starts work
(an article, a topic session, a site search, a batch) answers with a run, batch or job id; the caller
watches that id through its status route or tool and never starts the work again. A step re-run after
a reset is a new job and is charged again. The approval retry (section 4) is the one place a repeated
call is expected, and it is matched by jobRef, not by a header you invent.
The HTTP API has no rate limit of its own beyond the daily read cap; the MCP server rate-limits per key
(section 3). A 429 carries Retry-After in seconds.
8. Connect an agent
MCP endpoint: https://mcp.linkio.com/mcp, header Authorization: Bearer <your key>. The server lists
only the tools the key's scopes allow (mcp-tools-for-accounts.md), answers usageReceipt on every
write, and serves the agent rules as its instructions.
Connectors that sign in instead of pasting a key (Claude.ai, ChatGPT) use OAuth 2.1:
/.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource publish the
endpoints; POST /api/oauth/register registers the client (RFC 7591, no credentials needed);
the authorize step is PKCE (S256); /api/oauth/token supports authorization_code and
refresh_token. There is no client_credentials grant: OAuth signs an existing account in, it cannot
create an account. The access token is a JWT and travels only as Authorization: Bearer.
9. Sign up as an agent
An agent can create the account without a browser and without a key. Two public routes; the person who owns the mailbox proves it once by reading one mail. The browser signup form keeps its captcha; these routes stand behind the limits in the table below instead. Accounts that Linkio staff create follow their own path; the gate covers these two public routes.
- Register with
POST /api/signup:
curl -s -X POST https://www.linkio.com/api/signup -H 'content-type: application/json' \
-d '{"email":"owner@example.com","password":"<8+ chars>","name":"Dana","website":"https://example.com"}'
202 {"status":"verification_sent"} is the one success answer, the same for a new, a pending or an already
registered email (the body never says which). password needs at least 8 characters; name is required;
website and company are optional strings. Refusals: 400 INVALID_REQUEST (not JSON, or email, password
or name missing), 400 PASSWORD_TOO_SHORT, 400 EMAIL_REJECTED (the address does not parse, its domain has
no MX record, or it is a disposable domain), 429 RATE_LIMITED with a Retry-After header (a limit in the
table), 409 IDENTITY_PASSWORD_LOCKED (the email is a login of another kind with another password). A pending
address registered again gets the same code mailed once more (once a minute, 5 a day). No mail within 10 minutes
means the email is already registered and verified: sign in on the web.
-
The mail "Verify Your Email Address - Linkio" carries the link for a person and, on its own line, the sentence "Did an AI agent create this account for you? Give it this verification code:" followed by the code. The person gives the code to the agent.
-
Verify and receive the first key with
POST /api/signup/verify:
curl -s -X POST https://www.linkio.com/api/signup/verify -H 'content-type: application/json' -d '{"token":"<code>"}'
201 { accountId, apiKey, keyPrefix, scopes, expiresAt: null, next: { receipt, providerKeys, addMoney, pricing } }.
apiKey is the acc_ key, shown once. Refusals: 410 TOKEN_USED (the code was already used through this
route; the account is verified, use the key it returned; an account blocked while pending answers this too),
400 TOKEN_EXPIRED (older than 24 hours: register the same email again and a fresh mail with the code is sent),
404 TOKEN_UNKNOWN (no pending account created through this route carries the code; if the person clicked
the link first, the code answers 404: the account is verified, sign in on the web and create a key on
/account/api-keys), 400 INVALID_REQUEST (no token).
- What the first key can do: it holds 33 scopes, the self-serve list without
billing:write, so it reads and works but cannot change the account's spending limits or card, cannot decide approvals and cannot mint keys (403owner_only; the owner adds keys on/account/api-keys, and a key for an account whose email is not verified is refused with403email_not_verified). Nothing is spendable until the account has its own provider keys (both live, which also grants the free start described inself-serve-billing-api.md) or a card:GET /api/account/usage/receiptanswerscanStartPaidWorkfalse until then.nextnames the four calls that follow.
| Limit | Value | Answer |
|---|---|---|
| Per IP | 2 per hour | 429 RATE_LIMITED |
| Per new email | 1 per day | 429 RATE_LIMITED |
| Per email domain | 5 per day | 429 RATE_LIMITED; shared mail providers such as gmail.com are exempt |
| Verification mail resent | once a minute, 5 per day | 429 RATE_LIMITED |
| New emails, all callers together | 60 per hour | 429 RATE_LIMITED |
| Code lifetime | 24 hours | 400 TOKEN_EXPIRED |
When the hourly cap is reached a new email answers 429 while an already registered one still answers 202;
the cap is an outbound-mail bound, not a secrecy promise. The limits count per server process.