RankNest

Push Endpoint for Custom Sites

Implement one signed HTTPS route on your custom-coded site and RankNest pushes approved changes to it automatically, like the WordPress plugin.

A push endpoint gives a custom-coded site the same one-click Push to Site experience as WordPress. You implement one HTTPS route on the site, register its URL on the client's Custom-Coded Site connector, and RankNest delivers each approved change to that route within about five minutes. Your code decides how the change is applied: update a database row, edit a CMS entry, or anything else that changes the live page.

Without a push endpoint, a custom connector still works. Approved changes wait in the connector's queue for you (or an AI coding assistant) to apply through the CLI or MCP. The endpoint is the upgrade from "queued for pickup" to "applied automatically".

Where to find it

Open the client's Settings, go to the Integrations tab, and expand the Custom-Coded Site connector. The Automatic Push Endpoint section sits below the CLI instructions.

How to register your endpoint

  1. Deploy a route on the client's site that implements the contract below.
  2. Paste its full HTTPS URL into the Automatic Push Endpoint field and click Register.
  3. Copy the signing secret that appears. RankNest shows it once and stores it encrypted. Set it on the site as an environment variable, for example RANKNEST_PUSH_SECRET.
  4. Click Send test ping. When the route answers correctly, the section shows Verified and queued changes start flowing.

Warning: Registering again replaces the secret. The site must be updated with the new value or every push will be rejected.

The contract

RankNest sends POST requests to your route with a JSON body and two headers:

Header Value
X-RankNest-Timestamp Unix time in milliseconds
X-RankNest-Signature sha256=<hex HMAC-SHA256 of "timestamp.body">

Verify every request before acting on it:

  1. Reject when the timestamp is more than 5 minutes from your clock.
  2. Compute HMAC-SHA256(secret, timestamp + "." + rawBody) and compare it to the signature header with a timing-safe compare.
import crypto from "node:crypto";

function verify(secret: string, timestamp: string, signature: string, rawBody: string): boolean {
  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60_000) return false;
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Answer unauthenticated requests with 401 and an empty body.

Request types

Your route receives three body shapes, distinguished by type. Ignore any fields you do not recognize; new optional fields may appear over time.

ping, sent by the Send test ping button:

{ "type": "ping" }

Reply 200 with { "ok": true }.

add_link, sent for an approved internal link recommendation:

{
  "type": "add_link",
  "item_id": "9be0…",
  "source_url": "https://www.example.com/blog/some-post/",
  "target_url": "https://www.example.com/services/thing/",
  "anchor_text": "our thing service",
  "placement": {
    "original_paragraph": "<p>…the paragraph as crawled…</p>",
    "edited_paragraph": "<p>…the same paragraph with the new link…</p>"
  }
}

Find the page that serves source_url, locate original_paragraph in its content (match loosely: strip tags, decode entities, collapse whitespace), and add the link. Wrapping the first occurrence of anchor_text in the matched paragraph preserves your stored markup better than replacing the whole paragraph.

update_meta, sent when an SEO test variant goes live:

{
  "type": "update_meta",
  "item_id": "41c2…",
  "source_url": "https://www.example.com/blog/some-post/",
  "title": "New page title",
  "meta_description": "New meta description.",
  "h1": { "current": "Old heading", "new": "New heading" }
}

All three change fields are optional; apply the ones present. h1.current is the locator: change the heading only when it matches the live one.

Response shape

Reply 200 with JSON in one of two shapes.

Applied:

{
  "applied": true,
  "revision_id": "optional string you can use to find the change later",
  "previous": { "title": "Old title", "meta_description": "Old description" },
  "warning": "optional note, e.g. already_linked"
}

previous matters for update_meta: RankNest stores it so a test's original values can be restored later. Return the values that were live before your write.

Declined:

{
  "applied": false,
  "code": "unmanaged_url",
  "message": "That page is built from code, not managed content."
}

A decline is treated as final. The item is marked failed with your message, no retries, and the change stays available for manual pickup through the CLI or MCP. Useful codes: unmanaged_url (the URL is not content your route can edit), not_found (no page serves that URL), paragraph_not_found, anchor_not_found.

Retries

Non-2xx responses are retried up to 3 times when they look transient (429 or 5xx). Other statuses fail the item immediately. Make your route idempotent: applying the same item_id twice must be safe, since a timeout after a successful write leads to a retry.

Tips

  • Revalidate or purge any page cache after a write, or the worker's live-page verification (which fetches the public URL a few minutes later) reports the change as missing.
  • Keep the route fast. RankNest times out after 15 seconds.
  • Return unmanaged_url for pages whose copy lives in source code. Those changes stay in the queue view, and an AI coding assistant working in the repo can apply them through the MCP instead.

Troubleshooting

The test ping fails with a signature error. The site holds a different secret than RankNest. Register again to mint a fresh secret and update the site's environment variable.

Pushes complete but verification reports a mismatch. The write landed in your database, but the public page still serves cached HTML. Revalidate the page in your framework after every write.

Every push fails with unmanaged_url. The queued changes target pages your route does not manage. Apply those through the CLI or MCP and mark them completed.

Last updated 2026-08-14