Aautoranko

Getting started

Webhooks

Autoranko can send a signed JSON event to your server when a draft is ready for review, when an article goes live and when generation fails. No polling needed.

Set up an endpoint

  1. Open Dashboard → API, enter your endpoint's URL under Webhooks and click Add webhook. It receives all three events.
  2. Copy the signing secret (whsec_…) right away: it's shown once. Keep it with your other secrets; if you lose it, delete the endpoint and add it again.
  3. Deploy a handler that verifies the signature and answers with any 2xx within 10 seconds, then send it a test event.

Public URLs only

The URL must be a public http or https address. localhost, loopback and private network addresses are rejected, so a webhook can't be pointed at internal services. Use HTTPS in production. To develop locally, expose your handler through a tunnel and register the tunnel's public HTTPS URL.

An account can have up to 5 endpoints. Every endpoint receives events for every job on the account, whether the job was created through the API or the dashboard, immediately or on a schedule.

Events

EventSent when the job entersExtra keys in data
job.reviewreviewpublish_error
job.publishedpublishedwp_link
job.failedfailederror

job.review

A draft is ready for approval: generation finished and the job is waiting in review (because auto_publish was off or the site has no WordPress connection). It's also sent when a publish attempt fails and the job returns to review; data.publish_error is then true, and GET /jobs/{id} has the reason in error.

Added to data
  • publish_errorbooleanOptional
    Only on a job.review sent after a failed publish attempt: always true. The reason is in error from GET /jobs/{id}.
job.review payload
{
  "event": "job.review",
  "created_at": "2026-10-25T20:13:48.902Z",
  "data": {
    "id": "3",
    "site_id": "1",
    "topic": "How to winterize a sprinkler system before the first freeze",
    "target_keyword": "winterize sprinkler system",
    "status": "review",
    "article_title": "How to Winterize a Sprinkler System Before the First Freeze",
    "meta_description": "Learn how to winterize a sprinkler system before the first freeze, with steps for shutting off water, draining lines and protecting equipment.",
    "wp_post_id": null
  }
}

job.published

The article was published to WordPress, after POST /jobs/{id}/publish or automatically with auto_publish. data.wp_post_id is the new post's ID and data.wp_link its URL.

Added to data
  • wp_linkstring or nullOptional
    Only on job.published: the post URL WordPress returned, or null.
job.published payload
{
  "event": "job.published",
  "created_at": "2026-11-02T09:04:31.226Z",
  "data": {
    "id": "4",
    "site_id": "1",
    "topic": "Drip irrigation vs. sprinklers: which saves more water?",
    "target_keyword": "drip irrigation vs sprinklers",
    "status": "published",
    "article_title": "Drip Irrigation vs. Sprinklers: Which Saves More Water?",
    "meta_description": "Compare drip irrigation and sprinklers on water use, cost and upkeep to choose the right system for your lawn and garden.",
    "wp_post_id": "1542",
    "wp_link": "https://demo.autoranko.com/drip-irrigation-vs-sprinklers/"
  }
}

job.failed

Generation failed. data.error says what went wrong. The job doesn't count toward the plan's allowance.

Added to data
  • errorstringOptional
    Only on job.failed: why generation failed.
job.failed payload
{
  "event": "job.failed",
  "created_at": "2026-10-26T10:03:05.118Z",
  "data": {
    "id": "5",
    "site_id": "1",
    "topic": "How often should you water a new lawn?",
    "target_keyword": "how often to water new grass",
    "status": "failed",
    "article_title": null,
    "meta_description": null,
    "wp_post_id": null,
    "error": "Outline generation returned no result."
  }
}

Payload

Each event is a POST with a JSON body in this envelope. data is the job at the moment of the event, without the outline or article HTML: fetch GET /jobs/{id} when you need them.

Event bodyapplication/json
  • eventstring
    The event name (also sent in the X-Autoranko-Event header).

    One ofjob.reviewjob.publishedjob.failed

  • created_atstring (date-time)
    When the event was sent (UTC). A retried delivery repeats the same value.
  • dataobject
    The job at the moment of the event. Fetch GET /jobs/{id} for the outline and article HTML.
    data keys(11)JobEventData
    • idstring
      Job ID: a 64-bit integer serialized as a string.
    • site_idstring
      ID of the site the article is for.
    • topicstring
      The topic the article was requested for.
    • target_keywordstring or null
      The keyword the article targets, or null.
    • statusstring
      Where the job is in its lifecycle: pending → generating → review or publishing → published, or failed.

      One ofpendinggeneratingreviewpublishingpublishedfailed

    • article_titlestring or null
      Title chosen in the outline step; null until the draft is ready.
    • meta_descriptionstring or null
      SEO meta description (aimed at under 160 characters), also used as the WordPress excerpt; null until the draft is ready.
    • wp_post_idstring or null
      ID of the WordPress post once published (a 64-bit integer serialized as a string), otherwise null.
    • errorstringOptional
      Only on job.failed: why generation failed.
    • wp_linkstring or nullOptional
      Only on job.published: the post URL WordPress returned, or null.
    • publish_errorbooleanOptional
      Only on a job.review sent after a failed publish attempt: always true. The reason is in error from GET /jobs/{id}.
The examples on this page are pretty-printed for reading. The real body is compact JSON, with no whitespace, and the signature covers those exact bytes.

Headers

HeaderExampleDescription
Content-Typeapplication/jsonThe body is JSON.
User-AgentAutoranko-Webhooks/1.0Identifies Autoranko's webhook sender.
X-Autoranko-Eventjob.publishedThe event name, the same as event in the body.
X-Autoranko-Signaturesha256=c6c8d24a1eae8ac534944be6dffe67a2bc0ec3f43dc4abc37a00233fae2292f8HMAC-SHA256 of the raw body, hex-encoded and keyed with the endpoint's signing secret.

