Ends July 1$89/mo locks in for life. Reverts to $129 after July 1.
///INTEGRATION

Custom API (Webhook) Integration

Connect Articfly to any HTTPS endpoint. Articfly POSTs article.publish, article.update, and article.delete events to your endpoint with a fixed JSON schema and a bearer token. Compatible with Strapi, Sanity, Directus, n8n, Zapier, and any custom backend you write.

Prerequisites

  • A public HTTPS endpoint that accepts POST requests with a JSON body. HTTP is rejected for security. Local development can use ngrok or Cloudflare Tunnel to expose localhost over HTTPS.
  • A shared bearer secret, you generate it (any random 16+ char string), Articfly stores it encrypted and includes it on every request as Authorization: Bearer <secret>. Your receiver verifies it.
  • Active Articfly account, sign up at app.articfly.com if you have not already.

Connecting Your Endpoint

01

Build a receiver that accepts POST

Implement an HTTPS endpoint that returns 2xx for valid requests and 4xx/5xx for errors. Verify the bearer token before processing. See the example below for a minimal Node.js receiver, port the same logic to whatever stack you use.

02

Connect from Articfly

Open Integrations → Custom API in the dashboard. Paste your webhook URL and the bearer secret you generated, then click Connect Custom API. Articfly pings your endpoint to confirm it's reachable, encrypts the secret, and saves the connection.

03

Publish an article to verify

Generate or open any article in the Articfly dashboard, pick Custom API from the destination dropdown, and click Publish. Articfly POSTs an article.publish event to your endpoint. Check your server logs to confirm it arrived.

Request Format

Every event is a POST with a JSON body. Headers:

  • Authorization: Bearer <your-shared-secret>, verify this on every request.
  • Content-Type: application/json
  • User-Agent: Articfly/1.0 (+https://articfly.com)
  • X-Articfly-Request-ID: <article_id>_<event_type>_<unix_seconds>, informational; useful for log correlation.

Body is one of three event shapes:

article.publish

Sent when an article is published or scheduled for the first time.

{
  "articfly_event": "article.publish",
  "articfly_version": "1",
  "article": {
    "id": "9f1b3d2a-...",
    "title": "How to Choose the Right CMS",
    "content_html": "<h2>Introduction</h2><p>...</p>",
    "slug": "choose-the-right-cms",
    "excerpt": "A practical guide to picking the right CMS for your team.",
    "tags": ["cms", "comparison", "saas"],
    "featured_image_url": "https://res.cloudinary.com/articfly/image/...",
    "author_name": "Articfly",
    "scheduled_for": null,
    "status": "publish"
  }
}

article.update

Sent when a previously published article is republished. Includes the external_id you returned on article.publish so you can locate the record in your CMS.

{
  "articfly_event": "article.update",
  "articfly_version": "1",
  "external_id": "abc-123",
  "article": {
    "id": "9f1b3d2a-...",
    "title": "How to Choose the Right CMS (updated)",
    "content_html": "<h2>Introduction</h2><p>...</p>",
    "slug": "choose-the-right-cms",
    "excerpt": "A practical guide to picking the right CMS for your team.",
    "tags": ["cms", "comparison", "saas"],
    "featured_image_url": "https://res.cloudinary.com/articfly/image/...",
    "author_name": "Articfly",
    "scheduled_for": null,
    "status": "publish"
  }
}

article.delete

Sent when an article is deleted in Articfly. Body contains only external_id, no full article payload.

{
  "articfly_event": "article.delete",
  "articfly_version": "1",
  "external_id": "abc-123"
}

Article Fields

Every article object has these keys. Optional fields may be null.

id string · required

Articfly article UUID. Stable across re-publishes, use for dedup.

title string

Article title. Defaults to "Untitled" if missing.

content_html string

Full article body as HTML. Headings, paragraphs, lists, blockquotes, images, links, bold, italic.

slug string | null

URL-friendly slug. Use as-is or regenerate on your side.

excerpt string | null

Short SEO summary, typically < 200 chars.

tags string[]

Article tags. Always an array (may be empty).

featured_image_url string | null

CDN URL of the cover image. Hosted on Articfly's CDN, link, don't mirror.

author_name string | null

Free-text author name. Map to your CMS's author model however you like.

scheduled_for string | null

ISO 8601 timestamp when status === "schedule"; otherwise null.

status enum

One of "publish", "draft", "schedule".

Response Format

Return any 2xx status to indicate success. Articfly optionally reads two fields from the response body:

  • id or external_id, saved on the article and sent back on subsequent article.update / article.delete events. If you don't return one, Articfly falls back to the article UUID.
  • url or external_url, public URL of the published post on your site. Shown in the Articfly dashboard as a click-through.

Recommended response body (200):

{
  "id": "abc-123",
  "url": "https://yourblog.com/posts/choose-the-right-cms"
}

Empty body is fine. Non-2xx statuses surface as errors in the Articfly dashboard. There's no automatic retry, failed publishes need to be re-triggered manually from the article page.

Curl Example

Send a synthetic article.publish event to your endpoint to test the contract before connecting from Articfly:

curl -X POST https://api.yourapp.com/articfly \
  -H "Authorization: Bearer YOUR_SHARED_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "articfly_event": "article.publish",
    "articfly_version": "1",
    "article": {
      "id": "test-123",
      "title": "Test article",
      "content_html": "<p>Hello from Articfly</p>",
      "slug": "test-article",
      "excerpt": "Test",
      "tags": ["test"],
      "featured_image_url": null,
      "author_name": "Articfly",
      "scheduled_for": null,
      "status": "publish"
    }
  }'

