API9 min read

Integrate inbox placement into your SaaS

If your SaaS sends email for users, you should show them their placement. Here is how to wire up the Inbox Check API with a branded UI, webhooks and usage accounting.

Every SaaS that touches email eventually gets the same support ticket: "my emails are going to spam — please fix." You can either absorb that as a T2 cost centre forever, or you can surface placement data inside your product and let users diagnose themselves. This article walks through the second option end to end: API wiring, tenant isolation, branding, webhooks and billing.

Who this is for

ESPs, CRMs, cold-outreach platforms, marketing-automation tools, review-request tools — any product that sends email on behalf of tenants. If your users never see their own SPF/DKIM status, they will assume a spam issue is your fault. A placement widget moves that conversation from "fix my email" to "here is what your DNS says."

When this integration makes sense

There are three product shapes where an inbox-placement widget pays for itself in a quarter:

  • ESPs and marketing-automation tools. Your tenants own their own sending domains. You want a "test this campaign before send" button next to the schedule dialog.
  • Cold-outreach platforms. Tenants rotate through many domains. They need a pre-flight check for every new domain before running a sequence.
  • CRMs and support tools. Transactional email is invisible until it breaks. A quiet background check every week tells your customer success team which accounts are one SPF fix away from churning.

If your product never sends email on behalf of users, this is not the feature for you. If it does, read on.

Architecture sketch

Three moving parts. Your backend holds one Inbox Check API key. Users talk to your backend; your backend talks to Inbox Check. Webhook deliveries come back to your backend and fan out to the right tenant. No tenant ever sees the Inbox Check key, URL, or request headers.

[browser tenant UI]
       |
       v
[your backend]  --Bearer ic_live_xxx-->  [check.live-direct-marketing.online/api]
       ^
       |  HMAC-signed webhooks
       +------------------------------   [check.live-direct-marketing.online]

The reason is compliance. Inbox Check keys are billing-scoped to your company, not your tenants. If you exposed them client-side, anyone could read-out the key from DevTools and run tests on your quota.

The tenant-to-API-key mapping problem

You have one Inbox Check API key. You have N tenants. Two ways to keep them separate:

Option A: tag every test with a tenant ID

Pass metadata.tenantId on every POST /api/tests. The field round-trips back on the result and the webhook. Use it to route back to the right tenant in your database.

// your-backend: POST /internal/deliverability/test
await fetch('https://check.live-direct-marketing.online/api/tests', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.INBOX_CHECK_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from:    tenant.fromAddress,
    subject: req.body.subject,
    html:    req.body.html,
    metadata: { tenantId: tenant.id, campaignId: req.body.campaignId },
  }),
});

Option B: one sub-key per tenant

If your plan supports sub-keys, mint one per tenant and keep it in your tenant config row. More secure (a compromised sub-key blast-radius is one tenant, not all of them) and gives you per-tenant rate-limit partitioning for free. More operational surface area.

Most integrations start with A and graduate to B once they have more than a few hundred tenants.

Proxying requests (server-side only)

Your frontend calls your API, never the Inbox Check API directly. Here is the minimal Express proxy:

// routes/deliverability.ts
router.post('/tests', requireAuth, async (req, res) => {
  const tenant = req.user.tenant;

  // 1. Enforce tenant quota BEFORE calling upstream
  if (tenant.testsThisMonth >= tenant.plan.testsPerMonth) {
    return res.status(402).json({ error: 'quota_exhausted' });
  }

  // 2. Call upstream with OUR key
  const upstream = await fetch('https://check.live-direct-marketing.online/api/tests', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.INBOX_CHECK_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: tenant.fromAddress,
      subject: req.body.subject,
      html: req.body.html,
      metadata: { tenantId: tenant.id },
    }),
  });

  const { data } = await upstream.json();

  // 3. Record the test against the tenant
  await db.tests.insert({ tenantId: tenant.id, externalId: data.id });
  await db.tenants.increment(tenant.id, 'testsThisMonth');

  res.json({ id: data.id, status: data.status });
});

Displaying results — what to show, what to hide