Verify signatures

Every delivery is signed with the endpoint's secret. Check the signature before trusting anything in the body: anyone can send a POST to a public URL.

Signature header
X-Autoranko-Signature: sha256=c6c8d24a1eae8ac534944be6dffe67a2bc0ec3f43dc4abc37a00233fae2292f8

The value is sha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with the endpoint's signing secret. To check it:

  1. Read the raw request body as bytes, before any JSON parsing. Re-serializing parsed JSON changes the bytes and breaks the signature.
  2. Compute an HMAC-SHA256 of those bytes, keyed with the endpoint's signing secret (the whole whsec_… string, prefix included), and hex-encode it in lowercase.
  3. Compare sha256= plus your hex digest with the X-Autoranko-Signature header using a constant-time comparison. Reject the request (for example with 401) if they differ.
  4. The signature doesn't cover a timestamp. To limit replays, also reject events whose created_at is more than a few minutes old, and skip deliveries you've already processed.
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.AUTORANKO_WEBHOOK_SECRET; // whsec_…

createServer((req, res) => {
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const raw = Buffer.concat(chunks); // the exact bytes, before any JSON parsing
    const expected = Buffer.from("sha256=" + createHmac("sha256", SECRET).update(raw).digest("hex"));
    const received = Buffer.from(req.headers["x-autoranko-signature"] ?? "");
    if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
      return res.writeHead(401).end();
    }
    const { event, data } = JSON.parse(raw);
    console.log(event, data.id, data.status); // hand off to a queue, then answer fast
    res.writeHead(200).end();
  });
}).listen(3000);

Each sample reads the secret from the AUTORANKO_WEBHOOK_SECRET environment variable, compares in constant time and answers 401 when the signature doesn't match.

Test your endpoint

A known-good input for testing a verifier. Signed with this secret, this exact body must produce this signature.

Secretwhsec_0123456789abcdef0123456789abcdef0123456789abcdef
Signaturesha256=c6c8d24a1eae8ac534944be6dffe67a2bc0ec3f43dc4abc37a00233fae2292f8
Body (exact bytes, compact)
{"event":"job.published","created_at":"2026-11-02T09:04:31.226Z","data":{"id":"4","site_id":"1","topic":"Drip irrigation vs. sprinklers: which saves more water?","target_keyword":"drip irrigation vs sprinklers","status":"published","article_title":"Drip Irrigation vs. Sprinklers: Which Saves More Water?","meta_description":"Compare drip irrigation and sprinklers on water use, cost and upkeep to choose the right system for your lawn and garden.","wp_post_id":"1542","wp_link":"https://demo.autoranko.com/drip-irrigation-vs-sprinklers/"}}

Run your handler with AUTORANKO_WEBHOOK_SECRET set to the test secret, then send it the test event. This command targets the Node.js sample on port 3000 (http://localhost:3000/webhooks/autoranko); change the URL for yours. Expect a 2xx; change any character of the body and expect 401.

Shell
curl -i -X POST http://localhost:3000/webhooks/autoranko \
  -H "Content-Type: application/json" \
  -H "X-Autoranko-Event: job.published" \
  -H "X-Autoranko-Signature: sha256=c6c8d24a1eae8ac534944be6dffe67a2bc0ec3f43dc4abc37a00233fae2292f8" \
  --data-binary '{"event":"job.published","created_at":"2026-11-02T09:04:31.226Z","data":{"id":"4","site_id":"1","topic":"Drip irrigation vs. sprinklers: which saves more water?","target_keyword":"drip irrigation vs sprinklers","status":"published","article_title":"Drip Irrigation vs. Sprinklers: Which Saves More Water?","meta_description":"Compare drip irrigation and sprinklers on water use, cost and upkeep to choose the right system for your lawn and garden.","wp_post_id":"1542","wp_link":"https://demo.autoranko.com/drip-irrigation-vs-sprinklers/"}}'
The test secret is public. Use it only on your own machine, and run in production with the endpoint's secret from Dashboard → API.

Delivery and retries

MethodPOST
Timeout10 seconds per attempt
Retries1 more attempt, 2 seconds later, after a network error, a timeout or a 5xx
Not retriedAny other response, including 3xx and 4xx
SuccessAny 2xx
EndpointsUp to 5 per account
  • Add endpoints in Dashboard → API: up to 5 per account, each subscribed to any of job.review, job.published and job.failed (all three by default). The URL must be a public http or https address; localhost and private network addresses are rejected. Use HTTPS in production.
  • When adding an endpoint you get its signing secret (whsec_ followed by 48 hexadecimal characters). It's shown once; if you lose it, delete the endpoint and add it again.
  • Webhooks fire for every job on your account, whether it was created through the API or the dashboard, immediately or on a schedule.
  • Each event is sent as a POST with a JSON body to every active endpoint subscribed to it. Each attempt times out after 10 seconds.
  • A network error, a timeout or a 5xx response is retried once, 2 seconds later. Other responses, including 4xx, aren't retried. Answer with any 2xx quickly and do slow work asynchronously.
  • Dashboard → API shows each endpoint's last delivery: the HTTP status of the final attempt (empty when no response was received) and when it happened.
  • Delivery is best-effort: there's no queue and no later redelivery. If your endpoint was down, catch up by listing jobs with GET /sites/{id}/jobs.
  • A retry can deliver the same event twice (for example when your server processed it but answered after the timeout). Make handlers idempotent: a retried delivery has the same event, data.id and created_at.
  • Point the webhook at its final URL. A 301 or 302 redirect turns the POST into a GET without the body.