npmjs.com ↗
Installnpm install signalbird · composer require pariette/signalbird

Partner - open your customer's account in Signalbird

If you sell Signalbird inside your own product, this surface is yours. You open the customer account, you add their domain, you take the payment and you turn the module on; the customer never goes through a separate setup in Signalbird.

This surface is contractual. The key (sb_secret_live_…) is not handed out to everyone; it is issued to platforms that have an agreement with Signalbird (a CMS, a consent management tool, a hosting panel). What sets it apart from the other four surfaces: it works across teams.

It is not a super admin. Your key reaches only the accounts you opened. Another partner's record, or a self-service customer's, returns 404: you are not even told that the record exists.

When it helps:

  • Automatic provisioning: the moment your customer opens an account with you, their Signalbird account is born too and their site goes under monitoring.
  • Selling modules: when a customer buys the email marketing module from you, that module opens on the Signalbird side.
  • Showing it on your own screen: the uptime report inside your own domain list, the live chat screen inside your own panel.

The key

The Signalbird team issues the key and gives it to you once. Keep it on your server: it never reaches a browser.

text
SIGNALBIRD_DOMAIN_KEY=sb_secret_live_…

A key of the wrong kind (a public key sb_public_live_…) is rejected at construction time with WRONG_KEY_TYPE.

Setup

ts
import { SignalbirdPartner } from 'signalbird'

const partner = new SignalbirdPartner({
  domainKey: process.env.SIGNALBIRD_DOMAIN_KEY!,
})
php
use Signalbird\Sdk\Facades\Signalbird;

$partner = Signalbird::partner();   // config/signalbird.php → partner_key

Opening a customer

Every write is made with your own identifier (external_id) and is idempotent: a second call with the same identifier does not open a new account, it returns the existing one with created: false. Your webhook firing twice causes no harm.

ts
const res = await partner.createCompany({
  external_id: 'sc_9911',                 // the customer id on your side
  name: 'Acme Web',
  owner: {
    email: 'owner@acme.com',
    name: 'Acme Owner',
    external_id: 'u_88',                  // the user id on your side
  },
})

if (res.data.created) {
  // These two keys are returned ONLY now: store them on your side.
  save(res.data.keys.domain_key)   // sb_…      → messaging/management
  save(res.data.keys.app_key)   // sb_public_live_… → chat widget, push
}

If you lose a key, rotateKey('sc_9911', 'api' | 'app') produces a new one; there is no way to get the old one back.

The owner user is born without a password: your customer enters Signalbird through an embed token. If they want one, they set their own password through the "forgot password" flow.

Adding a domain and monitoring it

ts
const res = await partner.addDomain('sc_9911', {
  external_id: 'd_5',
  domain: 'acme.com',
  monitoring: { enabled: true, frequency: 5 },   // minutes
})

The domain goes under monitoring as soon as it is added. The response also returns the TXT record that is expected:

json
{
  "domain": { "verified_via": "partner", "can_send_campaigns": false },
  "dns": [{ "host": "_signalbird.acme.com", "type": "TXT", "value": "signalbird-verify=…" }]
}

The TXT rule

A domain you add is born with verified_via: "partner". That means:

Module State
Monitoring (uptime) ✅ on
Live chat ✅ on
Push notifications ✅ on
Email / SMS campaigns ❌ off until the TXT record is published

The reason: the sending envelope leaves from Signalbird's own domain, so the sending reputation is Signalbird's. "Mail may be sent on behalf of this domain" is not a decision that can be made by declaration.

This is not a dead end: most of the time you manage your customer's DNS yourself.

ts
// 1. Publish the dns record from the response
// 2. Have it verified
const check = await partner.verifyDomain('d_5')

if (check.data.verified) {
  // domain.can_send_campaigns is now true
}

Uptime report

To show it on your own domain list screen:

