Help center Page 11
Publishing cpywrk articles to your own website
Checked against the app on .
cpywrk can post each approved article straight to your own website. The setup in cpywrk takes about five minutes. On your website’s side, a small script has to accept the articles; the technical reference below describes exactly what it receives and includes two working examples you can copy.
If your site runs WordPress
Use the WordPress connector instead. It needs only an application password from your WordPress admin and no code on your site. The steps below are for every other platform.
Setting up the webhook
- In cpywrk, open your business and go to the Publishing tab. At the bottom, click Set up webhook.
- Enter the HTTPS address on your website where the articles should arrive, for example
https://www.yoursite.com/cpywrk-webhook. This is the address of the script from the technical reference. Save. - cpywrk shows a secret, once. Copy it and store it on your website alongside the script; it is what proves a delivery really comes from cpywrk. Then tick the confirmation box and close the dialog. If it ever gets lost, Regenerate on the card creates a new one.
- When the script is in place, click Test webhook on the card. A green check means your website answered.
- Publish an approved article to the webhook from the article’s publish panel, or schedule it. The Publishing tab shows the row as Published, with a link to the post on your site.
Republishing an article later updates the existing post rather than creating a new one, and scheduled articles go out within five minutes of the chosen time.
What arrives on your website
Each article comes in as one request with the title, the text as HTML, the images and a signature. The exact format and two ready-to-use scripts, one in PHP and one in Node, follow below. Whoever looks after your website can take them as they are.
Technical reference: what cpywrk sends and expects back
Every publish is one HTTPS POST to your URL with a JSON body. cpywrk does not follow redirects, waits 10 seconds for a reply, and treats any 2xx as success.
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
X-CPYWRK-Signature | sha256= followed by a hex HMAC-SHA256 of timestamp.rawBody with the shared secret |
X-CPYWRK-Timestamp | Unix time in seconds, as a string |
X-CPYWRK-Publish-Id | ID of this publish; the same value is reused if cpywrk retries |
User-Agent | cpywrk-webhook/1.0 |
Body
| Field | Meaning |
|---|---|
event | article.published for real deliveries, test for the Test button |
publish_id | Same as the header; use it to ignore duplicate deliveries |
is_update | false on the first publish of an article, true when cpywrk republishes it |
external_id | null on first publish; on a republish, the ID your receiver returned the first time |
client_name, client_site_url | The business as configured in cpywrk |
article.title | The headline |
article.slug | URL slug, already sanitised |
article.meta_description | Meta description |
article.html | The article body as HTML, no page wrapper |
article.markdown | The same body as Markdown |
article.featured_image | url, alt, url_expires_at, or null if the article has no images |
article.images | The other images, same shape, in body order |
Image URLs are temporary links that expire after 7 days. Download each image, host it on the site, and rewrite the src attributes in the HTML to your copies.
What the receiving script does
- Verify the signature first: HMAC-SHA256 over the raw body prefixed with the timestamp and a dot, hex encoded, compared with the value after
sha256=. Reject with 401 if missing or different. - Answer
event: testwith 200 and stop; it carries placeholder content. - If
is_updateistrueandexternal_idis set, update that post. Otherwise create a new one. Never decide by slug: a slug can change, and creating a new post each time leaves duplicates. - Save the images on receipt.
- Reply with 200 and JSON. cpywrk reads two optional fields:
external_url(shown as the published link in the app) andexternal_id(sent back on the next republish). Keep the reply under 1 MB.
On a timeout, connection error, 408, 429 or 5xx cpywrk retries once. Any other 4xx is a rejection: no retry, the row shows Failed, and you receive a failure email. Retries reuse the same publish_id; if the post already exists for it, return the existing URL and ID.
The payload carries no tags, categories, author, language or publish date. Set those on your side. The HTML comes from cpywrk’s own Markdown pipeline and contains no scripts; apply the same sanitising you apply to editor content. Regenerating the secret in cpywrk invalidates the old one immediately.
PHP
<?php
$secret = getenv('CPYWRK_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_CPYWRK_SIGNATURE'] ?? '';
$ts = $_SERVER['HTTP_X_CPYWRK_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', "$ts.$raw", $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$payload = json_decode($raw, true);
if ($payload['event'] === 'test') {
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
exit;
}
$article = $payload['article'];
$id = $payload['external_id'];
if ($payload['is_update'] && $id) {
// update the post with ID $id using $article['title'], $article['html'], ...
} else {
// create a new post and put its ID in $id
}
header('Content-Type: application/json');
echo json_encode([
'external_url' => 'https://www.yoursite.com/blog/' . $article['slug'],
'external_id' => (string) $id,
]);
Node with Express
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
const SECRET = process.env.CPYWRK_WEBHOOK_SECRET;
app.post('/cpywrk-webhook', (req, res) => {
const sig = req.headers['x-cpywrk-signature'] || '';
const ts = req.headers['x-cpywrk-timestamp'] || '';
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
.update(`${ts}.${req.rawBody}`).digest('hex');
if (sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
const { event, is_update, external_id, article } = req.body;
if (event === 'test') return res.json({ ok: true });
let id = external_id;
if (is_update && id) {
// update the post with ID `id`
} else {
// create a new post and set `id` to its ID
}
res.json({
external_url: `https://www.yoursite.com/blog/${article.slug}`,
external_id: String(id),
});
});
app.listen(3000);