Skip to content

Sending invitations from your own system

Markdown

Sending 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.

  1. 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.
  2. In the Invitation sending › Sending route step, select Our own system sends them.
  3. In the Notification address field, enter the https address 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.

POST https://crm.yourbrand.com/sayproof/notices
Content-Type: application/json
User-Agent: sayproof-webhook
X-Sayproof-Timestamp: 2026-09-28T07:30:00.123456+00:00
X-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.

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.

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);
});
  • Any 2xx response counts as success; the response body is not read. Respond within 10 seconds and send the email in the background.
  • 5xx, 408 and 429 responses, 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 4xx responses, 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 the token value. A reminder carries the same token and is told apart by the “Reminder: “ prefix at the start of the subject line.
  • The address must be https and must resolve to a public IP address. Redirects are not followed.
  • The address must use the default https port; if it contains a port number (such as :8443), the request is not sent and port_refused is 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).

  • 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.