Publish notifications
Get called when a pack is published, with signature verification and retry rules
If you would rather not poll the image pack pulling endpoints, set an endpoint URL: when the shop owner clicks "Publish" on the website and a new version is actually created, Paitumao POSTs a message to that URL. The message carries only the basics of that version, no images and no text. On receipt, fetch the content with a key that has documents:read via GET /api/v1/image-pack-publications/<id>.
Settings live under "Developer Center → Notifications": enter the URL (https:// only), turn the switch on, copy the signing secret. "Send test notification" sends a ping right away so you can confirm the endpoint is reachable and the signature verifies.
When we send
| Situation | Sent? |
|---|---|
| "Publish" clicked, version number goes up | Yes |
Repeated submit with the same Idempotency-Key, no new version | No |
| Publish rejected (unsaved draft changes, missing images, empty pack) | No |
| Switch off, or URL empty | No |
The request
POST <your URL>
Content-Type: application/json
User-Agent: paitumao-webhook/1
X-Paitumao-Event: image_pack.published
X-Paitumao-Delivery: 7f5b0c9e-…
X-Paitumao-Timestamp: 1757600000
X-Paitumao-Signature: v1=3f1a…{
"event": "image_pack.published",
"publication": {
"id": 42,
"pack_id": "pk_xxxxxxxx",
"version": 3,
"style_code": "QS-2601",
"published_at": 1757600000000
}
}| Header | Meaning |
|---|---|
X-Paitumao-Event | image_pack.published, or ping from the test button |
X-Paitumao-Delivery | Id of this delivery. Unchanged across retries; dedupe on it |
X-Paitumao-Timestamp | Send time, Unix seconds. Refreshed on each retry |
X-Paitumao-Signature | v1= + hex HMAC-SHA256, see below |
The ping body is just {"event":"ping"}, with no publication.
Fields mean the same as in the pulling endpoints: id is the record id you fetch content with, pack_id identifies the product, style_code is display only.
Verifying the signature
The signing secret is shown under "Developer Center → Notifications" and starts with whsec_. Algorithm:
- Take
tsfrom theX-Paitumao-Timestampheader and the raw request bodybody(do not parse and re-serialise; the bytes must match exactly) - Compute
HMAC-SHA256(secret, ts + "." + body)as hex - Compare it with the value after
v1=inX-Paitumao-Signatureusing a constant-time comparison - Reject if
tsdiffers from the current time by more than 5 minutes, to block replays
Node.js reference:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, headers, rawBody) {
const ts = headers['x-paitumao-timestamp'];
const sig = headers['x-paitumao-signature'] ?? '';
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = Buffer.from(
createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex'),
'hex'
);
for (const part of sig.split(',')) {
const [v, hex] = part.trim().split('=');
if (v !== 'v1' || !hex) continue;
const given = Buffer.from(hex, 'hex');
if (given.length === expected.length && timingSafeEqual(given, expected)) {
return true;
}
}
return false;
}If verification fails, respond 401 and discard. If the secret leaks, click "Regenerate" on the page: the old secret stops working immediately and every later notification is signed with the new one.
Timeouts and retries
- Each request times out after 8 seconds. Your endpoint only needs to verify, record the id and respond
2xx. Fetch the content afterwards, not before responding. - Non-
2xx, connection failures and timeouts all count as failures. We then retry twice (after 2 s and 10 s) and give up after the third attempt. - Nothing is re-sent after that. Your system should already follow the incremental pulling contract as the fallback: a notification only lets you pull sooner, it is not the only source.
- Retries keep the same
X-Paitumao-Delivery;X-Paitumao-Timestampand the signature are recomputed. The same notification may arrive twice, so dedupe on the delivery id or on(pack_id, version). - Delivery happens in the background after the publish. A failed delivery never affects the publish itself; the outcome is shown under "Last delivery" on the settings page.
URL requirements
https://only. Redirects are not followed (3xxcounts as a failure).- No
localhost, private hostnames or IP literals: Paitumao's servers cannot reach them. For local debugging use a publicly reachable tunnel (for examplecloudflared tunnel --url). - No username or password in the URL.
Photopurr Docs