Sending invitations from your own system
MarkdownSending invitation emails and rejection notices from your own email system instead of Sayproof.
By default, Sayproof sends the invitation emails on your behalf. If you want the emails to be sent from your own system (a CRM, an email service or marketing automation), Sayproof sends no email. Instead, it sends a signed HTTP request (a webhook) to your system for each invitation, and your system sends the email.
If you want no invitation emails at all, with customers writing their review on your store’s order detail page, see Without invitation emails.
The timing rules do not change: the waiting time, sending hours, daily limit, reminder and customers who do not want emails are all checked on the Sayproof side. Every request that reaches your system represents an email that should be sent.
Setting it up
Section titled “Setting it up”- On the Keys tab of the Channels screen, open the Advanced: signing passwords section and press Create notification address password. The password is shown only once; copy it and store it in your system. This password is used only for this purpose and cannot sign order transfers.
- In the Invitation sending › Sending route step, select Our own system sends them.
- In the Notification address field, enter the
httpsaddress of the endpoint on your system (up to 500 characters).
If the password or the address is missing, the setting is not saved. The sending route and the notification address can only be changed by a user signed in to the panel; a change request made with an API key receives 403 refused / panel_session_required.
The request
Section titled “The request”POST https://crm.yourbrand.com/sayproof/noticesContent-Type: application/jsonUser-Agent: sayproof-webhookX-Sayproof-Timestamp: 2026-09-28T07:30:00.123456+00:00X-Sayproof-Event-Id: 7d0c2f3e-…X-Sayproof-Signature: 5f1a…{ "address": "jane@example.com", "context": { "brand": "Your brand", "channel": "web", "landing_url": "https://www.yourbrand.com/write-a-review", "line_references": ["1"], "order_reference": "ORD-1001" }, "event_id": "7d0c2f3e-…", "language": "en", "message_class": "transactional", "subject": "How was your order?", "template_key": "default", "text": "…", "token": "sp_…"}| Field | Meaning |
|---|---|
address |
The recipient’s email address |
subject |
The subject line. For reminders, “Reminder: “ is added at the start. |
text |
The plain-text content of the email; it includes the review link and the link to stop receiving emails |
token |
The token used in the review link |
context.landing_url |
The page the invitation link opens |
context.channel, context.brand |
The code of the channel the order came from and the brand name to use in the email (the channel’s Name shown to customers, or the brand name if it is empty). If your channels appear to customers under different names, choose the email design by these fields. |
context.order_reference, context.line_references |
The order and order lines the invitation belongs to |
language |
tr or en |
message_class |
transactional (a transactional email) |
template_key |
The key of the invitation email used; for rejection notices, rejection:<reason> |
event_id |
The request’s ID; the same as the X-Sayproof-Event-Id header |
If you use your own email design, build the review link by adding #review_token= and the token value to the end of the landing_url address:
https://www.yourbrand.com/write-a-review#review_token=sp_…Because the token sits in the part of the address after the # sign, the browser does not send it to the server and it does not reach the page address recorded by analytics tools. The script also reads links in the older ?review_token=sp_… form. The link in text is prepared in this form.
The link to stop receiving emails in text must always be included in your email.
Shopping experience invitations created for cancelled or returned lines are delivered in the same way; the subject line and text are prepared for that invitation. See Invitations and emails.
Rejection notices
Section titled “Rejection notices”The notice to be sent to the author of a review rejected by a moderator is delivered to the same address. In these requests, template_key has the form rejection:<reason>, context is empty, and text contains the rejection reason and the correction link. A rejection notice is a service notice and must be sent without delay. Do not change the text: the Turkish Advertising Board’s Guideline on Consumer Reviews treats steering the consumer towards changing their review in a rejection notice as a manipulative practice.
Verifying the signature
Section titled “Verifying the signature”The signature has the same form as in order transfer, but it is calculated with the Notification address password, not a signing password: hex(HMAC-SHA256(password, time + "." + body)). Read the body as raw bytes; if you parse the JSON and convert it back to text, the signature will not match.
import express from "express";import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
app.post("/sayproof/notices", express.raw({ type: "application/json" }), (request, response) => { const timestamp = request.get("X-Sayproof-Timestamp") ?? ""; const given = Buffer.from(request.get("X-Sayproof-Signature") ?? ""); const expected = Buffer.from( createHmac("sha256", process.env.SAYPROOF_NOTICE_SECRET).update(`${timestamp}.`).update(request.body).digest("hex") ); const fresh = Math.abs(Date.now() - Date.parse(timestamp)) < 5 * 60 * 1000; if (!fresh || given.length !== expected.length || !timingSafeEqual(given, expected)) { return response.sendStatus(401); } queueEmail(JSON.parse(request.body.toString("utf-8"))); response.sendStatus(202);});Response and retries
Section titled “Response and retries”- Any
2xxresponse counts as success; the response body is not read. Respond within 10 seconds and send the email in the background. 5xx,408and429responses, timeouts and connection errors are treated as temporary: the send is retried after half an hour, 1 hour, 2 hours and 4 hours. If the fifth attempt also fails, the invitation’s status becomes “Failed”. Rejection notices are retried at the same intervals.- For other
4xxresponses, and when the address does not meet the rules, the invitation becomes “Failed” at once. - Every attempt is a separate send record and carries a new
event_id. If your system received the request but the response time ran out, the same invitation may arrive again; de-duplicate with thetokenvalue. A reminder carries the sametokenand is told apart by the “Reminder: “ prefix at the start of the subject line. - The address must be
httpsand must resolve to a public IP address. Redirects are not followed. - The address must use the default
httpsport; if it contains a port number (such as:8443), the request is not sent andport_refusedis written to the send log.
Every send is recorded on the Message log screen. For failed sends, the reason (for example http_status: 503) is shown in the details. Once your system is fixed, invitations in the “Failed” state can be sent again with the Try failed ones again button on the Invitations screen; they carry the same token with a new event_id. Invitations whose link has expired are skipped (Invitations).
Your responsibilities
Section titled “Your responsibilities”- Send the email to the
address, with the subject line and content from the request. - Do not add coupons, discounts, product recommendations or campaigns to the email. The invitation is a transactional email and therefore does not require the customer’s separate consent; once promotional content is added, it becomes a commercial electronic message.
- Do not remove the link to stop receiving emails.
- Send the rejection notices as well.