Partner submission API
HMAC-signed REST endpoint for organisational partners submitting books to the Real Life Stories curation queue. Contact us to receive a key ID and secret.
Endpoint
POST /api/public/books/submit
Authentication
Every request must include these headers:
x-rls-key-id— your public key ID.x-rls-timestamp— unix seconds. Rejected if more than 5 minutes skewed.x-rls-signature—hex(hmac_sha256(sha256(secret), `$${ts}.$${sha256(body)}`)).
The signing key is sha256(secret), not the raw secret. We only store that hash; if you leak your secret, request rotation.
Body
JSON with these fields (see the Zod schema on the server for exact limits):
title— 3–300 chars.synopsis— 20–4000 chars.authorName— optional; author is created if new.language— ISO 639-1 (defaulten).originCountry— ISO 3166-1 alpha-2, optional.category— free-form, ≤ 80 chars.pageCount— integer, 1–50 000.imageCount— integer, default 0.coverUrl,sourceUrl,manuscriptUrl— optional https URLs.manuscriptFormat—epub | pdf | mobi | audio, optional.
Pricing formula
Price is locked at submission: round((pageCount × 4¢ + imageCount × 25¢) × 1.5). 50% flows to the storyteller.
Response
201 Created → { id, slug, status: "submitted" }. The book enters the curation queue with status = "draft" and curation_status = "submitted", and only becomes public after an editor approves it.
Errors: 401 auth, 400 validation, 413 body over 100 KB.
Node.js example
import { createHash, createHmac } from "crypto";
const keyId = process.env.RLS_KEY_ID!;
const secret = process.env.RLS_SECRET!;
const body = JSON.stringify({
title: "The Salt Path",
synopsis: "A memoir of walking the South West Coast Path...",
authorName: "Raynor Winn",
language: "en",
originCountry: "GB",
category: "memoir",
pageCount: 288,
imageCount: 0,
});
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = createHash("sha256").update(body).digest("hex");
// Signing key is sha256(secret)
const signingKey = createHash("sha256").update(secret).digest("hex");
const signature = createHmac("sha256", signingKey)
.update(`${ts}.${bodyHash}`)
.digest("hex");
const res = await fetch("https://reallifestories.store/api/public/books/submit", {
method: "POST",
headers: {
"content-type": "application/json",
"x-rls-key-id": keyId,
"x-rls-timestamp": ts,
"x-rls-signature": signature,
},
body,
});
console.log(res.status, await res.json());Handoff kit — “Create a Book from This Story”
Connected story projects (Candle Light Cloud, Remembered Here) hand a single story to Real Life Stories server-to-server. Your project calls POST /api/public/handoff/start, receives a one-time redirectUrl, and sends the author there. The author then reviews exactly what will be copied, confirms rights, and pays the one-off $10 book-creation fee before any draft is made.
Signing is identical to the submission API, with its own headers: x-rls-source (your source slug), x-rls-timestamp (unix seconds, ±5 minutes) and x-rls-signature = hex(hmac_sha256(secret, `$${timestamp}.$${sha256(rawBody)}`)).
externalUserRef— your stable user id (required).externalUserEmail— optional, used to match an existing Real Life Stories account.sourceContentId,sourceContentUrl— the story in your system.content.title,content.bodyText,content.authorName,content.publishedAt.content.assets[]— up to 200 items of{ kind, url, caption, credit, width, height, bytes, mimeType }, wherekindisimage | video | audio | link | document.rightsDeclaration,attributionText— optional; attribution defaults to your project name.
Response: 200 → { token, expiresAt, redirectUrl }. The token is single-use and expires in 30 minutes — redirect the author immediately and never log it. Errors: 401 unknown source, bad signature or stale timestamp; 400 validation.
import { createHash, createHmac } from "crypto";
// Shared secret agreed with Real Life Stories for your source project.
const secret = process.env.RLS_HANDOFF_SECRET!;
const sourceSlug = "real-life-stories";
const body = JSON.stringify({
externalUserRef: user.id,
externalUserEmail: user.email,
sourceContentId: story.id,
sourceContentUrl: `https://reallifestories.example/stories/${story.slug}`,
content: {
title: story.title,
bodyText: story.plainText,
authorName: user.displayName,
publishedAt: story.publishedAt,
assets: story.images.map((img) => ({
kind: "image",
url: img.url,
caption: img.caption ?? null,
})),
},
attributionText: `Originally published on Real Life Stories`,
});
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = createHash("sha256").update(body).digest("hex");
const signature = createHmac("sha256", secret)
.update(`${ts}.${bodyHash}`)
.digest("hex");
const res = await fetch("https://reallifestories.store/api/public/handoff/start", {
method: "POST",
headers: {
"content-type": "application/json",
"x-rls-source": sourceSlug,
"x-rls-timestamp": ts,
"x-rls-signature": signature,
},
body,
});
const { redirectUrl } = await res.json();
// Send the author straight there — the token is single-use and short-lived.
return Response.redirect(redirectUrl, 303);