# Sendworth API — Integration Guide for AI Coding Agents

You are integrating **Sendworth**, a spam-detection and form-triage API, into this
codebase. Follow the rules and examples below exactly. When you finish, the app should
send every contact-form / lead-form submission to Sendworth and branch on the result so
that spam is dropped and real messages are categorized.

Base URL: `https://sendworth.dev`
Endpoint: `POST /api/v1/check`

---

## 0. Golden rules (read first)

1. **Call this API from the server, never from the browser.** The API key is a secret.
   Do not put it in client-side JavaScript, a React component that runs in the browser,
   a mobile app bundle, or any public repo. Put it in an environment variable and call
   the API from a backend route / server action / serverless function.
2. **Store the key in an environment variable** named `SENDWORTH_KEY`. Never hardcode it.
3. **At least one of `email` or `message` is required** in every request.
4. **The response is two-staged.** Fast checks answer in <100ms. Anything ambiguous comes
   back with `pending: true` and is refined by the AI layer moments later. Do not block
   the user's form submission waiting for the AI — the API already handles that for you.
5. **Fail open, not closed.** If the Sendworth call errors or times out, deliver the
   message anyway (treat it as not-spam). Never lose a real customer message because the
   check failed.

---

## 1. Authentication

Every request needs the API key as a bearer token:

```
Authorization: Bearer ig_live_your_key
```

Keys look like `ig_live_…` and are created in the Sendworth dashboard. Read it from the
environment:

```ts
const SENDWORTH_KEY = process.env.SENDWORTH_KEY
```

