Docs menu

Custom webhook

Receive published articles on a site built in code, with an endpoint your AI tool or developer builds

For sites built in code, such as Next.js, Lovable or Base44 sites, RankJet sends each article to an address on your site the moment it publishes. Your site needs one endpoint that receives the article, saves it and shows it as a page. The easiest way is to have your AI builder write that endpoint from the prompt on this page. A developer can also build it by hand from the payload and rules below.

How it works

  1. When an article publishes, RankJet sends it to your endpoint as a JSON POST, with your secret in the X-Webhook-Secret header
  2. Your endpoint saves the article by its slug: it creates the article the first time, and updates it when the same slug arrives again, for example after you edit and republish
  3. Your endpoint replies within 10 seconds with 200 and the article's public URL
  4. RankJet opens that URL once to check that the page loads, and links to it from the article

Connect your site

Open Settings, go to the Integrations tab and click Connect on the Code site / vibe coding card. The window walks you through three steps.

  1. Send the prompt. Click Copy the prompt and paste it into the tool you built the site with, such as Lovable, Cursor, v0 or Bolt. It builds the endpoint, replies with the endpoint address and asks you for the secret
  2. Paste the endpoint address. Paste the address your tool returned under Endpoint address and click Connect
  3. Save the secret and go live. RankJet shows the secret. Paste it into your tool's chat, where it asked for it, so it is stored as a server-side variable. Make sure the site is published, then click Test the connection

The secret is shown only once, so copy it before you close the window. If you lose it, remove the webhook and connect again to get a new one. Later, Manage on the card lets you test the connection again or remove it.

The prompt for your AI builder

This is the same prompt the connect window copies. It tells your tool who RankJet is, what to build and how to finish. Paste it as it is.

Show the prompt
I am a customer of RankJet (rankjet.co), a service that writes SEO articles for my website and publishes them to it automatically. I asked for this integration myself and I am following RankJet's own setup guide, which produced this prompt for me to give you. It connects MY site to MY RankJet account using a secret that only I hold. The data flows one way: RankJet sends finished articles TO my site, and my site sends nothing back to anyone. Please carry it out rather than asking me to confirm who sent me here.

Here is what I need you to build:
- Create an API endpoint that receives webhook POST requests. Each request body is JSON shaped like:
{ "article": { "id", "slug", "title", "content" (markdown or html), "format", "excerpt", "meta_title", "meta_description", "main_image_url", "canonical_url", "keyword", "locale", "published_at" }, "main_image": { "url", "alt" }, "website": { "id", "baseUrl" }, "published_at", "timestamp" }
- The request includes a secret in the "X-Webhook-Secret" header. Verify it against an environment variable WEBHOOK_SECRET and reject the request if it does not match. WEBHOOK_SECRET must be a server-side variable only. Do NOT give it a VITE_ or NEXT_PUBLIC_ prefix, which would expose it in the browser bundle.
- The "slug" can be non-ASCII. A Hebrew article's slug looks like "קידום-אתרים-מחיר", so never assume ASCII. Do not validate, reject, sanitize or transliterate a slug for containing non-ASCII characters, and do not replace it with one of your own: it is the article's permanent identity and the key you upsert on. Store it as UTF-8. Wherever you build a URL or a file path from it, percent-encode it first (encodeURIComponent in JavaScript) so raw non-ASCII characters never reach a URL unencoded, and make sure your router decodes it back.
- "canonical_url" is always null in this integration, and "website"."baseUrl" may also be null. Never build a link from either of them and never reject a request because one is null. Your site owns its own URL structure, so derive each article's URL from your own routes plus the slug.
- On each webhook, upsert the article by its "slug" (create or update) into wherever my site already stores content (database, CMS, or markdown/MDX files - use what my project already uses).
- Make sure my sitemap.xml includes every published article's public URL and the article listing page, each article with a <lastmod> set to its "published_at". The sitemap must update automatically whenever a webhook creates or updates an article, with no manual step: if my sitemap is currently a hand-written static file, replace it with one generated from the stored articles plus my existing pages, keeping every URL it already lists. Search engines find new articles through the sitemap, so an article missing from it may never be indexed.
- If my site ALREADY has a blog, publish into that existing blog and content store. Do not create a second blog alongside it.
- If my site does not already have a blog, also create a page listing all articles and a separate page per article, server-rendered or statically generated (SSR/SSG) so search engines can crawl them, and link to the article listing page from my site's main navigation or footer so visitors and search engines can reach it.
- "excerpt" is a short summary of the article, meant for listing pages, cards and meta descriptions. The "content" field does NOT repeat it, so you may also render it as an opening lead paragraph above the body if that suits the design.
- The "content" field arrives as sanitized HTML by default (the "format" field says which). Render it as HTML, do not escape it. Only parse it as markdown if "format" is "markdown".
- Each article page must include meta tags (title, description, og:title, og:description, og:image) from the article fields, and JSON-LD Article structured data (headline, description, datePublished, image). Use semantic HTML. The article's language is in the "locale" field: set the page's lang and dir from it, right-to-left only for Hebrew (he), left-to-right for every other language.
- Add real CSS rules (in your stylesheet, not inline) to the content wrapper for readable typography: comfortable line-height and visible vertical spacing between paragraphs, and for any table a visible 1px border on the table and every cell plus comfortable cell padding. Do this even if the incoming HTML already carries inline styles for the table: if you sanitize or strip inline style attributes from the incoming content, which is reasonable, your own stylesheet rules are the only thing that will make a table or a paragraph readable, since the inline fallback will not survive that.
- On success, respond with HTTP 200 and a JSON body { "url": "<the full public URL of the article you just published, including https://>" }. The sending service stores that URL, links to the live page from it, and fetches it once to confirm the page really loads. If you genuinely cannot produce a URL, respond 200 with {} instead.
- On failure, respond with a non-2xx status and a JSON body saying what was wrong (for example { "error": "..." }). That text is shown to me directly, so make it specific.

