npm install signalbird · composer require pariette/signalbirdPartner - 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.
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
import { SignalbirdPartner } from 'signalbird'
const partner = new SignalbirdPartner({
domainKey: process.env.SIGNALBIRD_DOMAIN_KEY!,
})use Signalbird\Sdk\Facades\Signalbird;
$partner = Signalbird::partner(); // config/signalbird.php → partner_keyOpening 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.
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
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:
{
"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.
// 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:
const report = await partner.companyUptime('sc_9911', '7d') // 24h | 7d | 30d{
"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,
uptimecomes back asnull, not100. 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:
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:
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.
// 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 })<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:
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.