API reference

Scheduled delivery webhook

When a scheduled post becomes due, AI Content Studio sends a signed JSON payload to the delivery endpoint you configure for each brand. This page describes exactly what we send and how to verify it.

Your endpointPOSThttps://your-domain.example.com/webhooks/social-wewebAny HTTPS URL you control
HTTP method
POST
Content type
application/json
Timeout
10 seconds
Max attempts
5

Getting started

Configure a delivery endpoint per brand from the calendar screen. Each brand stores its own HTTPS URL and signing secret, and delivery only runs while the endpoint is enabled.

  1. 1Open the calendar and choose delivery settings for a brand.
  2. 2Enter the HTTPS URL of your receiver and a signing secret you generate yourself.
  3. 3Enable delivery and send a test event to confirm your receiver answers with a 2xx status.

Request headers

Every request carries these headers. Use the signature header to authenticate the request and the idempotency key to deduplicate retries.

HeaderExampleDescription
content-typeapplication/jsonAlways JSON encoded UTF-8.
user-agentSocial-WeWeb-Scheduled-Delivery/1.0Identifies the scheduler that sent the request.
x-social-weweb-eventpost.scheduledThe event name, post.scheduled or delivery.test. Branch on this header, not the body.
x-social-weweb-signaturesha256=9f86d0818884…HMAC SHA-256 of the raw request body, keyed with your signing secret and hex encoded.
idempotency-key0f6d5a1c-2f1b-4f0e-9f6d-2b7a1c8e4d33Stable per delivery. Retries of the same post reuse the same key, so treat repeats as duplicates.

Verifying the signature

Compute an HMAC SHA-256 over the raw request body using your signing secret, hex encode it, and compare it with the value after the sha256= prefix. Always compare in constant time and always hash the raw bytes before any JSON parsing or reformatting.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: Buffer, header: string, secret: string) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const received = header.replace(/^sha256=/, "");
  if (received.length !== expected.length) return false;
  return timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

Reject any request whose signature does not match. A mismatch means the body was altered in transit or the sender does not hold your secret.

Events

Two event names are sent to the same endpoint. Branch on the x-social-weweb-event header so test traffic never reaches your production publishing path.

EventSent whenNotes
post.scheduledA post reaches its scheduled time and delivery is enabled for its brand.Carries the real post. A successful response marks the post as published.
delivery.testYou press the test button in delivery settings.Carries placeholder content. Post and page identifiers are null.

Payload fields

The body is a single JSON object. Fields are always present; nullable fields are sent as null rather than omitted.

FieldTypeDescription
Top level
post_idstring | nullIdentifier of the generated post. Null for test events.
titlestringShort headline for the post.
contentstringMain body copy for the post.
hashtagsstring[]Hashtags for the post, without the leading hash character.
image_urlstring | nullAbsolute URL of the selected image. Null when no image was selected.
published_atstring | nullISO 8601 timestamp the post was scheduled for. Null for test events.
channelstringSocial network of the page, such as facebook or line. Test events send test.
target_pagestringDisplay name of the social page.

Example payloads

Sample bodies for both events, formatted for readability. The actual request body is minified.

post.scheduled

{
  "post_id": "d41a6e77-0b2c-4a19-bb5e-6f8c4d2a9013",
  "title": "New cold brew, out today",
  "content": "Slow steeped for 18 hours and served over clear ice.",
  "hashtags": ["coldbrew", "bangkokcafe"],
  "image_url": "https://cdn.example.com/media/cold-brew.jpg",
  "published_at": "2026-08-05T09:00:00.000Z",
  "channel": "facebook",
  "target_page": "Nara Cafe Bangkok"
}

delivery.test

{
  "post_id": null,
  "title": "Scheduled delivery test",
  "content": "This payload verifies the brand delivery destination.",
  "hashtags": [],
  "image_url": null,
  "published_at": null,
  "channel": "test",
  "target_page": "Test page"
}

Expected response

Answer with any 2xx status as soon as you have persisted the payload. Do your publishing work asynchronously: the request is aborted after 10 seconds and any non-2xx status counts as a failure.

  • Any 2xx status is treated as delivered, and the post moves to published.
  • Any other status, a timeout, or a connection error is treated as failed and queued for retry.
  • Redirects are not followed. Publish your receiver at its final URL.
  • The response body is ignored, so an empty 200 is fine.

Retries and idempotency

Failed deliveries are retried with a fixed backoff. After the fifth attempt the delivery is marked failed and is not retried again.

AttemptDelay after previous failure
1Immediate
21m
35m
415m
560m

Every attempt for the same post reuses the same idempotency key, so store that key and ignore a payload you have already processed.

Endpoint requirements

Delivery URLs are validated when saved and again before every send.

  • The URL must use HTTPS.
  • The hostname must resolve only to public IP addresses. Loopback, private and link-local ranges are rejected.
  • The URL must not embed a username or password. Authenticate with the signature instead.
  • A signing secret is required before delivery can be enabled.