Tenants want the headline number and one action per problem. Do not dump the raw JSON. A useful widget has three sections:

  1. Placement ring. Inbox / Spam / Missing, one line each, big number, per-provider breakdown on hover.
  2. Authentication trio. SPF pass/fail, DKIM pass/fail, DMARC pass/fail — each with a one-line action if it fails ("add include:sendgrid.net to SPF").
  3. Content score. SpamAssassin / Rspamd score with the top 3 contributing rules, plain-English.

Hide: internal seed-mailbox identifiers, raw DNS responses, per-seed timestamps, tracking-domain detail. These are debugging signals for you, not the tenant.

Branding the response

The API response has no visual assets, just data. Brand it yourself: colours, tooltips, empty-states. A few small things make a big difference:

  • Localise authentication failure messages to your product voice. "Your SPF record is missing" reads better than spf=fail.
  • Swap DNS record snippets for deep-links to your own DNS editor when possible.
  • Never expose the word "Inbox Check" in tenant UI unless your plan is marked white-label in the dashboard. The terms of service make this explicit.

Handling webhook fan-out to tenants

The webhook lands on your backend. Verify the HMAC signature, look up the test by external ID, find the tenant, route the notification through your own channels (email, in-app, Slack integration, whatever you offer).

// POST /webhooks/inbox-check
app.post('/webhooks/inbox-check',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // 1. Verify signature
    const sig = req.header('x-inbox-check-signature');
    const expected = crypto
      .createHmac('sha256', process.env.INBOX_CHECK_WEBHOOK_SECRET)
      .update(req.body)
      .digest('hex');
    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.sendStatus(401);
    }

    // 2. Find tenant via metadata we sent
    const event = JSON.parse(req.body);
    const tenantId = event.data.metadata?.tenantId;
    if (!tenantId) return res.sendStatus(200);

    // 3. Fan out
    await notifyTenant(tenantId, event);
    res.sendStatus(200);
  },
);

Usage accounting and billing

You are on the hook for the bill. Your tenants are on the hook for whatever you charge them. The simplest model: meter tests per tenant per month, include N in the base plan, charge overage. Record every test ID in your own database and reconcile against the Inbox Check invoice at month-end.

Common gotcha: a tenant triggers a test, then refreshes the page and triggers another. Deduplicate on the client (disable the button) and on the server (idempotency key scoped to tenant-minute). Otherwise you will pay for the same test three times.

Rate limit partitioning

A single runaway tenant can eat your whole per-minute limit. Enforce a per-tenant limit in your proxy — something like 10 tests per minute per tenant — before calling upstream. This way one tenant's bad Zapier loop does not starve every other tenant's placement widget.

Caching

Two cacheable things. GET /api/me — cache it for five minutes, it changes on plan updates only. The list of supported seed providers — cache it for a day. Everything else is per-test and should be fetched fresh.

Do not cache test results across tenants even if the content is identical — the placement varies by sending domain and the results belong to whoever paid for them. Cross-tenant caching is a data-leak bug waiting to happen.

Frequently asked questions

How do I white-label the widget?

Fetch the raw JSON through your proxy and render it in your own components. The API has no branded UI to remove. Contact sales to turn on a white-label plan if you want to remove the 'powered by' requirement from your public UI.

Can tenants bring their own Inbox Check API keys?

Technically yes, but it is rare. Most SaaS integrations use a single platform key because (a) it simplifies billing and (b) tenants do not want to manage a second vendor relationship. Only the biggest tenants tend to ask for BYO keys.

What happens if Inbox Check is down?

Return a graceful 503 from your proxy, surface a 'service temporarily unavailable' message in the UI, and keep the user's test payload in a retry queue. The SLA is tight but not 100% — never block a campaign send on a placement test.

Do I need to pass user PII (email addresses) to the API?

No. The 'from' address is the only identity field you need. Never send recipient addresses, tenant user emails, or any other PII. The API only tests to its own seed mailboxes — your tenants' real recipients are never in scope.
Related reading

Check your deliverability across 20+ providers

Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail and more. Real inbox screenshots, SPF/DKIM/DMARC, spam engine verdicts. Free, no signup.

Run Free Test →

Unlimited tests · 20+ seed mailboxes · Live results · No account required