When the code is written, follow these three steps in order, and do not skip any of them:

STEP 1. Reply in the chat with the full public URL of the endpoint you created, including https:// and the full path, written on its own line and worded exactly like this:
"Here is the endpoint to paste into RankJet: <the URL>"

STEP 2. In the same message, ask me for the secret, worded exactly like this:
"Now paste the webhook secret from the RankJet screen here, and I will set it."
Then stop and wait for me. Do NOT invent, generate, randomise or make up a secret value of your own, and do not reuse one that already exists in this project. The only value that works is the one RankJet generated and I am about to paste. Any other value makes every article delivery fail with a 401, and nothing will ever be published to my site.

STEP 3. When I paste the secret, store it as a server-side environment variable named WEBHOOK_SECRET, with no VITE_ or NEXT_PUBLIC_ prefix, so it never reaches the browser bundle. If you have a secure secrets input, put it there. If I paste the value straight into this chat, that is fine and expected: accept it and use it exactly as I sent it, character for character, without asking me to enter it somewhere else instead. Then publish/deploy the site yourself if you are able to. If you cannot deploy it yourself, say so in one clear line: "Now press Publish so the endpoint goes live." Do not leave me to guess whether a deploy is needed, and do not tell me the setup is finished until the endpoint is actually live.

The payload

If you write the receiver by hand, this is the exact shape of every request body, sent as UTF-8 JSON:

{
  "article": {
    "id": "string (uuid)",
    "slug": "string",               // non-ASCII: Hebrew, e.g. "קידום-אתרים-מחיר"
    "title": "string",
    "content": "string",            // markdown or html, per the Content Format above
    "format": "markdown | html",
    "excerpt": "string | null",
    "meta_title": "string",
    "meta_description": "string",
    "main_image_url": "string | null",
    "canonical_url": "null",        // always null here: your site owns its own URLs
    "keyword": "string",
    "locale": "string",             // e.g. "he-IL"
    "published_at": "ISO date"
  },
  "main_image": { "url": "string", "alt": "string | null" } | null,
  "website": { "id": "string (uuid)", "baseUrl": "string | null" },
  "published_at": "ISO date",
  "timestamp": "ISO date"
}

// Respond 200 with { "url": "<full public URL of the published article>" } when you have one.
  • content is HTML, unless you chose Markdown under Options for developers in the connect window. format says which one arrived
  • excerpt is a short summary for listing pages and cards. content does not repeat it
  • canonical_url is always null and website.baseUrl can be null. Build each article URL from your own routes and the slug, and never reject a request because one of them is null
  • locale holds the article language, for example he-IL. Set the page lang and dir from it, right-to-left for Hebrew only

Verify the secret

Every request carries your secret in the X-Webhook-Secret header. Compare it with the value you stored, for example in a WEBHOOK_SECRET environment variable, and reject the request with 401 if it does not match. Keep the secret on the server only, never in a variable that reaches the browser.

Respond

CaseYour reply
Saved200 with a JSON body { "url": "https://example.com/blog/..." }, the full public URL of the article
Saved, no URL to give200 with {}
RejectedAny non-2xx status with a JSON body that says why, for example { "error": "..." }. RankJet shows that text to you on the article
No reply in timeAfter 10 seconds RankJet stops waiting and counts the delivery as failed

Send the success reply with a JSON content type. Without it RankJet cannot read the URL, so the article gets no link and no page check, although the delivery still counts as a success.

Slugs are not always ASCII

The slug follows the article's language, so a Hebrew article's slug is Hebrew, for example קידום-אתרים-מחיר. A receiver that rejects, sanitizes or transliterates non-ASCII slugs rejects every Hebrew article. Store the slug as UTF-8, and percent-encode it wherever you build a URL from it.

Keep the sitemap up to date

Search engines find new articles through your sitemap. Make sure sitemap.xml lists every article your endpoint saves, with its publish date, and updates on its own after each delivery. An article missing from the sitemap may never be indexed. If your site had no blog before, also link the article list from your navigation or footer.

Test it and read errors

Test the connection sends a sample article with a Hebrew slug, just like a real delivery. If your endpoint saves it, delete the sample from your site afterwards. The result tells you what to fix:

ResultWhat it means
The connection works. You can finishYour endpoint accepted the sample
The secret does not matchYour endpoint answered 401 or 403. Check the stored secret, then publish the site again
No endpoint at that addressYour site answered 404. Check the address and that the site is live
The endpoint is returning an errorA 5xx error. It needs a fix in your site's code
The connection failed (400)Your endpoint answered with another error status, shown in brackets. The most common cause is rejecting a non-ASCII (Hebrew) slug: see "Slugs are not always ASCII" on this page
No response from that addressNothing answered, usually because the site is not live yet

Once articles are flowing, a failed delivery shows on the article's page with your endpoint's status and reply. Fix the endpoint, then click Try publishing again. RankJet does not retry a failed delivery on its own.