Limited time offer: Get 6 months free on yearly plans (50% off).

0

Days

10

Hours

30

Mins

9

Secs

LoginGet Started
Back to Help Centre

Webhooks

A webhook turns every article you publish in Junia AI into an HTTP request to an address you choose. Whatever sits at that address (your own site, a headless CMS, or a Zapier, Make or n8n workflow) receives the finished article as JSON and decides what to do with it.

At a glance

  1. You publish an article to your webhook, by hand, on a schedule, or from an Autoblog campaign.
  2. Junia sends a POST request with the article as JSON.
  3. Your endpoint checks that the request came from Junia, saves the article and replies with a 2xx status.
  4. If you publish the same article again later, it arrives with is_update: true so you can update it instead of creating a copy.

The no-code route: Zapier, Make or n8n

You don't need to write any code to use webhooks.

  1. In your automation tool, start a new workflow with a webhook trigger (in Zapier it's called Catch Hook) and copy the URL it gives you.
  2. In Junia, open Integrations → Publish via Webhook → Connect.
  3. Paste the URL and set Security to Shared token.
  4. Click Send Test. Your automation tool now knows every field Junia sends, so you can map title, content_html and the rest into the next step.
  5. Click Save Webhook.

Building your own endpoint

Step 1: add the webhook in Junia

Open Integrations → Publish via Webhook → Connect and fill in:

SettingWhat to enter
Integration NameAnything that helps you recognise it, e.g. "Production blog".
Webhook URLA public http(s) address that accepts POST requests. Local and private network addresses are rejected.
SecuritySigned requests (recommended) or Shared token, see Step 2.
Secret KeyGenerated for you. Copy it into your server's configuration, e.g. an environment variable called JUNIA_WEBHOOK_SECRET. You can regenerate it at any time.

Step 2: check that requests really come from Junia

Your endpoint is public, so it should ignore requests that don't carry your secret.

Signed requestsShared token
HeaderX-Junia-SignatureAuthorization: Bearer <secret>
What your endpoint doesRecalculates an HMAC SHA-256 of the request body with your secret and comparesCompares the header with your secret
The secret travels with the requestNoYes
Also proves the body wasn't changedYesNo
Best forYour own codeNo-code tools

Every request also carries Content-Type: application/json, User-Agent: Junia-Webhooks/1.0 and a unique X-Junia-Delivery id you can log.

About signed requests: the signature covers the exact bytes Junia sent, so calculate it from the unparsed request body. Parsing the JSON and turning it back into a string can change spacing or escaping, and the signature would no longer match. The examples below read the raw body first and then parse it.

Next.js (App Router)

// app/api/junia/route.js
import crypto from "crypto";

export async function POST(req) {
  const body = await req.text();
  const signature = crypto
    .createHmac("sha256", process.env.JUNIA_WEBHOOK_SECRET)
    .update(body)
    .digest("hex");

  if (req.headers.get("x-junia-signature") !== signature) {
    return new Response("Forbidden", { status: 403 });
  }

  const article = JSON.parse(body);
  if (!article.test) {
    await saveArticle(article); // see "Keeping articles in sync"
  }
  return Response.json({ ok: true });
}

Express

import crypto from "crypto";
import express from "express";

const app = express();

// Keep the raw bytes for the signature check; req.body is still parsed JSON.
app.use(express.json({ verify: (req, res, buf) => (req.rawBody = buf) }));

app.post("/junia", async (req, res) => {
  const signature = crypto
    .createHmac("sha256", process.env.JUNIA_WEBHOOK_SECRET)
    .update(req.rawBody)
    .digest("hex");

  if (req.get("x-junia-signature") !== signature) {
    return res.sendStatus(403);
  }

  if (!req.body.test) {
    await saveArticle(req.body);
  }
  res.json({ ok: true });
});

For a constant-time comparison, use crypto.timingSafeEqual instead of !==.

PHP

<?php
$body = file_get_contents('php://input');
$signature = hash_hmac('sha256', $body, getenv('JUNIA_WEBHOOK_SECRET'));

if (!hash_equals($signature, $_SERVER['HTTP_X_JUNIA_SIGNATURE'] ?? '')) {
    http_response_code(403);
    exit;
}

$article = json_decode($body, true);
if (empty($article['test'])) {
    save_article($article);
}

header('Content-Type: application/json');
echo json_encode(['ok' => true]);

Using a shared token instead

Replace the signature check with a header comparison. Your framework's parsed body can be used directly.

if (req.headers.authorization !== `Bearer ${process.env.JUNIA_WEBHOOK_SECRET}`) {
  return res.sendStatus(403);
}
if (!hash_equals('Bearer ' . getenv('JUNIA_WEBHOOK_SECRET'), $_SERVER['HTTP_AUTHORIZATION'] ?? '')) {
    http_response_code(403);
    exit;
}

Step 3: send a test

Before (or after) saving, click Send Test in the webhook settings. Junia posts a sample article with "test": true and shows you the status code, the response time and whatever your endpoint replied. Skip saving when test is true, as in the examples above.

Step 4: save and publish

Click Save Webhook. Your webhook now appears everywhere you can publish: the Publish menu in the editor, bulk generation and Autoblog campaigns.

Keeping articles in sync

Every article keeps the same id and slug for as long as it is sent to the same webhook. That lets you treat each delivery as "here is the latest version of this article" rather than "here is a new article".

When you'll receive an update

  • You edit an article in Junia, open Publish → Webhook and click Update Article.
  • An article that was already delivered is published to the same webhook again, e.g. from a schedule.

Updates arrive with "is_update": true. The title, content, images and meta fields reflect your latest edits, and published_url contains the URL your endpoint returned the first time (see "Responding to Junia").

How to store articles

  1. Keep Junia's id next to each article you save (for example in a junia_id column with a unique index).
  2. On every delivery, look the article up by id. Update it if it exists, create it if not.
  3. Use is_update for logging or notifications, not to decide whether to insert. That way a missed or repeated delivery can never create a duplicate.

In SQL, that's a single statement:

INSERT INTO articles (junia_id, slug, title, body_html, updated_at)
VALUES ($1, $2, $3, $4, now())
ON CONFLICT (junia_id) DO UPDATE
SET title = EXCLUDED.title,
    body_html = EXCLUDED.body_html,
    updated_at = now();

The slug is deliberately left out of the update. It never changes after the first delivery, so links to the article keep working even if you rename it.

Payload reference

{
  "id": "5f0c1c2e-8a4b-4d57-9b8e-2f1d7c3a9e10",
  "title": "10 Ways to Repurpose a Blog Post",
  "content_html": "<p>One good article can become a week of content...</p><h2>1. Turn it into a newsletter</h2>...",
  "content_markdown": "One good article can become a week of content...\n\n## 1. Turn it into a newsletter\n...",
  "slug": "10-ways-to-repurpose-a-blog-post",
  "meta_title": "10 Ways to Repurpose a Blog Post (2026 Guide)",
  "meta_description": "Get more from every article with these ten repurposing ideas.",
  "status": "published",
  "featured_image": "https://example.com/images/repurpose.png",
  "featured_image_alt": "Blog post turned into social media posts",
  "published_url": null,
  "scheduled_date": null,
  "published_at": "2026-09-23T10:30:00.000Z",
  "is_update": false,
  "test": false,
  "language": "English",
  "tags": ["content marketing", "repurposing"],
  "faqs": [
    { "question": "How often should I repurpose content?", "answer": "Whenever an article is still getting traffic." }
  ]
}
FieldTypeNotes
idstringJunia's article id. Never changes, use it as your key.
titlestringThe article's H1.
content_htmlstringBody as HTML, without the title and featured image (they have their own fields). Other images are included with their URLs.
content_markdownstringThe same body as Markdown.
slugstringURL slug. Fixed after the first delivery to this webhook.
meta_title, meta_descriptionstringSEO metadata.
statusstringpublished, or draft if you picked Draft when publishing.
featured_image, featured_image_altstring or nullFeatured image URL and its alt text.
published_urlstring or nullThe article's public URL, if your endpoint returned one on an earlier delivery.
scheduled_datestring or nullThe date you scheduled the article for, if any.
published_atstringWhen this request was sent (ISO 8601). Rejecting old values protects against replayed requests.
is_updatebooleantrue if this article was delivered to this webhook before.
testbooleantrue only for Send Test requests.
languagestringArticle language, e.g. English.
tagsstring[]Tags on the article.
faqsobject[]{ question, answer } pairs.

Responding to Junia

  • Reply with any 2xx status within 30 seconds to confirm delivery. Anything else counts as a failure.
  • Redirects are not followed, so point the webhook at the final URL.
  • Optionally include the article's public address in a JSON response: { "url": "https://example.com/blog/10-ways-to-repurpose-a-blog-post" }. Junia opens it after publishing, can submit it to search engines for indexing, and sends it back as published_url in later updates.

When something goes wrong

What you seeLikely causeWhat to do
Your endpoint returns 401 or 403The secret or security type doesn't matchCopy the Secret Key again and check the Security setting matches your code. With signed requests, make sure you sign the raw body.
403 before your code even runsA firewall, bot protection or security plugin blocked the requestAllow POST requests with the user agent Junia-Webhooks/1.0 to your webhook path.
"Webhook URL must be publicly accessible"The URL points to localhost or a private networkDeploy your endpoint, or expose it with a tunnel such as ngrok while developing.
Status 3xxYour URL redirects, e.g. http to https or a missing trailing slashUse the final URL.
"No response within 30 seconds"Your endpoint does slow work before replyingReply first, then process the article in the background.
Duplicate articles on your siteSaving by title or creating a new row on every requestUpsert on id, see "Keeping articles in sync".

When a scheduled or Autoblog delivery fails, Junia emails you so you can publish the article again.

Questions

Can one article go to several webhooks? Yes. Add as many webhooks as you like and publish to each one. Updates are tracked separately per webhook.

Can I send drafts? Yes. Choose Draft when publishing and the payload arrives with "status": "draft". It's up to your endpoint to keep it unpublished.

Is my content encrypted? Use an https:// URL and the request is encrypted in transit. Signed requests also make sure nobody changed the content along the way.

What if I regenerate the secret? Requests are signed with the new secret straight away, so update your server's configuration at the same time.

Does the payload include images? Yes. The featured image has its own fields, and every other image stays in content_html (and content_markdown) with its original URL.

Need more help?

Our support team is ready to assist you.

Contact Support