Outbound guard
Check an invite, share or notification before your product sends it from your domain.
Server-side endpoint
POST https://nolgard.com/v1/outbound
{"secret":"sk_…","accountId":"user_42","kind":"invite","recipients":["a@example.com","b@example.com"],"text":"Join my team","ip":"198.51.100.9","userAgent":"Mozilla/5.0 …"}
{"decision":"allow","reasons":[]}Call this before sending any user-triggered email. Keep the secret and this call on your server.
Request fields
| Field | Type | Notes |
|---|---|---|
| secret | string | Required. Site secret. |
| accountId | string | Required. The sending account, up to 256 characters. |
| kind | string | Required. One of invite, share, notify, message. |
| recipients | string[] | Required. 1 to 100 email addresses in one call. |
| text | string | Optional. The user-controlled text that appears in the email, up to 4000 characters. |
| ip | string | Optional. IP address of the sender. |
| userAgent | string | Optional. User agent of the sender. |
Response fields
| Field | Type | Notes |
|---|---|---|
| decision | string | allow, review, or block. |
| reasons | string[] | Reason codes behind the decision. Empty on a clean allow. |
review means hold the email for manual approval. block means drop it. allow means send.
Reasons and defaults
| Reason | Default |
|---|---|
| outbound_new_account | Unknown account, or one less than 24 hours old, sending to 3 or more recipients in one call (newAccountRecipients 3) or 10 or more on its first day (newAccountDaily 10) |
| outbound_burst | More than 50 recipients in an hour or more than 200 in a day |
| outbound_domain_spread | More than 15 distinct recipient domains in an hour; freemail counts once |
| outbound_lure | Text carries a phone number of 9 or more digits, an http(s) URL or a www. host, or a billing word such as invoice, refund, renewal, subscription, payment, charged or call together with a currency amount |
| outbound_flagged_account | The account carries a usage flag or a fraud label, or its last decision was block or shadow |
Decision mapping: block for a flagged account, or for a lure from an unknown or new account. review for any other reason. Otherwise allow.
Errors
| Status | Error |
|---|---|
| 400 | invalid-input |
| 401 | invalid-secret |
| 403 | read-only (demo site) |
| 429 | rate-limited |
Webhook
outbound.abuse
{"accountId":"user_42","kind":"invite","decision":"block","reasons":["outbound_lure"],"recipients":12}recipients is a count rather than a list. The event fires once per account per day. See Webhooks and clawback for delivery and signing.
SDK
const result = await nolgard.outbound({
accountId: user.id,
kind: 'invite',
recipients,
text
});
if (result.decision === 'block') return;
if (result.decision === 'review') await holdForApproval(draft);The Node SDK fails open: if the call cannot complete, it returns an allow so your email pipeline keeps running. See SDKs.
Dashboard and testing
Settings → Outbound abuse lists recent decisions and the thresholds behind them. Thresholds are owner-editable.
Test secrets are stateless: test-bot blocks, test-limit reviews, any other test secret allows. They store nothing and send no webhooks.
Data retention
text is evaluated in memory and never stored. Only counts and hashed recipient domains are kept, for 7 days.
Limits
- Defaults are logical starting points rather than measured guarantees. Tune them for how your product sends email.
- A real user onboarding a whole team on day one looks like a new-account burst.
- Product emails that legitimately contain links, phone numbers or billing words can match the lure reason.
- Recipient domain counts group freemail into one domain, so a large consumer invite list spreads less than a corporate one.
- This endpoint decides; it does not send, queue or withhold email. Your backend owns delivery.
- Counts and hashed recipient domains cover 7 days. Older activity is not visible to the detectors.