How it works
API and webhooks, step by step
A key of your own, revocable
Per project, and revoking it is immediate.
Signed webhooks out
testimonial.created, testimonial.approved, testimonial.rejected and testimonial.withdrawn, each signed so your end can verify the message came from here. A test button, the last ten deliveries with what your endpoint answered and which try each was, and an endpoint that fails twenty times in a row is switched off by itself, with one email to you — a test that is answered switches it back on.
Approvals and withdrawals are tried again
An approval and a withdrawal are tried again when your endpoint does not answer, times out, or answers 408, 425, 429 or 5xx: up to 4 attempts in all, about 1 minute, 10 minutes and 1 hour apart, with the same X-Hazelsong-Delivery id each time so your end can drop a repeat. An approval is charged to the account's month of API calls before it is sent: every attempt it may need is held when it is written, and what it does not use is given back. When the month cannot hold them, it is not sent. A withdrawal is never charged and never held back for the month. testimonial.created and testimonial.rejected are attempted once, as before.
Told when a writer withdraws
testimonial.withdrawn tells your endpoint that a writer withdrew their permission: the testimonial's id and when, never its words, once per testimonial, to the one endpoint of the project that asks for it. Anything that copied it should take it down. An endpoint made before 17 Oct 2026 receives it only once it asks for it.
JSON, in the shape you would have designed
The words, the person, the rating, the source, the media, the permission scopes.
Your report's figures, in one call
GET /api/v1/report answers what the MCP server's report tools answer: the wall's showings, plays, clicks and button presses this month against last, by widget and wall and by where readers came from; the lift test's counts and verdict with its range; and how far people got through each form. The same fields a client share link shows, kept for thirty minutes.
Notes on your chart
POST /api/v1/notes with a day, up to 140 characters and an idempotency_key puts a note on the report's day-by-day chart — a deploy, a newsletter, a price change — marked as added through the API. A retry with the same key returns the note it made instead of a second one; the same key with different words is refused. One of the month's calls.
A month of calls, counted where you can read it
Every call a key or a connected assistant makes is counted against your account's month, shared by all your projects. Most calls count as one, the report's figures included, which count as four when they have to be counted afresh (at most once in thirty minutes). Reading the project's summary, asking somebody for a testimonial, and a lift test purchase or refund count as two, and approving, rejecting or deleting a testimonial, putting a testimonial in and sending an order as three; putting a testimonial in or rejecting one also counts one more for every three webhook endpoints it is sent to. An approval and a writer's withdrawal, from anywhere, are sent to webhook endpoints through an outbox that tries again: they count one, plus two for each endpoint, plus two for each endpoint each time it has to be tried again — every try is held when it is written and what is not needed is given back. The dashboard's API page and your report show how many are used; at 80% the person paying gets one email. Past the month, a call is answered 429 with a sentence that says when it starts again (the 1st, UTC), and the key keeps working from then. Removing a webhook endpoint is never counted and always works.
Safe by design
The rule that makes it safe to have
Clear boundaries
What it will not do
Every capability page ends with this list. It is the part that says what you are actually buying.
- It will not put an author's email address anywhere public. Your key reads it, because it is your customer; the widget, the wall and the public endpoint never carry it.
- It will not return testimonials that refused the website.
- It will not let an automation rewrite the request a person is sent. Who is asked is yours to decide; what they are promised is not.
- There is no write endpoint that can change a testimonial's text — that does not exist anywhere in this product.
- It will not apply a PATCH without the rev your integration read (every row and webhook carries it), or over a change made since: that is 409 changed, with the current rev, and nothing is written. A rejection or a return to Pending names its reason from the same list as the dashboard, and never a negative tone or a low rating.
- It does not bill an overage; past the month the API pauses until the 1st.
- It will not send an approval or a withdrawal the month's API calls cannot hold, and it will not resend one it refused; the API page says how many were not sent.
- It will not try a delivery more than four times, or again after a redirect or any 4xx other than 408, 425 and 429.
- It will not send an approval again once the testimonial is no longer approved, and a withdrawal never carries the words.
- The report never carries a testimonial's words, a name or an address, and never projects a figure.
- A note sent through the API says what happened on a day; it is shown beside the figures, never as their cause, and it cannot be read back or changed through the API.
Where it lives in the code: lib/api-keys.ts · lib/api-auth.ts · lib/api-meter.ts · lib/api-meter-rules.ts · lib/webhooks.ts · lib/webhook-outbox.ts · lib/webhook-outbox-rules.ts · scripts/webhook-outbox-check.mjs · lib/report-tools.ts · app/api/v1/report/route.ts · app/api/v1/testimonials/route.ts · app/api/v1/hooks/route.ts · app/api/v1/invites/route.ts · app/api/v1/lift/purchases/route.ts · app/api/v1/lift/refunds/route.ts · app/api/v1/orders/route.ts · app/api/v1/notes/route.ts · lib/notes.ts
Questions
Asked about this
From Help, where every answer is written from the product as it runs.
How much code is it?
Two lines: a script tag and a div. The layout is a query string on the script tag, or a data attribute on the div — both of which live in your page, so turning a wall into a marquee later means editing those two lines where you pasted them. There is nothing to rebuild at this end.
It shows nothing on my page.
Three usual causes, in order of likelihood: nothing is published yet (Pending is not published); the builder you are using strips script tags from block content, in which case the tag goes in the theme footer; or you are looking at the editor preview rather than the live page, which is normal on Webflow, Framer, Elementor and Squarespace.
Can I embed it in Notion or a newsletter?
No, and not because of us: neither runs a third-party script. Link the hosted wall in Notion, and use a testimonial image in the newsletter. Email has not run JavaScript for twenty years.
Does the widget slow my page down?
It is one small script with no framework behind it and no font to fetch. It draws after your page has drawn.
This is running now
Open a project and use it. Nothing on this page is a roadmap.