← Bookends
API
Tokens

Run Bookends from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the article (its opening, its ending, its type and the skill's type-fit table), scans the opening, the title and the ending for the cliche openers and machine-writing tells the skills disqualify, picks the CTA archetype from the decision tree, works out the A/B sample size and run time, and measures the button's contrast - all free, in bookkit.js. The run writes the openings and titles, or the CTA; the page then holds the reply to the skills' rules and its own facts (bookreply.js) and turns a CTA into a drop-in HTML block. The run never invents a statistic, a quote, a person or a result.

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"bookends"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, article, brief, facts) is missing. The page always sends all five of task, article, brief, carry, facts; carry may be an empty string.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order a writer works - the opening first, then the end:

A missing or unknown task is answered as hooks. Both lanes send the same five fields. The facts are the page's worked-out lines - different per lane - and a line starting "must:" is a rule the reply has to hold. A long article is cut in the middle (the opening and the ending are kept) with a marker, and the facts say how much was cut.

The input

FieldRequiredWhat
taskyeshooks or cta. If it is missing or neither, do hooks and set "task": "hooks".
articleyesthe finished draft as pasted (a long one is cut in the middle, with a marker).
briefyesthe writer's answers, one labelled line per item; "not given" means not given.
carrysend it (empty unless task is cta and an opening was chosen)cta task only: the opening and title the writer chose in the hooks task, or empty.
factsyesthe page's worked-out lines for this task. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skills, @samber/copywriting-hooks and @samber/copywriting-cta - the only standard the run applies:

For the opening (@samber/copywriting-hooks):

For the CTA (@samber/copywriting-cta):

House rules

The replies

hooks

Offer the writer a real choice of openings, and titles.

  1. summary - one sentence, at most 40 words: what the current opening does wrong or right, and the strongest direction.
  2. hooks - three or four objects, genuinely different: name - the hook's name exactly as the catalog in the facts writes it; lever - its lever exactly as the catalog gives it; candidates - exactly two different opening sentences or short paragraphs for this article (different angles, not rewordings); commits - one sentence: what this opening commits the next paragraphs to.
  3. titles - three to five objects: formula - one of curiosity, contrarian, data, list, how-to; text - a title for this article that the article genuinely pays off.
  4. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "hooks",
  "summary": "one sentence",
  "hooks": [ { "name": "Contrarian", "lever": "break a prediction", "candidates": [ "candidate A", "candidate B" ], "commits": "one sentence" } ],
  "titles": [ { "formula": "how-to", "text": "a title" } ],
  "caveats": [ ]
}

The page holds it to the skill's rules and its own facts (check() in bookreply.js; any warning makes the reply flagged): the reply answers the task it was sent; three or four hooks, each named exactly as the catalog writes it and carrying the catalog's lever, none offered twice and none on the avoid list for the article's type; exactly two different candidates per hook, none using a disqualified opener or machine-writing tell, and none using a number the article does not give; a commits sentence for every hook; at least one intellectual, one sensory and one reader-direct hook among the options; three to five titles, each with a formula from curiosity, contrarian, data, list and how-to, none using a cliche ("won't believe" included), none using a number the article does not give, and none in Title Case when the article is French. A note that does not flag: options that pull fewer than three levers.

cta