Minimal Node.js Receiver

A complete Express handler covering all three events. Verify the bearer, dispatch by articfly_event, return an id for dedup:

import express from 'express'
const app = express()
app.use(express.json())

app.post('/articfly', (req, res) => {
  const expected = `Bearer ${process.env.ARTICFLY_SECRET}`
  if (req.headers.authorization !== expected) {
    return res.status(401).json({ error: 'invalid_bearer' })
  }

  const { articfly_event, article, external_id } = req.body
  switch (articfly_event) {
    case 'article.publish':
      // Persist article to your CMS, return the new record's id + url
      return res.status(200).json({ id: 'cms-' + article.id, url: `https://yourblog.com/p/${article.slug}` })
    case 'article.update':
      // Look up by external_id, update fields
      return res.status(200).json({ id: external_id })
    case 'article.delete':
      // Look up by external_id, archive or hard-delete
      return res.status(200).json({ id: external_id })
    default:
      return res.status(400).json({ error: 'unknown_event' })
  }
})

app.listen(3000)

Common Platforms

n8n

Use a Webhook node with HTTP method POST and authentication set to Header Auth matching your bearer secret. Branch on $json.articfly_event with a Switch node, then call your CMS's API per branch.

Zapier

Use a Webhooks by Zapier Catch Hook trigger. Note that Zapier always returns 200 on a valid hook, so failed downstream actions won't surface as Articfly errors, consider a Filter step plus error-channel notification.

Strapi

Build a custom controller route (e.g. /articfly) that calls strapi.entityService.create()for an Article content type. Verify the bearer in middleware. Return the new entry's id and the public URL.

Sanity

Use a Vercel/Cloudflare/Netlify function as the receiver. Inside, call @sanity/client create() for a post document. Return _id as the id field.

Directus

Use a Flow with a Webhook trigger and a Create Item operation on your Articles collection. Map the incoming JSON to the collection's fields. Return the item's primary key as id.

Custom backend

Any HTTP framework works, Express, Fastify, Django, Laravel, FastAPI, ASP.NET, Go net/http, etc. The contract is just JSON in, JSON out, with bearer auth.

Security Notes

  • HTTPS is required. Articfly rejects HTTP URLs at connect time. Public IPs only, private network ranges (10.x, 192.168.x, localhost, AWS metadata) are blocked at connect AND on every event call (defense in depth).
  • Your bearer secret is encrypted at rest using AES-256-GCM. Articfly sends it only on outbound POSTs to the URL you connected.
  • Always verify the bearer header on your receiver. A leaked URL without the secret can't do anything; the URL alone is not authorization.
  • Rotate the secret by reconnecting in Articfly with a new value. The old secret is overwritten, no replay risk.

Limitations

  • No automatic retry. A failed event surfaces as an error in the Articfly dashboard; the user re-triggers manually. We may add a retry queue in a future release.
  • 30s timeout per event. Long-running CMS imports should respond 2xx fast and process asynchronously on your side.
  • Fixed JSON schema. User-defined payload templates are not supported. If your CMS expects different field names, transform on your side or via n8n / Zapier as middleware.
  • No HMAC signature. Bearer auth only in the current release. HMAC may be added in a future version.

Troubleshooting

"Could not reach <hostname>" at connect time

Articfly's server could not establish an HTTPS connection to your endpoint. Check that the URL has an https:// prefix, the domain resolves over public DNS, and your service is online. For local dev, expose localhost over HTTPS via ngrok or Cloudflare Tunnel, Articfly cannot reach private networks.

"webhook_url points to a private or reserved address"

For security, Articfly blocks URLs resolving to private network ranges (e.g. 192.168.x.x, 10.x.x.x, localhost, AWS metadata at 169.254.169.254). Use a public domain with a real TLS certificate.

Article publishes succeed in Articfly but nothing appears on my site

Articfly considers any 2xx response a success, even an empty 200 from a Zapier hook with no downstream action wired up. Check your receiver logs to confirm the request arrived AND that your downstream logic (write to DB, call CMS API, etc.) actually ran. Add a final assertion step that fails the response with a 5xx if the write didn't happen, so failures surface in the Articfly dashboard.

article.update / article.delete sends a strange external_id

If your receiver doesn't return an id field on article.publish, Articfly falls back to the article UUID. Subsequent update and delete events will use that UUID, your receiver must be able to look up the corresponding record by it. Either store the UUID at publish time as an external reference column on your CMS records, or have your receiver always return your CMS's own ID on publish.