Webhook / API Set Up Guide for Publishers (Developer Method)

Direct webhook integration for publishers on custom-built sites. Covers HMAC signing, payload schema, retries, and the test ping.

Last updated

This guide is for publishers on a custom-built site (not WordPress, not a CMS supported by Zapier) who want to receive Mintfunnel press release orders via a direct, signed webhook. Choose this method if you have a developer and want the fastest, most reliable integration with no third-party services in the path.

Other methods:

What you'll need

  • A Mintfunnel publisher account with at least one PR-enabled site
  • A developer who can build a small HTTP endpoint that accepts a signed POST and creates a post in your CMS
  • A publicly reachable HTTPS URL for that endpoint

Estimated developer time: 1 to 3 hours, depending on your CMS.

How it works

When you approve a PR order in your Mintfunnel dashboard, Mintfunnel sends a signed HTTPS POST to the endpoint URL you configure. Your endpoint:

  1. Verifies the request signature using a shared secret.
  2. Creates (or updates) a post in your CMS.
  3. Returns a 2xx response with an optional url and id so Mintfunnel can show the live link in the advertiser's dashboard and target future updates to the same post.

Orders always wait in your Mintfunnel approval queue before anything is sent. Approving an order is what fires the webhook. You can turn on auto-approve in your publishing settings to skip the queue.