Prescribe the end-of-article CTA.

  1. summary - one sentence, at most 40 words.
  2. archetype - the facts' archetype letter and name exactly (for example "A. Author-signature subscribe"); when the facts give none, the closest one, and say why in why.
  3. why - one or two sentences naming the input combination.
  4. copy - headline (the value line), body (one or two lines of proof or detail, using only proof the facts allow), button (the button text - a specific verb and object), secondary (the second, lighter action as link text when the archetype pairs two actions, as J does; "" otherwise), subtext (a risk reversal, or "Omit: " and the reason).
  5. form - placement, weight (low, medium or high, with a reason), layout, proof (what to put beside it, or "none").
  6. mechanism - the named mechanism and one sentence on why it fits, or "None: " and why.
  7. ab - first (one variable, two genuinely different variants), why (one sentence), sample (the facts' sample size and run time copied exactly, or the facts' reason to skip), next (two further tests, in order).
  8. accessibility - contrast (the facts' ratios copied exactly, or the targets), touch, markup, keyboard, color.
  9. risks - two or three anti-patterns this writer is at risk of, each one sentence.
  10. next_moves - two or three follow-ups (for example a cross-post variant, or the hard-sell version shown for contrast).
  11. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "cta",
  "summary": "one sentence",
  "archetype": "A. Author-signature subscribe",
  "why": "one or two sentences",
  "copy": { "headline": "the value line", "body": "one or two lines", "button": "the button text", "secondary": "", "subtext": "a risk reversal" },
  "form": { "placement": "end-only", "weight": "medium - why", "layout": "single button", "proof": "none" },
  "mechanism": "None: why",
  "ab": { "first": "button A vs button B", "why": "one sentence", "sample": "copied from the facts", "next": [ "a test", "a test" ] },
  "accessibility": { "contrast": "copied from the facts", "touch": "44 by 44 pixels", "markup": "a native link or button", "keyboard": "a visible focus ring", "color": "an underline or an arrow as well as colour" },
  "risks": [ "one sentence" ],
  "next_moves": [ "a follow-up" ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the archetype starts with the decision tree's letter ("J." for a brand reader comparing options), or is written "A. Name" to "K. Name" when the tree gives none; a headline and a button; a button that is not a generic verb (Learn more, Click here, Get started, Subscribe, Sign up and their French forms); no demo or sales ask on a reader who is not ready to act; no disqualified opener and no "Subscribe for more" in the copy; no number in the copy or the proof line that the article, the brief, the carried opening or the facts do not give, and no reader count when the writer gave no proof; the A/B sample copying the page's readers per variant when the page has a sample size, or saying to skip when the page says skip; a first test named; the contrast line copying the page's text ratio, and saying so when the button fails AA; a secondary link for archetype J; two or three risks and two or three next moves. Notes that do not flag: a mechanism that reaches for urgency the writer did not choose, and a CTA written without a chosen opening.

Worked examples

Three runs from the page's three free samples (fictional writers, companies and figures): the openings for the CI write-up, the CTA for the B2B comparison (archetype J, with its secondary link), and the CTA for the French newsletter essay. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the article and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

hooks - An indie developer's CI write-up

The page read the article as a tutorial / how-to from its shape (252 words): strong hooks Direct Problem (PAS), Promise / Benefit, Conditional; avoid Scene Opener / In Medias Res, Historical Analogy. Its opening uses In today's fast-paced world, In this article, I'll....

Input (the article is 1446 characters; the first 698 are shown):

{
 "task": "hooks",
 "article": "In today's fast-paced world, waiting for CI is something every developer knows too well. In this article, I'll walk you through how I made our pipeline faster.\n\n## Where the 41 minutes went\n\nOur test suite for the billing service took 41 minutes on every push. I exported a week of pipeline timings and sorted the steps by total time. Three things stood out: dependency installs took 11 minutes because the cache key changed on every lockfile touch, the integration tests ran one after another on a single runner, and a Docker image was rebuilt from scratch even when only test files changed.\n\n## Step 1: a cache key that survives\n\nI keyed the dependency cache on the lockfile hash plus the runtime\n\n[... 748 more characters of the article cut from this display ...]",
 "brief": "Title as drafted: How I Made Our CI Pipeline Faster\nLanguage: English\nAudience: Backend developers at small product teams\nWhere it is published: personal blog, cross-posted to dev.to\nArticle type: Tutorial / how-to (read by the page)\nArticle context: Personal / independent blog or essay\nPrimary objective: Newsletter / email subscription\nReader relationship: First-time visitor\nFunnel stage: Not applicable\nMechanism preference: None: value statement only\nConstraints: No popups. One block at the end.\nProof the writer can show: not given",
 "carry": "",
 "facts": "Article: 252 words, about 1 minute to read, 5 headings.\nNumbers in the article: 41, 11, 1, 90, 2, 640, 6, 5, 3, 9, 60.\nType: Tutorial / how-to. Strong hooks for this type: Direct Problem (PAS), Promise / Benefit, Conditional. Avoid: Scene Opener / In Medias Res, Historical Analogy.\nCurrent opening, first sentence: \"In today's fast-paced world, waiting for CI is something every developer knows too well.\" - it uses In today's fast-paced world, In this article, I'll..., which the skill disqualifies.\nTitle as drafted: 33 characters, Title Case.\nThe catalog, name (lever, group): Curiosity Gap (open a gap, intellectual); Contrarian (break a prediction, intellectual); Bold Claim / Promise (promise a payoff); Scene Opener / In Medias Res (drop into a scene, sensory); Surprising Statistic (borrow weight); Question (open a gap); Quote + Disagreement (borrow weight); Personal Confession (drop into a scene); Concrete Specific Detail (drop into a scene, sensory); Pattern Interrupt (break a prediction); Direct Problem (PAS) (promise a payoff, reader-direct); Promise / Benefit (promise a payoff, reader-direct); Historical Analogy (borrow weight, intellectual); Definition Reversal (break a prediction, intellectual); Authority Borrow (borrow weight); Time Anchor (drop into a scene); Conditional (promise a payoff, reader-direct); Open Loop (open a gap).\nmust: 3 or 4 hooks from the catalog, each with a different lever where possible, including at least one intellectual, one sensory and one reader-direct hook, and none from the avoid list.\nmust: every number, name, date and quotation in a candidate or a title appears in the article; never a statistic, a quote or a person the article does not give."
}

Reply:

{
 "task": "hooks",
 "summary": "The current opening spends two sentences on disqualified openers before saying anything; the strongest direction is to lead with the 41-minute pipeline and the fact that 60 lines of config brought it to 9.",
 "hooks": [
  {
   "name": "Direct Problem (PAS)",
   "lever": "promise a payoff",
   "candidates": [
    "Our billing service's test suite took 41 minutes on every push, so everyone started a second task while they waited for a green build. Three changes and 60 lines of pipeline config brought the median down to 9 minutes.",
    "A 41-minute pipeline means nobody waits for it: you push, open something else, and come back to the build with the first task half forgotten. Here are the three changes that took ours to 9."
   ],
   "commits": "The next paragraphs must show where the 41 minutes went and then walk through each of the three fixes in turn."
  },
  {
   "name": "Concrete Specific Detail",
   "lever": "drop into a scene",
   "candidates": [
    "Dependency installs alone took 11 minutes on every push, because our cache key changed whenever anyone touched the lockfile.",
    "Dependency installs: 11 minutes. Integration tests: 640 of them, one after another on a single runner. The Docker image: rebuilt from scratch even when only test files changed. That was our 41-minute CI pipeline."
   ],
   "commits": "The article must go straight into the week of pipeline timings and explain each slow step before fixing it."
  },
  {
   "name": "Curiosity Gap",
   "lever": "open a gap",
   "candidates": [
    "I exported a week of pipeline timings expecting to find one slow step. I found three, and only one of them was the tests themselves.",
    "The slowest part of our CI was not a slow step. It was steps waiting their turn: 640 integration tests running one after another on a single runner."
   ],
   "commits": "The next section must name all three slow steps and show how sorting the timings by total time exposed them."
  },
  {
   "name": "Promise / Benefit",
   "lever": "promise a payoff",
   "candidates": [
    "Three changes, 60 lines of pipeline config, and our median CI run went from 41 minutes to 9. Here is each change and the idea behind it, so you can try it on your own pipeline.",
    "By the end of this post you will have three changes to try on your own CI: a cache key that survives lockfile edits, integration tests split by recorded duration, and an image build that skips test-only pushes."
   ],
   "commits": "Each step must be concrete enough for a reader to copy, with the before and after time for that step."
  }
 ],
 "titles": [
  {
   "formula": "how-to",
   "text": "How We Cut Our CI Pipeline From 41 Minutes to 9"
  },
  {
   "formula": "list",
   "text": "3 Changes That Took Our CI From 41 Minutes to 9"
  },
  {
   "formula": "data",
   "text": "41 Minutes to 9 With 60 Lines of Pipeline Config"
  },
  {
   "formula": "curiosity",
   "text": "Where Our 41-Minute CI Pipeline Was Really Spending Its Time"
  }
 ],
 "caveats": [
  "The ending asks for comments and says Subscribe for more; the end-of-article CTA replaces both.",
  "Only installs and the slowest shard have before and after times; the Docker step has no number, so no hook promises one."
 ]
}

What step 5 prints for it: - Direct Problem (PAS): Our billing service's test suite took 41 minutes on every push, so everyone started a second task while they waited for a green build. Three changes and 60 lines of pipeline config brought the median down to 9 minutes. | - Concrete Specific Detail: Dependency installs alone took 11 minutes on every push, because our cache key changed whenever anyone touched the lockfile. | - Curiosity Gap: I exported a week of pipeline timings expecting to find one slow step. I found three, and only one of them was the tests themselves. | - Promise / Benefit: Three changes, 60 lines of pipeline config, and our median CI run went from 41 minutes to 9. Here is each change and the idea behind it, so you can try it on your own pipeline.

The page's check: kept - nothing to flag

cta - A B2B comparison for buyers weighing options

Archetype J. Direct + Transitional pair - a brand blog reader comparing options. The carry is the sample's first hook candidate and first title, as if the writer had picked them. A/B: 10317 readers per variant, verdict run. Contrast: text 6.7:1.

Input (the article is 1530 characters; the first 698 are shown):

{
 "task": "cta",
 "article": "Have you ever wondered why month-end close still takes a week at so many distributors?\n\nFinance teams reconcile supplier invoices against purchase orders and warehouse receipts. There are three common ways to do it, and each breaks in a different place.\n\n## 1. Manual matching in spreadsheets\n\nA clerk exports open purchase orders and receipts and matches each invoice line by hand. It costs nothing to start and it works at 200 invoices a month. At 2,000 it is a full-time job, and partial deliveries are where errors hide.\n\n## 2. ERP auto-match with tolerances\n\nMost ERPs can auto-match when the invoice, the order and the receipt agree within a tolerance. It clears clean invoices well. It fails\n\n[... 832 more characters of the article cut from this display ...]",
 "brief": "Title as drafted: The Truth About Three-Way Invoice Matching\nLanguage: English\nAudience: Controllers and accounts payable leads at mid-size distributors\nWhere it is published: the Quellbrook blog\nArticle type: Longform analysis\nArticle context: Brand / company / content-marketing blog\nPrimary objective: Demo or sales call booking\nReader relationship: Mixed / unknown\nFunnel stage: MOFU: evaluating options\nMechanism preference: Reciprocity (free asset first)\nConstraints: Brand voice is plain and specific; no exclamation marks.\nProof the writer can show: Quellbrook is used by 380 distributors; a 12-page comparison guide exists",
 "carry": "Opening chosen: A new tool is not always the fix for slow invoice matching. Under 500 invoices a month, a well-kept spreadsheet is fine, and if most of your invoices are clean, the ERP you already run will match them once its tolerances are tuned.\nTitle chosen: How to Choose a Three-Way Invoice Matching Method for Your Volume",
 "facts": "Article: 251 words, about 1 minute to read, 4 headings.\nNumbers in the article: 1, 200, 2000, 2, 3, 500.\nArchetype from the skill's decision tree: J. Direct + Transitional pair - a brand blog reader comparing options.\nMechanism default: only a mechanism that genuinely fits; none is a valid answer.\nmust: the CTA keeps the promise the chosen opening makes - a CTA that contradicts the article is an anti-pattern.\nCurrent ending: \"Learn more about Quellbrook. Book a demo. Download our ebook. Follow us on LinkedIn: https://example.com/a https://example.com/b https://example.com/c\" - it uses a generic verb (Learn more, Click here, Get started); a hard-sell demo ask.\nProof the writer gave (the only proof the CTA may cite): Quellbrook is used by 380 distributors; a 12-page comparison guide exists.\nA/B sample size, two-sided 95 percent and 80 percent power: 10317 readers per variant to detect a 20 percent relative lift from 4 percent; about 3 weeks at the given readership. Verdict: run - run at least 3 full weeks to a pre-committed sample, and don't peek. must: copy these figures exactly.\nButton contrast: text 6.7:1 (passes AA 4.5:1); button against the page 6.7:1 (passes 3:1). must: copy these ratios exactly."
}

Reply:

{
 "task": "cta",
 "summary": "Replace the four competing links with one pair: the 12-page comparison guide as the button, and a quieter link to a walkthrough with Quellbrook for readers whose split shipments are routine.",
 "archetype": "J. Direct + Transitional pair",
 "why": "A brand blog reader at the evaluating stage, a demo objective and a reciprocity preference: the guide serves the controller still comparing methods, and the direct link serves the one who has already recognised the third case.",
 "copy": {
  "headline": "Work out which matching method fits your invoice volume before you change anything",
  "body": "The 12-page comparison guide sets the three methods side by side. Quellbrook is used by 380 distributors.",
  "button": "Get the 12-page comparison guide",
  "secondary": "Partial deliveries and price changes routine for you? See Quellbrook match your own invoices",
  "subtext": "Free, and no call needed to read it."
 },
 "form": {
  "placement": "end-only, after How to choose, replacing the current four links",
  "weight": "medium - the guide button is the visible action and the walkthrough link sits beside it as plain text, because most readers are still evaluating",
  "layout": "One button for the guide, then the secondary text link to the demo booking for readers already past comparing.",
  "proof": "Quellbrook is used by 380 distributors, as one plain line under the body"
 },
 "mechanism": "Reciprocity: the free guide gives an accounts payable lead something useful before Quellbrook asks for a call, which fits a reader comparing options and the writer's own preference.",
 "ab": {
  "first": "Which action gets the button: variant A puts the guide on the button and the walkthrough in the text link; variant B puts the walkthrough on the button and the guide in the text link",
  "why": "The primary objective is a booked call, so the first question is whether leading with the guide costs bookings or earns them later.",
  "sample": "10317 readers per variant to detect a 20 percent relative lift from 4 percent; about 3 weeks at the given readership. Verdict: run - run at least 3 full weeks to a pre-committed sample, and don't peek.",
  "next": [
   "The headline: the volume-rule headline against one that names split shipments and price changes",
   "The proof line shown against left out, to see whether the 380 distributors line helps or reads as a sales turn"
  ]
 },
 "accessibility": {
  "contrast": "Text 6.7:1 (passes AA 4.5:1); button against the page 6.7:1 (passes 3:1).",
  "touch": "44 by 44 pixels at least for the button and the secondary link",
  "markup": "a native link for the guide download and a native link for the walkthrough, not clickable divs",
  "keyboard": "a visible focus ring on both links, guide first in Tab order",
  "color": "the secondary link stays underlined and the button carries an arrow, so neither relies on colour alone"
 },
 "risks": [
  "Keeping Book a demo next to an ebook and LinkedIn gives a reader still comparing methods three equal choices and no reason to pick one.",
  "A hard sales turn at the end would contradict an article that tells readers under 500 invoices a month to keep the spreadsheet.",
  "Gating the guide behind a long form would turn a reciprocity offer into a toll."
 ],
 "next_moves": [
  "A version for readers who arrive at the third option: the walkthrough link as the button, placed right after the matching layer section.",
  "The hard-sell version for contrast: a single Book a demo button with the 380 distributors line, which suits a ready-to-act reader but not this one."
 ],
 "caveats": [
  "Confirm that the 12-page guide covers these three methods; if it covers something else, change the body line.",
  "Count the result in booked calls as well as downloads, since the objective is the call."
 ]
}

The drop-in block the page exports for it (R.ctaHtml):

<aside class="end-cta" aria-labelledby="end-cta-title" style="border-top:1px solid #d0d7de;margin:2.5rem 0 0;padding:1.25rem 0">
  <p id="end-cta-title" style="font-weight:700;font-size:1.15rem;margin:0 0 .4rem">Work out which matching method fits your invoice volume before you change anything</p>
  <p style="margin:0 0 .9rem">The 12-page comparison guide sets the three methods side by side. Quellbrook is used by 380 distributors.</p>
  <a href="#" class="end-cta-button" style="display:inline-block;min-height:44px;min-width:44px;padding:.7rem 1.1rem;border-radius:8px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:#ffffff;background:#1d4ed8">Get the 12-page comparison guide <span aria-hidden="true">&rarr;</span></a>
  <p style="margin:.6rem 0 0"><a href="#">Partial deliveries and price changes routine for you? See Quellbrook match your own invoices</a></p>
  <p style="margin:.6rem 0 0;font-size:.9rem">Free, and no call needed to read it.</p>
</aside>
<!-- Replace href="#" with the real destination. Give .end-cta-button a visible :focus-visible outline in your stylesheet. -->

What step 5 prints for it: J. Direct + Transitional pair | Get the 12-page comparison guide

The page's check: kept - nothing to flag

cta - A French newsletter essay, free to paid

Archetype G. Value-gap tease - a newsletter converting free readers to paid. The carry is the sample's first hook candidate and first title, as if the writer had picked them. A/B: no baseline given, so no sample size. Contrast: text 8.29:1.

Input (the article is 1122 characters; the first 698 are shown):

{
 "task": "cta",
 "article": "Dans un monde en constante évolution, il est crucial de se poser les bonnes questions sur l'endroit où l'on vit.\n\nLe 3 septembre 2025, j'ai rendu les clés d'un deux-pièces de 38 mètres carrés rue de la Roquette. Huit heures de train plus tard, j'ouvrais la porte d'une maison de ville à Aurillac, pour un loyer inférieur de moitié.\n\nOn m'avait prévenue : je perdrais mon réseau, mes clients, mon énergie. Un an après, je n'ai perdu qu'un client sur onze. Ce que j'ai perdu, en revanche, personne ne me l'avait annoncé : l'habitude de dire oui à tout parce que tout était à dix minutes de métro.\n\nLe premier mois, j'ai refusé quatre dîners professionnels. Pas par principe : le dernier train partait\n\n[... 424 more characters of the article cut from this display ...]",
 "brief": "Title as drafted: Un an après Paris\nLanguage: French\nAudience: Indépendants et freelances qui envisagent de quitter une grande ville\nWhere it is published: newsletter Substack\nArticle type: Newsletter issue\nArticle context: Newsletter / paid publication\nPrimary objective: Free-to-paid conversion\nReader relationship: Returning reader, not subscribed\nFunnel stage: Not applicable\nMechanism preference: None: value statement only\nConstraints: not given\nProof the writer can show: 1 400 abonnés gratuits, 90 abonnés payants",
 "carry": "Opening chosen: Le 3 septembre 2025, j'ai rendu les clés de 38 mètres carrés rue de la Roquette. Huit heures de train plus tard, j'ouvrais la porte d'une maison de ville à Aurillac, pour un loyer inférieur de moitié.\nTitle chosen: Ce que j'ai vraiment perdu en quittant Paris",
 "facts": "Article: 193 words, about 1 minute to read, 0 headings.\nNumbers in the article: 3, 2025, 38, 17, 40, 20.\nArchetype from the skill's decision tree: G. Value-gap tease - a newsletter converting free readers to paid.\nMechanism default: none - returning readers and subscribers get the value statement only.\nmust: the CTA keeps the promise the chosen opening makes - a CTA that contradicts the article is an anti-pattern.\nCurrent ending: \"Abonnez-vous pour plus de contenu !\" - it uses \"Subscribe for more\" - no reason to.\nProof the writer gave (the only proof the CTA may cite): 1 400 abonnés gratuits, 90 abonnés payants.\nA/B: no baseline conversion rate given - say what to measure first, and give no sample size.\nButton contrast: text 8.29:1 (passes AA 4.5:1); button against the page 7.77:1 (passes 3:1). must: copy these ratios exactly."
}

Reply:

{
 "task": "cta",
 "summary": "Remplacer Abonnez-vous pour plus de contenu par un seul bloc qui s'appuie sur le prochain numéro déjà annoncé, la facturation en province et les tarifs augmentés de 20 %, réservé aux abonnés payants.",
 "archetype": "G. Value-gap tease",
 "why": "Une newsletter payante, un objectif de passage du gratuit au payant et des lecteurs fidèles mais pas encore payants : le texte annonce déjà une suite précise, c'est elle qui justifie de payer.",
 "copy": {
  "headline": "Dans le prochain numéro : pourquoi j'ai augmenté mes tarifs de 20 % en partant de Paris",
  "body": "Ce que la province a changé dans ma façon de facturer, pour les indépendants qui envisagent le même départ. Ce numéro sera réservé aux abonnés payants.",
  "button": "Passer à l'abonnement payant",
  "secondary": "",
  "subtext": "Omit: le brief ne donne ni essai gratuit ni garantie, et il ne faut pas en inventer."
 },
 "form": {
  "placement": "fin de texte uniquement, à la place de la dernière ligne actuelle",
  "weight": "moyen - un lecteur fidèle n'a pas besoin d'être poussé, la suite annoncée suffit",
  "layout": "un seul bouton sous le titre et une ligne de détail, sans second lien",
  "proof": "none - pour des lecteurs qui reviennent, la valeur du prochain numéro suffit ; les chiffres d'abonnés n'ajoutent rien ici"
 },
 "mechanism": "None: des lecteurs qui reviennent connaissent déjà cette voix ; l'annonce concrète du prochain numéro porte seule la demande.",
 "ab": {
  "first": "Titre A annonçant les tarifs augmentés de 20 % ; titre B annonçant ce que la province fait à la façon de facturer",
  "why": "Les deux promesses viennent du même numéro, et savoir laquelle convainc le mieux oriente les prochains appels à l'action.",
  "sample": "Pas encore : aucun taux de conversion de référence n'est donné. Mesurez d'abord le taux de passage du gratuit au payant sur ce numéro et les suivants, puis calculez un échantillon.",
  "next": [
   "Le bouton : Passer à l'abonnement payant contre Lire le numéro sur la facturation",
   "Le bloc seul en fin de texte contre une ligne d'annonce supplémentaire après le paragraphe des dîners refusés"
  ]
 },
 "accessibility": {
  "contrast": "Texte 8.29:1 (conforme AA 4.5:1) ; bouton sur la page 7.77:1 (conforme 3:1).",
  "touch": "au moins 44 par 44 pixels pour le bouton",
  "markup": "un vrai lien vers la page d'abonnement Substack, pas un élément cliquable sans rôle",
  "keyboard": "un contour de focus visible sur le bouton, atteignable avec Tab",
  "color": "une flèche ou un soulignement en plus de la couleur du bouton"
 },
 "risks": [
  "Abonnez-vous pour plus de contenu ne donne aucune raison de payer à un lecteur qui reçoit déjà le texte gratuitement.",
  "Annoncer une suite payante puis la publier gratuitement ôterait toute valeur au prochain bloc du même genre.",
  "Un ton promotionnel jurerait avec un texte sobre qui raconte un départ et ses limites."
 ],
 "next_moves": [
  "Une version pour les lecteurs gratuits qui ne paieront pas encore : un lien de partage vers un collègue indépendant qui envisage de partir.",
  "La version agressive pour comparaison : une offre de réduction limitée dans le temps, qui ne convient pas sans vraie échéance."
 ],
 "caveats": [
  "Ce bloc suppose que le prochain numéro est réservé aux abonnés payants ; sinon, c'est une décision à prendre avant de publier ce bloc.",
  "Les 90 abonnés payants pour 1 400 gratuits restent hors du bloc : pour des lecteurs fidèles, la suite annoncée convainc mieux qu'un décompte."
 ]
}

The drop-in block the page exports for it (R.ctaHtml):

<aside class="end-cta" aria-labelledby="end-cta-title" style="border-top:1px solid #d0d7de;margin:2.5rem 0 0;padding:1.25rem 0">
  <p id="end-cta-title" style="font-weight:700;font-size:1.15rem;margin:0 0 .4rem">Dans le prochain numéro : pourquoi j&#39;ai augmenté mes tarifs de 20 % en partant de Paris</p>
  <p style="margin:0 0 .9rem">Ce que la province a changé dans ma façon de facturer, pour les indépendants qui envisagent le même départ. Ce numéro sera réservé aux abonnés payants.</p>
  <a href="#" class="end-cta-button" style="display:inline-block;min-height:44px;min-width:44px;padding:.7rem 1.1rem;border-radius:8px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:#ffffff;background:#7a3e1d">Passer à l&#39;abonnement payant <span aria-hidden="true">&rarr;</span></a>
</aside>
<!-- Replace href="#" with the real destination. Give .end-cta-button a visible :focus-visible outline in your stylesheet. -->

What step 5 prints for it: G. Value-gap tease | Passer à l'abonnement payant

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./bookkit.js"); require("./bookreply.js")), then:

const K = BookKit, R = BookReply;
const f = K.form(Object.assign(K.blank(), { title: "What we learned shipping 40 releases", article: myArticleText, language: "en",
  context: "independent", objective: "newsletter", relation: "first", stage: "na", mechanism: "none",
  fg: "#ffffff", bg: "#0f5c55", page: "#ffffff", readers: "8000", baseline: "2", mde: "20" }));
// f.type, f.openCliches, f.arch (the CTA archetype), f.ab (the A/B sample size), f.contrast, f.findings
const b = K.buildInput(f, "hooks");                          // or "cta" (set chosen and chosenTitle to carry an opening)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "hooks");
// R.check(reply, b)                                         // {status: kept or flagged, notes}
// R.ctaHtml(ctaReply, f)                                    // the CTA as an accessible HTML block

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://bookends.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"bookends"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "hooks", "article": "The finished draft ...", "brief": "Title as drafted: ...\nLanguage: English\n...", "carry": "", "facts": "Article: 252 words, ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses bookends:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A hooks reply carries summary, hooks (each with its name, lever, two candidates and what it commits the article to), titles and caveats; a cta reply carries summary, archetype, why, copy, form, mechanism, ab, accessibility, risks, next_moves and caveats. The code below prints the gist of either: each hook's name with its first candidate, or the CTA's archetype and button text.

KEY="bookends:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "cta" then "\(.archetype) | \(.copy.button)"
       else (.hooks[] | "- \(.name): \(.candidates[0])") end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: bookends:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json