ts
const report = await partner.companyUptime('sc_9911', '7d')   // 24h | 7d | 30d
json
{
  "range": "7d",
  "data": [{
    "external_id": "d_5", "domain": "acme.com", "monitored": true,
    "uptime": 99.86, "avg_response_ms": 214, "checks": 2016, "status": "up",
    "incidents": [{ "started_at": "…", "ended_at": "…", "duration_s": 420, "reason": "HTTP 503" }]
  }]
}

If no check has ever run, uptime comes back as null, not 100. Missing data is not optimism; show "no data" on your screen.

Turning modules on and off

Payment is taken by you; there is no second charge in Signalbird. When the payment completes, you turn the module on:

ts
await partner.grantModule('sc_9911', {
  module: 'email',                    // email | sms | push | chat | logger | monitoring
  limits: { max_emails: 25000 },      // if omitted, the module's default applies
  expires_at: '2027-08-20',           // the end date of the subscription
})

When expires_at arrives the module closes by itself: refresh it on renewal. To close it early:

ts
await partner.revokeModule('sc_9911', 'email')

Two situations are refused deliberately:

Code What it means
MODULE_IS_AUTOMATIC (422) This module is part of your own package (monitoring, for instance); it cannot be closed.
MODULE_REVOKED_BY_ADMIN (409) A Signalbird administrator closed this module; a sale does not reopen it.

Embedding a panel screen in your own page

The screen from Signalbird appears in your panel exactly as it is, because it is the same screen, not a copy. You mint a short-lived token on your own server and hand it to an iframe.

ts
// On YOUR server (the partner key never reaches a browser)
const { data } = await partner.createEmbedToken('sc_9911', {
  user_external_id: 'u_88',
  module: 'chat',          // chat | monitoring | campaigns | contacts | radio
  theme: 'dark',
  locale: 'en',
})

return Response.json({ url: data.url })
html
<iframe src="{{ url }}" style="width:100%;border:0" id="sb"></iframe>
<script>
  addEventListener('message', (e) => {
    if (e.data?.type === 'signalbird:height') {
      document.getElementById('sb').style.height = e.data.px + 'px'
    }
  })
</script>

The token lives for 120 seconds and is single use. Mint a new one when the page reloads. The short life is not optional: the token travels in the URL, which means it lands in server logs and in the Referer header.

To embed a screen the module must be open for that customer; if it is closed you get MODULE_DISABLED (403). The empty shell of a product nobody bought is not shown.

Users

An embed token is minted on behalf of a user, so that your support staff appear under their own name in Signalbird chat too:

ts
await partner.createUser('sc_9911', {
  external_id: 'agent_7',
  email: 'agent@acme.com',
  name: 'Support Agent',
  team_role: 'member',
})

removeUser removes the membership; it does not delete that person's Signalbird account, since the same person may be a member of another customer as well.

Linking an existing account

If your customer already has their own Signalbird account, you cannot take it over automatically. Passing link_email to createCompany opens a link request that is approved in the account owner's own panel (COMPANY_LINK_REQUIRES_CONSENT, 403).

This restriction is deliberate: a partner who knows an email address must not be able to reach that customer's data.

Error codes

Code Status What it means
PARTNER_KEY_MISSING / PARTNER_KEY_INVALID 401 The key is missing or has been revoked
PARTNER_INACTIVE 403 Your partner account is inactive
COMPANY_NOT_FOUND 404 This customer is not yours (or does not exist at all)
PARTNER_DOMAIN_LIMIT 422 Your per-customer domain limit is full
DOMAIN_OWNED_BY_OTHER_PARTNER 409 This domain is managed by another partner
MODULE_IS_AUTOMATIC 422 A module included in your package cannot be closed
MODULE_REVOKED_BY_ADMIN 409 An administrator closed it; a sale does not reopen it
EMBED_TOKEN_INVALID 401 The embed token has been used or has expired

The full method list (20 methods): docs/CONTRACT.md § 12.