Part 1: Configure the endpoint in Mintfunnel

  1. Go to Services > PR Distribution > Publishing setup.
  2. Pick the site you're setting up (if you have more than one).
  3. Choose My site is custom-built (I have a developer) as your distribution method.
  4. In the Endpoint URL field, paste the public HTTPS URL of your receiver (for example, https://yoursite.com/webhooks/mintfunnel). Trailing slashes are preserved exactly as entered.
  5. Click Generate signing secret. Mintfunnel will generate a strong random secret and store it. Copy it now. You will paste it into your server config in Part 2.
  6. Optional: toggle Require approval off if you want Mintfunnel to send the webhook automatically as soon as an order is paid, without sitting in your approval queue.
  7. Click Save.

Note: You can rotate the signing secret at any time by clicking Generate signing secret again. The old secret stops working immediately, so update your server config in the same maintenance window.

Part 2: Build the receiving endpoint

Your endpoint must:

  • Accept POST requests at the URL you registered.
  • Read the raw request body (do not parse before verifying).
  • Verify the X-Mintfunnel-Signature header against the body using HMAC-SHA256.
  • Return a 2xx response within 15 seconds.

Request headers Mintfunnel sends:

  • Content-Type: application/json
  • X-Mintfunnel-Event: one of pr.publish, pr.update, pr.test_ping
  • X-Mintfunnel-Signature: sha256=<hex_hmac>
  • X-Mintfunnel-Payload-Version: 1
  • User-Agent: Mintfunnel-PR/1.0

Signature verification: compute HMAC-SHA256(raw_request_body, signing_secret), lowercase-hex encode it, and prefix with sha256=. Compare to X-Mintfunnel-Signature using a constant-time comparison (hash_equals in PHP, crypto.timingSafeEqual in Node, hmac.compare_digest in Python). Reject any request whose signature does not match, including the test ping.

Example: Node.js / Express

import express from 'express';import crypto from 'crypto';const SECRET = process.env.MINTFUNNEL_SECRET;const app = express();app.post('/webhooks/mintfunnel', express.raw({ type: 'application/json' }), (req, res) => { const header = req.header('X-Mintfunnel-Signature') || ''; const expected = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(req.body) .digest('hex'); if (!crypto.timingSafeEqual( Buffer.from(header), Buffer.from(expected))) { return res.status(401).send('bad signature'); } const payload = JSON.parse(req.body.toString('utf8')); if (payload.event === 'pr.test_ping') { return res.status(200).json({ ok: true }); } const postId = createOrUpdatePost(payload); const postUrl = urlFor(postId); res.status(200).json({ id: postId, url: postUrl }); });

Example: PHP

$body = file_get_contents('php://input');$header = $_SERVER['HTTP_X_MINTFUNNEL_SIGNATURE'] ?? '';$expected = 'sha256=' . hash_hmac('sha256', $body, getenv('MINTFUNNEL_SECRET'));if (!hash_equals($expected, $header)) { http_response_code(401); exit('bad signature');}$payload = json_decode($body, true);// ...handle event, create/update post, return JSON with id + url...

Part 3: Send the test ping

Once your endpoint is deployed and your signing secret is configured on your server:

  1. Return to Services > PR Distribution > Publishing setup in Mintfunnel.
  2. Click Send test.
  3. The indicator turns green if your endpoint responded with any 2xx status within the 10-second test timeout. If it fails, the error from your server is shown inline (HTTP status plus a short body excerpt).

The test ping uses X-Mintfunnel-Event: pr.test_ping and a different payload shape (see the Payload reference section). Your endpoint should treat it as a connectivity check and return 200 without creating anything in your CMS.

Payload reference

Real orders (event = pr.publish or pr.update):

Field

Type

Description

payload_version

integer

Currently 1. Major bumps signal breaking changes; reject unknown majors.

event

string

pr.publish for new orders, pr.update for re-publishes.

order_id

integer

Mintfunnel's internal numeric order ID.

order_number

string

Human-readable order number.

title

string

Press release headline.

content

string (HTML)

Full press release body, ready for a rich-text or HTML field.

company_name

string or null

Advertiser company name.

featured_image_url

string or null

Direct URL to the hero image.

contact_name

string or null

Press contact name.

contact_email

string or null

Press contact email.

additional_notes

string or null

Internal notes from the advertiser. Do not publish.

placement_type

string

Placement type (Press Release, Sponsored Article, etc.).

external_post_id

string or null

On pr.update, the post ID your endpoint previously returned. Null on first publish.

Test ping (event = pr.test_ping):

{ "payload_version": 1, "event": "pr.test_ping", "sent_at": "2026-05-21T14:30:45Z", "message": "Test ping from Mintfunnel. Safe to acknowledge with a 2xx response.", "publisher": { "id": 123, "website": "example.com" }}

Response format

Return any 2xx status code to indicate success. Optionally include a JSON body so Mintfunnel can display the live link to the advertiser and target future updates correctly:

{ "id": "external-post-12345", "url": "https://yoursite.com/articles/my-article"}

  • id (or post_id, either is accepted): the identifier Mintfunnel sends back as external_post_id on future pr.update events for this order. If omitted, any previously stored ID is preserved.
  • url: the canonical published article URL. Displayed in the advertiser's reporting.

Retries and error handling

Endpoint response

How Mintfunnel treats it

2xx

Success. Response is parsed for url and id.

4xx (400, 401, 422, etc.)

Permanent failure. The order is marked failed without retry. A truncated body excerpt is logged for the Mintfunnel Team.

5xx, timeout, or network error

Transient failure. The job is retried by the queue worker with exponential backoff.

If your endpoint is experiencing a real outage, return a 5xx (rather than a 4xx) so the order will be retried automatically once you are back online.

Handling updates

When an order's content is edited after initial publication, Mintfunnel re-sends the webhook with event = pr.update and external_post_id set to the ID your endpoint returned on the original publish. Your endpoint should:

  1. Look up the existing post by external_post_id.
  2. Update its title, content, and (optionally) featured image.
  3. Return 200 with the same id in the JSON body.

If you do not want to support updates, simply ignore pr.update events (return 200 without doing anything). The original post stays live.

Multiple sites

If your publisher account has multiple PR-enabled sites, each one has its own independent distribution method and endpoint. Use the site picker on the Publishing setup page to configure each site separately. You can mix and match. For example, the WordPress plugin on one site and a direct webhook on another.

Security checklist

  • Always verify the X-Mintfunnel-Signature header before processing the body, including for test pings.
  • Use a constant-time comparison when checking the signature.
  • Store the signing secret as a server environment variable, never in source control.
  • Only accept webhook traffic over HTTPS.
  • Sanitize content if you allow any further processing. It arrives as HTML.
  • Treat additional_notes as internal: never render it in a public page.

Troubleshooting

Send test reports a 401 from my server. Your signature check is rejecting the request. Make sure you are hashing the raw body bytes (not a re-serialized JSON object) with the secret from your Mintfunnel dashboard. Common causes: stripping whitespace, parsing JSON first, or storing the secret with a trailing newline.

Send test times out. The test ping must complete within 10 seconds. If your endpoint cold-starts (serverless), warm it before testing. Real orders allow up to 15 seconds.

Updates are not reaching the right post. Make sure you are returning id (or post_id) in your response on the initial publish. Mintfunnel stores that value and sends it back in external_post_id on subsequent pr.update events.

I rotated the secret and now production webhooks fail. The old secret stops working the moment you generate a new one. Update your server config and roll your deployment in the same window. There is no grace period.

Still need help?

Ask Maren about billing, campaign setup, or anything not covered.