Add `SENDWORTH_KEY=ig_live_...` to `.env.local` (and to the hosting provider's env vars).
Add `.env.local` to `.gitignore` if it isn't already.

---

## 2. The request

`POST https://sendworth.dev/api/v1/check` with a JSON body:

| Field         | Type   | Required            | Notes                                                             |
| ------------- | ------ | ------------------- | ----------------------------------------------------------------- |
| `email`       | string | one of email / message | Sender address. Checked against disposable-domain + abuse lists. |
| `message`     | string | one of email / message | The form body. What the AI layer actually reads.                 |
| `name`        | string | optional            | Sender name. Extra context for the classifier.                    |
| `source_site` | string | optional            | Label for which site/form sent it. Shown in your dashboard.       |

Headers: `Authorization: Bearer <key>` and `Content-Type: application/json`.

---

## 3. The response (`200 OK`)

```json
{
  "id": "5b0d6a7e-9c1f-4f7a-9a44-1f2c3d4e5f6a",
  "status": "pending",
  "category": "lead",
  "isSpam": false,
  "confidence": 0.5,
  "reason": "Passed fast checks — awaiting content classification.",
  "signals": {
    "disposableDomain": false,
    "senderReputation": "clean",
    "sfsConfidence": 0,
    "llmUsed": false
  },
  "pending": true,
  "usage": { "checks_used": 42, "checks_limit": 10000 }
}
```

Field meanings:

- **`isSpam`** (boolean) — the decision you branch on most of the time. `true` = junk.
- **`category`** (string) — one of the account's categories (see §4). When `pending` is
  `true` this is provisional and may be refined by the AI layer.
- **`status`** — `resolved` (a fast signal decided it; final), `pending` (fast checks
  passed, AI is refining in the background), or `classified` (AI finished; visible in the
  dashboard / via webhook).
- **`pending`** (boolean) — `true` means the category may still change. It does **not**
  mean you should wait. Treat non-spam as deliverable immediately.
- **`confidence`** (number 0–1), **`reason`** (string) — explanation, good for logging.
- **`signals`** — raw detection signals, useful for debugging/analytics.
- **`usage`** — month-to-date checks against your plan limit.

**Rule of thumb: treat `isSpam: true` as junk and everything else as deliverable.**

---

## 4. Categories

Every account starts with these four slugs. Pro accounts can add custom ones and toggle
them; the classifier only ever returns categories that are enabled.

| slug              | meaning                                                              |
| ----------------- | ------------------------------------------------------------------- |
| `spam`            | Junk, abusive, or automated garbage. Do not send or store.          |
| `solicitor`       | Cold sales pitches, agencies, outreach trying to sell you something.|
| `lead`            | A genuine potential customer worth following up with.               |
| `support-request` | An existing user or customer who needs help.                        |

Do not hardcode a category list you invented. Branch on the slugs above (or on `isSpam`),
and treat any unrecognized slug as deliverable rather than dropping it.

---

## 5. Reference integration (server-side)

Generic backend handler. Adapt the framework specifics, keep the logic:

```ts
async function checkSubmission(form: { email: string; name?: string; message: string }) {
  try {
    const res = await fetch("https://sendworth.dev/api/v1/check", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SENDWORTH_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        email: form.email,
        name: form.name,
        message: form.message,
        source_site: "your-site-name",
      }),
    })

    if (!res.ok) {
      // 401 = bad key, 429 = rate/quota limit, 4xx/5xx = other.
      console.error("Sendworth check failed:", res.status)
      return { isSpam: false, category: "unknown" } // fail open
    }

    return (await res.json()) as {
      isSpam: boolean
      category: string
      pending: boolean
    }
  } catch (err) {
    console.error("Sendworth request errored:", err)
    return { isSpam: false, category: "unknown" } // fail open
  }
}
```

Then branch:

```ts
const check = await checkSubmission(form)

if (check.isSpam) {
  return // drop it — do not email, do not store
}

// Deliver / store the real message here.
```

---

## 6. Framework snippets

### Next.js — App Router route handler

```ts
// app/api/contact/route.ts
import { NextResponse } from "next/server"

export async function POST(request: Request) {
  const form = await request.json()

  const check = await fetch("https://sendworth.dev/api/v1/check", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SENDWORTH_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email: form.email, name: form.name, message: form.message }),
  })
    .then((r) => r.json())
    .catch(() => ({ isSpam: false, category: "unknown" }))

  if (check.isSpam) {
    return NextResponse.json({ ok: true }) // silently accept + drop
  }

  // ...send email / write to DB with the real submission...
  return NextResponse.json({ ok: true })
}
```

### Next.js — Server Action

```ts
"use server"

export async function submitContact(formData: FormData) {
  const payload = {
    email: String(formData.get("email") ?? ""),
    name: String(formData.get("name") ?? ""),
    message: String(formData.get("message") ?? ""),
  }

  const check = await fetch("https://sendworth.dev/api/v1/check", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SENDWORTH_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  })
    .then((r) => r.json())
    .catch(() => ({ isSpam: false }))

  if (check.isSpam) return { ok: true }
  // ...deliver the message...
  return { ok: true }
}
```

### Express / Node

```js
app.post("/contact", async (req, res) => {
  const { email, name, message } = req.body

  let check = { isSpam: false }
  try {
    const r = await fetch("https://sendworth.dev/api/v1/check", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SENDWORTH_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email, name, message }),
    })
    check = await r.json()
  } catch (e) {
    /* fail open */
  }

  if (check.isSpam) return res.json({ ok: true })
  // ...deliver...
  res.json({ ok: true })
})
```

### With Resend (only spend a send on real messages)

```ts
import { Resend } from "resend"
const resend = new Resend(process.env.RESEND_API_KEY)

const check = await fetch("https://sendworth.dev/api/v1/check", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SENDWORTH_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email: sub.email, name: sub.name, message: sub.message }),
}).then((r) => r.json())

if (check.category === "lead" || check.category === "support-request") {
  await resend.emails.send({
    from: "forms@yourdomain.com",
    to: "you@yourdomain.com",
    subject: `New ${check.category} from ${sub.name}`,
    text: sub.message,
  })
}
```

### cURL (for testing)

```bash
curl https://sendworth.dev/api/v1/check \
  -H "Authorization: Bearer ig_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "name": "Jane Doe",
    "message": "Hi, do you offer annual billing?",
    "source_site": "acme-marketing"
  }'
```

---

## 7. Errors & limits

| Status | Meaning                                                                             |
| ------ | ----------------------------------------------------------------------------------- |
| `400`  | Invalid JSON body, or neither `email` nor `message` was provided.                   |
| `401`  | Missing, invalid, or revoked API key.                                               |
| `429`  | Rate limit (burst) **or** monthly quota exhausted. The response body says which.    |
| `503`  | Service not configured (self-hosted installs only).                                 |

- A rate-limit `429` includes `limit`, `remaining`, and `reset` so you can back off.
- Quotas reset on the 1st of each month (UTC).
- On any non-`200`, **fail open**: deliver the submission rather than dropping it.

---

## 8. Integration checklist

- [ ] `SENDWORTH_KEY` added to environment variables (not committed to git).
- [ ] Sendworth called from a **server** route/action, never the browser.
- [ ] Request sends `email` and/or `message` (at least one).
- [ ] `isSpam: true` submissions are dropped (not emailed, not stored).
- [ ] Non-spam submissions are delivered/stored as before.
- [ ] Errors and timeouts fail open (message still delivered).
- [ ] `source_site` set to a recognizable label for the dashboard.
