RankParse

Proposals

Propose actions for a human to approve, list their status, and read one back with the proposals endpoints.

POST/v1/proposalsFree (proposing is free)API key

Proposals are how an agent asks for permission. You describe an action (submit URLs to IndexNow, send outreach emails), RankParse stores it as a pending item in the website owner's approval inbox, and nothing runs until a person approves it in the RankParse web app.

All four routes on this page cost 0 credits. Credits are spent only when an approved action runs: an IndexNow submission costs 1 credit at that moment, outreach costs 0.

Approval only happens in the web app. There is no API route to approve, skip, retry or execute a proposal, and an API key cannot be used to approve one. This is deliberate: a leaked key can only fill the inbox, never act on it.

Authentication and limits

  • Auth: X-API-Key header, the same key as every other endpoint. See Authentication.
  • Rate limit: 60 requests per minute per key. A 61st call in the window returns 429 with code: "rate_limited".
  • The workspace is always the one your key belongs to. A site_id or proposal id from another workspace returns 404, never data.
  • The routes on this page are not part of the secure OAuth connector's tool catalog today. See the MCP server page.

Create a proposal

POST /v1/proposals takes a JSON body with a kind plus that kind's fields.

Common fields

FieldTypeRequiredDescription
kindstringYesindexnow_submit or outreach_send. Any other value returns 400 invalid_proposal_input.
site_idstringDependsA website id from GET /v1/websites. Required to create a new outreach campaign; for IndexNow it is optional and, when given, must match the website that owns the URLs' host.
notestringNoWhy you are proposing this, in one or two plain sentences. Cleaned of control characters, cut to 1000 characters, and shown to the reviewer labelled as the agent's note.
client_request_idstringNo1 to 64 characters of A-Z a-z 0-9 _ -. Validated and otherwise ignored: duplicate detection is automatic (see Idempotency).

Unknown fields are rejected with 400 invalid_proposal_input. A workspace id can never be passed in the body.

Kind: indexnow_submit

Asks to notify Bing, Yandex, Seznam, Naver and other IndexNow engines about changed URLs.

FieldTypeRequiredDescription
urlsstring[]Yes1 to 1000 full http(s) URLs, each at most 2048 characters, no explicit port, all on one host. The host must be one of your RankParse websites. The stored list is de-duplicated and sorted, and its total size is capped at about 1.9 MB.

The host must already have an IndexNow key (see POST /v1/indexnow/key); otherwise the call returns 400 indexnow_key_not_found. Whether the key file is actually hosted is checked when the proposal runs, not when it is created.

Kind: outreach_send

Asks to send link-building emails from the workspace's connected Gmail account. Two shapes:

Inline contacts. RankParse creates (or reuses) a draft campaign and adds the contacts, then proposes sending them.

FieldTypeRequiredDescription
campaign_namestringYes1 to 200 characters. Reuses a live Gmail campaign with the same name on the same website.
contactsobject[]Yes1 to 100 contacts, one per domain. See the contact fields below.
site_idstringYesThe website the campaign belongs to.

Existing campaign. Propose the contacts a campaign already holds.

FieldTypeRequiredDescription
campaign_idstringYesA campaign you own. Its Gmail pipeline must be connected.
contactsobject[]NoOptional extra contacts (up to 100) to add before proposing. Omit to propose the campaign's current copy-ready contacts.

Contact fields: email (required, max 320), domain (required, the prospect domain, max 253), subject (required, max 500), body (required, plain text, max 10,000), name (optional, max 200), domain_authority (optional number; out-of-range values are stored as unknown).

An agent proposal snapshots at most 100 copy-ready contacts. Contacts that were already emailed in an earlier campaign are skipped, and a campaign holds at most 500 contacts.

Example

curl -X POST "https://api.rankparse.com/v1/proposals" \
  -H "X-API-Key: rp_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "indexnow_submit",
    "site_id": "site_abc123",
    "urls": ["https://example.com/blog/new-post", "https://example.com/pricing"],
    "note": "Published a new post and changed the pricing page."
  }'
import requests

res = requests.post(
    "https://api.rankparse.com/v1/proposals",
    headers={"X-API-Key": "rp_your_key"},
    json={
        "kind": "indexnow_submit",
        "site_id": "site_abc123",
        "urls": [
            "https://example.com/blog/new-post",
            "https://example.com/pricing",
        ],
        "note": "Published a new post and changed the pricing page.",
    },
)
print(res.status_code, res.json())
const res = await fetch("https://api.rankparse.com/v1/proposals", {
  method: "POST",
  headers: {
    "X-API-Key": "rp_your_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    kind: "indexnow_submit",
    site_id: "site_abc123",
    urls: ["https://example.com/blog/new-post", "https://example.com/pricing"],
    note: "Published a new post and changed the pricing page.",
  }),
});
console.log(res.status, await res.json());

