1. Reference
  2. Integration reference

​
WhooshBang integration reference

What the quickstart deliberately skipped: the rules you need once a send sits behind a retrying job queue and a real user is waiting on it.

Every request, response, and problem document here is checked against the published V1 contract by npm run docs:snippets.

​
Idempotency

Every send requires an Idempotency-Key. Choose one derived from the thing that caused the notification — the job ID, the order ID, the event ID — not a fresh random value per attempt. A random key per attempt makes the header useless, because a retry looks like a new message.

Repeating a send with the same key and an equivalent payload returns the original message, unchanged. No second message is created and no second Telegram notification appears. Reading the state of the returned message tells you what happened to the original.

Reusing the same key with materially different input is a conflict:

{
  "type": "https://whooshbang.flowxo.com/problems/idempotency_conflict",
  "title": "Idempotency conflict",
  "status": 409,
  "detail": "The key was already accepted with materially different validated input.",
  "code": "idempotency_conflict",
  "diagnostic_id": "diag_idem_conflict_001",
  "retryable": false
}

That is a bug in the caller, not a transient failure — retrying it will never succeed. Either you reused a key you should not have, or you changed a message you had already committed to sending.

Keys are scoped to the authenticated environment and the operation. The same key on a test send and a live send are unrelated.

Credential create and rotate take no idempotency key. Their secret is a one-time reveal, so there is nothing safe to replay: if the call fails in transit, issue a replacement and revoke the one you cannot see.

​
What “sent” means

WhooshBang reports what it can prove and nothing more. A provider returning a successful response proves it accepted the message: Slack posted it, Telegram created it, or the email provider took it for delivery. It does not prove a device displayed it, or that a person read it. There is no delivered state, and there will not be one invented for you.

Machine stateShow your usersMeans
accepted, dispatch_pendingAcceptedWhooshBang durably owns the message
queued, sendingQueuedNot yet accepted by the provider
provider_acceptedSentThe provider accepted it
retry_waitRetryingA safe retry is scheduled
terminal_failedCouldn’t sendNo further attempt will be made
outcome_unknownOutcome unknownSee below
cancelled, expiredCancelled or expiredNever reached the provider

sending shares the “Queued” label on purpose. Until the provider accepts, nothing has been sent, and an in-flight call is honestly still a message waiting on the provider. The per-attempt history distinguishes them.

Machine states are stable and never translated. The labels are what you put in front of a human.

​
Diagnosing a failure

Every response carries a WhooshBang-Diagnostic-Id header, and every message and problem document carries a diagnostic_id. Log it. It is the identifier support can look up, and it is safe to put in your own logs and error reports — it identifies the request, not its content.

A rejected request returns a Problem Details document; a message that failed after acceptance carries the same shape as classified_error under delivery. Either way the fields you read are the same:

{
  "type": "https://whooshbang.flowxo.com/problems/subscriber_unbound",
  "title": "Subscriber unbound",
  "status": 422,
  "detail": "No authorized channel binding is available for this subscriber and notifier.",
  "code": "subscriber_unbound",
  "diagnostic_id": "diag_subscriber_unbound_001",
  "retryable": false
}

retryable is the field to branch on. It tells you whether another attempt could plausibly succeed; code tells you what to do about it:

CodeWhat it usually means
subscriber_unboundThe subscriber never authorized, or revoked. Issue a new subscription link.
environment_mismatchA test credential reached for live, or the reverse — including a test credential writing project-wide recipient configuration, which is what live recipients see.
request_invalidThe request does not satisfy the published contract. field_errors names each offending field as a JSON Pointer, with a code saying which kind of wrong it was — required_missing, unexpected_property, type_invalid, format_invalid, pattern_invalid, length_invalid, range_invalid, value_invalid. The value you sent is never echoed back.
scope_forbiddenThe credential lacks the scope for this operation — or, with a <gate>_gate field error, the organization is not yet on the invited-alpha allowlist. A session that names no organization gets this too; its detail says so, because every project belongs to a WhooshBang organization.
idempotency_conflictSame key, different payload. See idempotency.
rate_limitedSlow down and retry after the indicated wait.
provider_retryableTransient; WhooshBang is already retrying if a retry is scheduled.
provider_terminalThe provider refused permanently. Do not retry.
provider_outcome_unknownSee unknown outcomes.
internal_errorA fault inside WhooshBang, not in your provider or your own service. Retry; if it keeps failing, send support the diagnostic_id.

