PhishGuard API
Submit emails for phishing analysis, retrieve verdicts, and stream verdict updates to your SIEM via signed webhooks. The PhishGuard Gmail add-on uses these same endpoints — it doubles as a reference client you can study.
Base URL: https://api.phishguard.socshield.dz
QUICKSTART
- Get your tenant provisioned by emailing sales@socshield.dz.
- Sign in to your tenant dashboard and create an API key under
/api. Plaintext keys are shown once at creation — copy them then. - POST your first email to the ingest endpoint:
curl -X POST https://api.phishguard.socshield.dz/api/v1/email/ingest \
-H "X-API-Key: $PHISHGUARD_KEY" \
-H "Content-Type: application/json" \
-d '{
"message_id": "<abc123@mail.gmail.com>",
"subject": "Urgent: verify your account",
"from": "no-reply@suspicious.example",
"to": "alice@your-company.com",
"raw": "Received: from ...\nFrom: ...\nSubject: ...\n\nBody here"
}'You get back an email_id and a verdict status that starts as pending. The verdict moves to phishing, safe, or unsure after analysis. Subscribe to the verdict.set webhook to get notified the moment the verdict lands.
AUTHENTICATION
Every request must include your API key in the X-API-Key header. Keys are scoped to a tenant and tied to a list of allowed scopes (ingest, read, etc.) which you set at creation.
X-API-Key: pg_<16-char-prefix>_<48-char-secret>
- IP allowlist — optional, set per key. Requests from outside the allowlist return
403 ip_not_allowed. - Expiry — optional, set per key. Expired keys return
401 expired_key. - Tenant status — if your tenant is
suspendedorarchivedall keys return403regardless of validity. - Rotation — rotate a key from the dashboard; the old one is revoked immediately and the new plaintext is shown once.
Every response carries an X-Request-Id header. Paste it in any support email and we'll find your request in our logs immediately.
RATE LIMITS & QUOTAS
PhishGuard enforces two limits independently: a per-key sliding-60-second rate limit (set per key, defaults to 60/min) and a per-tenant 30-day email-ingest quota (set per tenant by your account manager).
Every response includes these headers (omitted on keys with no rate limit):
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 47 X-RateLimit-Reset: 1716800000 # unix seconds
When a limit is exceeded the API returns 429 with a JSON body that explains which limit you hit:
{
"error": "Rate limit exceeded",
"code": "rate_limited",
"limit": 60,
"used": 60,
"retryAfterSec": 60,
"request_id": "req_..."
}The /api/v1/email/ingest endpoint is the only one that counts toward your monthly quota. Read endpoints (e.g. /email/<id>) and notification polls don't.
ENDPOINTS
/api/v1/email/ingestscope: ingestSubmit an email for analysis. Returns immediately with a verdict status of pending; subscribe to the verdict.set webhook to be notified once analysis completes.
Request body (JSON)
{
"message_id": "<RFC-822 Message-ID>", // required, used for dedup
"subject": "string",
"from": "sender@example.com",
"to": "recipient@example.com",
"raw": "<full RFC-822 message>", // required, the engine parses headers + body
"reporter": { "email": "user@example.com" } // optional, attribution
}Response
{
"email_id": "uuid",
"verdict": "pending",
"request_id": "req_..."
}curl -X POST https://api.phishguard.socshield.dz/api/v1/email/ingest \
-H "X-API-Key: $PHISHGUARD_KEY" \
-H "Content-Type: application/json" \
-d '{
"message_id": "<abc123@mail.gmail.com>",
"subject": "Urgent: verify your account",
"from": "no-reply@suspicious.example",
"to": "alice@your-company.com",
"raw": "Received: from ...\nFrom: ...\nSubject: ...\n\nBody here"
}'/api/v1/email/<email_id>scope: readFetch a previously-ingested email, its verdict, and the verdict-setter (if any).
curl https://api.phishguard.socshield.dz/api/v1/email/<email_id> \ -H "X-API-Key: $PHISHGUARD_KEY"
Response (truncated)
{
"email": {
"id": "uuid",
"subject": "...",
"from": "...",
"verdict": "phishing", // pending | phishing | safe | unsure
"verdict_reason": "Brand impersonation + suspicious link",
"verdict_set_at": "2026-05-27T10:13:58Z",
"verdict_set_by": "analyst@socshield.dz",
"created_at": "2026-05-27T09:55:00Z"
}
}/api/v1/notifications/label-updatesscope: readLong-polling endpoint used by the Gmail add-on (and any reference client) to fetch verdict updates since a timestamp. Not metered against your monthly quota.
curl "https://api.phishguard.socshield.dz/api/v1/notifications/label-updates?since=2026-05-01T00:00:00Z" \ -H "X-API-Key: $PHISHGUARD_KEY"
Returns up to 500 verdict-changes newer than ?since, ordered oldest-first. Use the latest verdict_set_at in the response as the since param on your next call.
WEBHOOKS
Register a webhook from your dashboard at /api → Webhooks. We POST a signed JSON payload to your URL whenever a subscribed event fires. The only event currently fired is verdict.set.
Headers we send
X-PhishGuard-Signature: <hex HMAC-SHA256 of the raw body> X-PhishGuard-Event: verdict.set X-PhishGuard-Delivery: <unique-delivery-id> User-Agent: PhishGuard-Webhook/1.0
Sample payload
{
"event": "verdict.set",
"timestamp": "2026-05-27T10:14:00Z",
"tenant_id": "uuid",
"webhook_id": "uuid",
"data": {
"email_id": "uuid",
"verdict": "phishing", // phishing | safe | unsure
"reason": "Brand impersonation + suspicious link",
"set_by": "analyst@socshield.dz",
"set_at": "2026-05-27T10:13:58Z"
}
}Verification (compute the HMAC over the RAW body, not the parsed JSON)
import { createHmac, timingSafeEqual } from 'crypto'
function verify(rawBody, signatureHeader, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
// Constant-time compare to avoid timing attacks.
const a = Buffer.from(signatureHeader, 'hex')
const b = Buffer.from(expected, 'hex')
return a.length === b.length && timingSafeEqual(a, b)
}RELIABILITY
If your endpoint returns non-2xx or fails to respond, we retry on an exponential schedule: 1s → 5s → 30s → 5min → 1h → 6h (six retries, seven total attempts). After that the delivery is marked dead and you can re-fire it manually from the dashboard. 4xx responses are treated as permanent failures and not retried.
ERRORS
Every error response is JSON with the same shape:
{
"error": "Human-readable message",
"code": "machine_code",
"request_id": "req_..."
}| Status | Code | Meaning |
|---|---|---|
| 401 | missing_key | No X-API-Key header |
| 401 | invalid_key | Key is not recognised or hash mismatch |
| 401 | invalid_key_format | Key does not match the pg_<prefix>_<secret> shape |
| 401 | expired_key | Key passed its expires_at timestamp |
| 403 | ip_not_allowed | Source IP is not on this key's allowlist |
| 403 | insufficient_scope | Key lacks the scope this endpoint requires |
| 403 | tenant_suspended | Tenant has been suspended — contact sales |
| 403 | tenant_archived | Tenant has been archived |
| 400 | validation_error | Request body failed schema validation |
| 409 | conflict | Resource conflict (e.g. duplicate slug) |
| 429 | rate_limited | Per-key rate limit exceeded |
| 429 | quota_exceeded | Tenant monthly ingest quota exceeded |
| 500 | internal | Unexpected server error — contact support with the request_id |
Tailored for your environment
Looking for higher quotas, dedicated infra, on-prem deployment, or a custom SLA? PhishGuard is sold direct. Email sales@socshield.dz with your use case and we'll get back to you within one business day.