Response

Most calls return 202 with a pending proposal:

{
  "status": "awaiting_approval",
  "proposal_id": "prp_V1StGXR8_Z5jdHi6B-myT",
  "review_url": "https://rankparse.com/dashboard/sites/site_abc123/today?proposal=prp_V1StGXR8_Z5jdHi6B-myT",
  "title": "Submit 2 URLs on example.com to IndexNow",
  "dedupe_hit": false,
  "message": "Waiting for the user's approval in RankParse: Submit 2 URLs on example.com to IndexNow. Nothing happens until they approve it. Tell the user to review it at the review_url, and check list_proposals later for the outcome."
}
FieldTypeDescription
statusstringawaiting_approval, approved or executed. See the table below.
proposal_idstringThe proposal's id, prp_ followed by url-safe characters. Use it with GET /v1/proposals/:id.
review_urlstringWhere the reviewer approves it. Show this link to the user. It contains only ids, never the proposal's content. A proposal without a website opens the workspace inbox instead.
titlestringBuilt by RankParse from the stored payload (counts and host), never from your text.
dedupe_hitbooleantrue when an identical open proposal already existed and was returned instead of creating a second one.
messagestringPlain-text guidance for an agent to relay to the user.

Possible outcomes

HTTPstatusMeaning
202awaiting_approvalThe proposal is pending. Nothing has happened. This is the normal result, and the only one for outreach.
202approvedThe same action was already approved and is running (dedupe_hit: true, with a review_url), or the owner's policy for this website allows IndexNow automatically and the run will be retried within the hour (the body then carries only status, proposal_id and message).
200executedIndexNow only: the owner's policy for this website is Automatic and the submission ran immediately. The body carries proposal_id and outcome (status, result, errorCode). The 1 credit is charged.

Outreach is never automatic. Whatever the website's settings say, an outreach_send proposal always waits for a person.

List proposals

GET/v1/proposalsFreeAPI key

Returns the workspace's proposals, newest first, including ones created by RankParse itself (for example "pages not indexed" suggestions).

ParameterTypeRequiredDefaultDescription
statusstringNoallOne of pending, approved, executing, succeeded, partially_succeeded, failed, skipped, expired, superseded, cancelled. Anything else returns 400 invalid_proposal_input.
site_idstringNoallOnly this website's proposals. A website of another workspace returns 404 site_not_found.
limitintegerNo251 to 100. Larger values are clamped to 100; invalid values fall back to 25.
curl "https://api.rankparse.com/v1/proposals?status=pending&site_id=site_abc123" \
  -H "X-API-Key: rp_your_key"
{
  "proposals": [
    {
      "id": "prp_V1StGXR8_Z5jdHi6B-myT",
      "kind": "indexnow_submit",
      "status": "pending",
      "title": "Submit 2 URLs on example.com to IndexNow",
      "site_id": "site_abc123",
      "source": "agent",
      "estimated_credits": 1,
      "created_at": 1790000000000,
      "expires_at": 1790604800000,
      "decided_at": null,
      "decided_via": null,
      "result": null,
      "error_code": null
    }
  ]
}

There is no pagination cursor. To see older items, filter by status or site_id.

Get one proposal

GET/v1/proposals/:idFreeAPI key

Returns { "proposal": { ... } } with the same fields as a list item. An unknown id, a malformed id, and another workspace's id all return the same 404 proposal_not_found.

curl "https://api.rankparse.com/v1/proposals/prp_V1StGXR8_Z5jdHi6B-myT" \
  -H "X-API-Key: rp_your_key"

Proposal fields