Problem documents never disclose rate-limit internals or provider details, and a request for a resource in another organization returns the same resource_not_found as one that does not exist. That is deliberate: it means an attacker holding a valid credential cannot use error differences to discover what exists elsewhere.

The dashboard shows the same information as a per-attempt timeline, with the plain-language labels above, if you would rather look than parse.

​
Waiting on an invitation

WhooshBang is invite-only while it is in alpha. There is no endpoint that reports whether your organization has been invited, and there will not be one: the programme is temporary, and freezing our rollout-gate names into the V1 contract would leave you and us carrying them afterwards.

The refusal is the answer. A gated operation returns 403 scope_forbidden with a field error whose code is the gate plus _gate — external_sign_up_gate when creating a project, subscription_creation_gate when issuing a subscription link, live_send_gate when sending to a real recipient. The JS SDK reads it for you:

try {
  await whooshBang.createMessage(request, { idempotencyKey: event.id });
} catch (error) {
  if (error instanceof WhooshBangProblemError && error.pendingInvitation) {
    // "live_send" — this organization has not been invited to this capability yet.
    report(`Waiting on a WhooshBang invitation for ${error.pendingInvitation}.`);
  }
}

Treat it as “not yet”, not as a permission bug: nothing about your credential or your code needs to change, and the same call succeeds once the invitation lands.

​
Unknown outcomes

Sometimes a provider call may have completed but produced no trustworthy result — a timeout after the request was already in flight, for example. WhooshBang records this honestly rather than guessing:

{
  "id": "msg_synthetic_002",
  "project_id": "project_synthetic",
  "environment": "test",
  "subscriber_id": "customer_456",
  "notifier_id": "default",
  "binding": {
    "id": "binding_synthetic_002",
    "project_id": "project_synthetic",
    "environment": "test",
    "subscriber_id": "customer_456",
    "notifier_id": "default",
    "channel": "telegram",
    "status": "active"
  },
  "delivery_target": "simulator",
  "state": "outcome_unknown",
  "accepted_at": "2026-07-25T17:45:00Z",
  "updated_at": "2026-07-25T17:45:03Z",
  "expires_at": "2026-07-26T17:45:00Z",
  "delivery": {
    "attempt_count": 1,
    "highest_proven_provider_state": "none",
    "retry_scheduled": false,
    "classified_error": {
      "code": "provider_outcome_unknown",
      "retryable": false,
      "detail": "The provider may have accepted the request, but no trustworthy result is available.",
      "diagnostic_id": "diag_provider_unknown_001"
    }
  },
  "diagnostic_id": "diag_message_unknown_001",
  "outcome_guidance": "Delivery may have been accepted by the provider. No automatic retry is scheduled; create a new message explicitly if another attempt is appropriate."
}

outcome_unknown is not a failure and not a success. No automatic retry is scheduled, because retrying could put a second visible message in front of the user. The decision is yours, and you make it by sending a new message with a new idempotency key:

const current = await whooshBang.getMessage(message.id);
if (current.state === "outcome_unknown") {
  // A new logical send, not a replay: reusing the original key would return
  // the original message and change nothing.
  const resend = await whooshBang.createMessage(request, {
    idempotencyKey: `${originalKey}-resend-1`,
  });
  if (resend.outcome !== "accepted") {
    // They can no longer be reached on this notifier; nothing was sent.
  }
}

For a password reset, resending is usually right — a duplicate is cheap and a missing message is not. For “your payment succeeded”, a duplicate may be worse than silence. WhooshBang cannot make that call for you, so it does not.

The same reasoning applies to a transport failure on your side. If createMessage throws WhooshBangOutcomeUnknownError, the send may have been accepted. Read the message by its idempotency key — repeat the same request with the same key, which returns the original if it exists — before deciding to send anything new. A plain WhooshBangTransportError means nothing reached the service, and a new attempt is safe.

​
Channel connections

A channel connection is one environment’s use of a provider identity — the bot a recipient actually sees. It belongs to exactly one environment, so test and live never share an identity, a subscriber, a binding, or a credential.

Three setup modes, and none of them falls back to another:

modeIdentityCredential
whooshbang_sharedA disclosed WhooshBang-owned botOurs. You cannot read, rotate, or retire it, and no reference to it appears anywhere in this API.
customer_managedA bot Telegram creates on your own Telegram accountFetched server to server. It never passes through a browser, a clipboard, a request, or a response.
customer_byokA bot you already ownYou send it once in credential. Write-only: sealed on arrival, never echoed by any later read, absent from every connection record.

whooshbang_shared is a supported product option rather than a migration step. It suits notifying yourself from an agent, an automation, or a webhook, and it has no retirement gate. What it does not do is carry your brand, which is what the other two are for.

An existing bot cannot become customer_managed. Telegram fixes a bot’s manager when the bot is created, so a bot you already have can only be connected with customer_byok.

​
Routing decides what actually sends

Creating a connection does not, by itself, make anything send through it. A notifier routes through exactly one connection at a time, and a connection nothing routes to is inert: it delivers nothing, and a subscriber cannot authorize against it either.

  • The first connection in an environment whose default notifier has no route is attached automatically. There is nothing to displace, so the only thing that changes is a dead end.
  • Every later connection lands unrouted. Promoting it is attachNotifierConnection, and it demotes the previous default in the same transaction.
  • Detaching the default leaves the notifier with none, which stops its sends. Nothing is promoted in its place, because failing over would put an identity in front of subscribers who consented to a different one.

Subscribers who authorized a connection keep that consent when it stops being default; they stop being deliverable until it is default again. Consent is never moved between identities.

If sends are accepted and nothing arrives, read the routes first. An environment holding connections and no default route is the ordinary shape of that symptom.

​
Health, repair, and retirement

health is unknown, healthy, degraded, or unhealthy. degraded still delivers on purpose — the identity a subscriber consented to has not changed, only its last health check, and refusing to send would turn a provider hiccup into a silent outage.

Archiving a connection is terminal. It stops that environment’s delivery and authorization and drops its routing. Reconnecting later is a new connection, so consent given to the archived identity is never revived under it. Archiving a customer-owned connection additionally removes the webhook at Telegram and destroys the stored credential, handing your bot back.

Retiring is yours for every mode, whooshbang_shared included. Retiring a shared connection ends this environment’s use of the WhooshBang bot and nothing else: the bindings that consented through it are invalidated exactly as a customer-owned retirement invalidates its own, and the notifier stops routing here. The bot itself is untouched — no credential retired, no webhook removed — because every other environment that selected it is still sending through it, and their subscribers consented to their own connection, never to yours.

What the shared identity does refuse is everything that would change WhooshBang’s own bot: rotating its credential, repairing its webhook, and purging it. Those are ours to run. Retirement is not one of them, and the refusals name only what they refuse.

Checking its health is not one of them either. health-check works on a shared connection: WhooshBang runs the check against its own bot on your behalf and records the result, so the identity you depend on has an answer rather than an unknown you cannot resolve. The bot’s credential is never exposed by it. A freshly selected shared connection is checked once at selection for the same reason; if the provider cannot be reached, the connection is still created and its health stays unknown rather than being reported unhealthy on the strength of our own outage.

​
Isolation

The credential determines the organization, project, and environment. There is no parameter that widens that scope, and nothing you can pass to reach another tenant’s data.

A whooshbang_shared connection is isolated on exactly the same terms even though the transport identity is WhooshBang’s. Two projects selecting the same shared bot share no notifier, subscriber, binding, authorization token, message, attempt, audit record, or rate limit. Provider chat identity alone is never a tenant selector: inbound updates resolve an exact authorization or a signed binding handle before any tenant state is touched, and an ambiguous command fails closed rather than guessing.

A credential from one project used against another project’s resource returns resource_not_found — the same response as a resource that does not exist. It does not confirm that the resource exists elsewhere, and it does not tell you whose it is. Do not treat resource_not_found as proof of absence when debugging; check which credential you used first.

Content is encrypted at rest and retained only as long as the alpha retention window allows. Delivery metadata and diagnostics outlive it, so an old message can still explain what happened to it after its text is gone.

Message text never appears in WhooshBang logs or metrics, and neither does any Telegram username, user ID, or chat ID. If you need message content in your own observability, log it on your side before you send it.