FieldTypeDescription
idstringProposal id (prp_...).
kindstringindexnow_submit or outreach_send for proposals an agent can create. RankParse can also create investigate_drop and outreach_sending_change proposals itself; you will see them in lists but cannot create them.
statusstringSee Statuses.
titlestringServer-built summary, for example Send 12 outreach emails to 12 domains.
site_idstring or nullThe website the proposal belongs to.
sourcestringagent (created through this API), system (created by RankParse) or user (a web button).
estimated_creditsintegerCredits the action will cost when it runs (1 for IndexNow, 0 for outreach).
created_at, expires_at, decided_atinteger or nullUnix time in milliseconds. decided_at is when it was approved or skipped.
decided_viastring or nullweb (a person approved it), policy (the website's Automatic setting) or system.
resultobject or nullOutcome details once it has run. IndexNow: host, url_count, key_location. Outreach: queued, suppressed, changed, already_queued, removed, changed_since_review.
error_codestring or nullWhy it failed, or null. See Failure codes.

The summary never contains the URLs, email addresses, subjects or bodies. Those stay in the proposal and are shown only to reviewers in the app.

Statuses

StatusMeaning
pendingWaiting for a person. Expires after 7 days (IndexNow) or 14 days (outreach).
approvedA person (or an Automatic policy) approved it and it is about to run or is queued for retry.
executingRunning now.
succeededFinished. result has the details.
partially_succeededOutreach only: fewer contacts were queued than the proposal held (some changed after review or were suppressed).
failedDid not run to completion. error_code says why. A person can retry it in the app.
skippedA person declined it.
expiredNobody decided before the expiry.
supersededReplaced by a newer proposal for the same campaign (outreach) before anyone decided.
cancelledA reviewer removed every email from an outreach proposal, leaving nothing to send.

Terminal statuses are retained for 400 days. See Approval inbox for retention details.

Idempotency

There is no idempotency key to manage. RankParse derives a dedupe key from the content:

  • IndexNow: the host plus the sorted, de-duplicated URL list.
  • Outreach: the campaign plus the exact set of contacts that would be sent.

Proposing the same content again while the first is still open (pending, approved or executing) returns the same proposal_id with dedupe_hit: true and does not count against your limits. Retrying after a network error is therefore always safe. If a pending outreach proposal for a campaign is re-proposed with different contacts, the old one becomes superseded and the new one replaces it.

Limits

LimitValueError
Pending proposals per website25429 too_many_pending_proposals
New proposals per account per UTC day100429 daily_proposal_limit_reached
URLs per IndexNow proposal1000400 invalid_proposal_input
Contacts per outreach proposal100400 invalid_proposal_input
Requests per minute60429 rate_limited

Expired pending proposals do not count toward the pending limit.

Errors

Errors use the standard shape, with error set to the same value as code:

{
  "error": "indexnow_host_not_a_website",
  "code": "indexnow_host_not_a_website",
  "message": "shop.example.org is not one of your RankParse websites"
}
HTTPCodeWhen
400invalid_proposal_inputBody is not a JSON object, kind is missing or unsupported, a field has the wrong type or is out of range, an unknown field was sent, site_id is missing where required, or the status filter is not a valid status.
400kind_not_availableThe kind exists but cannot be proposed yet.
400indexnow_invalid_urlA URL is not a valid public http(s) URL, or uses a non-default port.
400indexnow_too_many_urlsMore than 1000 URLs.
400indexnow_host_mismatchThe URLs span more than one host.
400indexnow_host_not_a_websiteThe URLs' host is not one of your RankParse websites.
400site_host_mismatchsite_id names a different website than the URLs' host.
400indexnow_key_not_foundThe host has no IndexNow key yet. Provision one first.
400gmail_not_connectedOutreach needs a connected Gmail account.
400sending_provider_unsupportedThe campaign does not send through Gmail.
400campaign_site_mismatchThe campaign belongs to a different website than site_id.
400no_contactsNo copy-ready contacts to propose, or none of the supplied contacts could be added.
409campaign_not_editableThe campaign is in a state that cannot take new contacts (for example a launch is running right now). Retry in a moment.
401invalid_api_keyMissing, invalid or revoked API key.
403approval_inbox_not_enabledThe approval inbox is not switched on for this account. Do not retry.
403action_disabled_by_policyThe owner turned this action Off for the website. Do not retry.
404site_not_foundThe website does not exist in this account, or is archived.
404campaign_not_foundThe campaign does not exist in this account.
404proposal_not_foundUnknown proposal id.
429too_many_pending_proposals, daily_proposal_limit_reached, rate_limitedSee Limits.

See also Error codes.

Failure codes

When a proposal ends failed (or partially_succeeded), error_code explains why:

CodeMeaning
payload_changed_since_approvalThe stored proposal no longer matches what was approved, so it did not run. Propose again.
approver_lost_permissionThe person who approved it no longer has permission to run it. Tell the user.
nothing_to_sendOutreach: every contact was already queued, sent or suppressed by the time it ran.
indexnow_host_not_a_websiteThe website was removed before the run.
indexnow_key_not_verifiedThe IndexNow key file is not reachable at its key_location.
insufficient_credits, spend_cap_reachedIndexNow could not be charged. The account balance or monthly spend cap blocked it.
indexnow_unavailable, indexnow_rate_limited, indexnow_key_rejected, indexnow_url_mismatch, indexnow_invalid_requestThe IndexNow service refused or could not take the submission. The credit is refunded.
execution_attempts_exhaustedThe run kept failing and was stopped after 5 attempts. Any charge is refunded.

A failed IndexNow run never keeps its credit.

MCP tools: propose_indexnow_submit, propose_outreach_send and list_proposals on the API-key MCP endpoint. See MCP server.

On this page