Skip to content

SMS Provider Connectors

Floh supports two SMS providers as built-in connectors: Twilio (primary) and Vonage (secondary). Both support general SMS messaging and OTP verification via their respective Verify APIs.

Twilio

Setup

  1. Create a Twilio account at twilio.com.
  2. Note your Account SID and create a scoped API Key (SID SK… + secret) under Account → API keys & tokens. Since connector v2.0.0 an API Key is required — the account Auth Token can no longer authenticate REST calls.
  3. Purchase or configure a phone number (long code, toll-free, or short code) with SMS capability.
  4. In Floh Admin → Connectors, create a new connector of type twilio.
  5. Fill in the connection config:
  6. accountSid — Your Twilio Account SID
  7. apiKeySid + apiKeySecret — Your scoped API Key pair (required; used for sending, Verify, and webhook provisioning)
  8. fromNumber — The Twilio phone number to send from (E.164 format, e.g. +14155551234)
  9. verifyServiceSid (optional) — A Twilio Verify Service SID to enable provider-managed OTP mode

Signing field. The webhook-signing field (webhookSigningSecret) is hidden and server-managed — it is written by automated webhook provisioning/rotation, not entered by hand. See Twilio Inbound SMS Webhooks → How Floh verifies a Twilio webhook.

Twilio Verify (Optional)

When verifyServiceSid is configured, the verify_contact step with deliveryChannel: "sms" delegates OTP issuance and verification to Twilio's Verify API instead of self-issuing codes. Benefits:

  • Carrier-level fraud signals (SIM-swap detection)
  • Provider-side attempt budgets and per-number throttling
  • Voice and email fallback channels (configured on the Verify Service)

To set up:

  1. In the Twilio Console, navigate to Verify → Services and create a service.
  2. Copy the Service SID (starts with VA...).
  3. Paste it into the connector's verifyServiceSid field.

Without verifyServiceSid, OTPs are self-issued (generated, hashed, and dispatched by Floh).

Webhook Configuration

Configure Twilio to send delivery status callbacks and inbound messages to Floh:

  • Status Callback URL: https://<your-floh-domain>/api/sms/webhook/<connectorId>
  • Inbound Message URL: https://<your-floh-domain>/api/sms/inbound/<connectorId>

Set these on your Twilio phone number's configuration page under "Messaging" → "A message comes in" (for inbound) and per-message via the API (for status callbacks — Floh sets this automatically on each send).

Twilio webhooks are authenticated via X-Twilio-Signature (HMAC-SHA1 over the callback URL + sorted form parameters, using the connector's webhookSigningSecret). Floh verifies this signature using crypto.timingSafeEqual before processing any webhook payload. Unsigned or mismatched requests are rejected with HTTP 403.

The signing secret (webhookSigningSecret)

webhookSigningSecret is the HMAC key Floh verifies webhooks against. Since v2 it is hidden and server-managed — populated by automated provisioning/rotation, not entered by hand. You have two options with very different blast radii:

  • Recommended (least privilege): send with the scoped API Key (apiKeySid + apiKeySecret) and verify webhooks with a Twilio SharedKey (configured for HMAC_SHA1) whose secret Floh auto-stores in webhookSigningSecret via the provisioning endpoint. This keeps the master account Auth Token out of Floh's database entirely.
  • Legacy (signing-only): the account Auth Token is what Twilio signs with by default, so storing it in webhookSigningSecret verifies inbound webhooks with no extra setup. Since v2 this covers signing only (sending needs an API Key), and — because the field is hidden — it must be written via the connector-config API, not the console. The trade-off is that Floh then holds the master account credential (billing, number management, sub-accounts). Prefer the API Key + SharedKey pairing for anything beyond a trial.

Note Floh's verifier is HMAC-SHA1 only; a SharedKey Setting configured for HMAC_SHA256 will not verify.

Behind a reverse proxy: TRUST_PROXY

Floh verifies the signature against the URL it reconstructs from the request (${protocol}://${host}${url}). Behind a TLS-terminating proxy or ingress, set TRUST_PROXY=true so Fastify honors X-Forwarded-Proto/X-Forwarded-Host and rebuilds the public URL Twilio actually signed. Without it, inbound requests 403 with sms.webhook_signature_rejected. See Configuration.

For the full step-by-step (SharedKey creation, Rules, verification, opt-out behavior, troubleshooting), see:

A2P 10DLC Registration (US)

For US-destined messages from local long codes, carriers require A2P 10DLC registration:

  1. Register your brand in the Twilio Console under Messaging → Trust Hub → Brands.
  2. Create a Campaign (use case) under Messaging → Trust Hub → Campaigns.
  3. Associate your Twilio phone number with the campaign.

Without registration, US carriers will filter messages from unregistered 10DLC numbers. Toll-free and short code numbers have separate registration processes.


Vonage

Setup

  1. Create a Vonage account at vonage.com.
  2. Note your API Key and API Secret from the Vonage Dashboard.
  3. Purchase or configure a virtual number with SMS capability.
  4. In Floh Admin → Connectors, create a new connector of type vonage.
  5. Fill in the connection config:
  6. apiKey (required) — Your Vonage API key
  7. apiSecret (required) — Your Vonage API secret (stored encrypted)
  8. from (required) — Default sender: either an E.164 phone number (e.g. +14155551234) or an alphanumeric sender ID (2–11 characters, e.g. FlohAlerts) depending on country and carrier agreement
  9. signingSecret (required for webhooks) — Webhook signing secret for JWT verification of inbound/status callbacks (stored encrypted). If absent, Vonage webhook routes reject all requests for this connector
  10. applicationId (optional) — Vonage Application ID, required for Vonage Verify v2 flows
  11. privateKey (optional) — Application private key PEM, required for Vonage Verify v2 flows (stored encrypted)

Vonage Verify v2 (Optional)

When both applicationId and privateKey are configured on the connector, the verify_contact step automatically uses Vonage Verify v2 for provider-managed OTP mode. Implicit activation: unlike Twilio (which requires an explicit verifyServiceSid), Vonage activates provider-verify mode as soon as the Verify credentials are present. To use Vonage for basic SMS only (without provider-managed OTP), leave applicationId and privateKey empty.

Benefits of Vonage Verify v2:

  • Server-side rate limiting and attempt budgets
  • Fraud detection signals
  • Silent auth and other advanced channels (if configured on the Vonage Application)

Webhook Configuration

Configure Vonage to send status callbacks and inbound messages to Floh:

  • Status URL: https://<your-floh-domain>/api/sms/webhook/<connectorId>
  • Inbound URL: https://<your-floh-domain>/api/sms/inbound/<connectorId>

Set these on your Vonage Application's Messages capability settings.

Vonage webhooks are authenticated via JWT (HS256) signed with the signingSecret. Floh verifies the signature including the iat claim (rejects tokens older than 5 minutes).

A2P 10DLC Registration (US)

US carriers require A2P 10DLC registration for local long codes, regardless of provider:

  1. Register your brand in the Vonage Dashboard under Trust & Safety → Brands.
  2. Create a Campaign for your use case.
  3. Associate your Vonage virtual number with the campaign.

The process mirrors Twilio's — without registration, US carriers will filter messages from unregistered 10DLC numbers.


Opt-Out Compliance

Both providers require opt-out compliance for US/UK/EU messaging:

  • STOP keyword handling: Floh automatically processes inbound STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, and QUIT messages. Recipients who send these are recorded in the opt-out store and will not receive further messages. The platform-internal critical_alert category bypasses opt-out by design (see Critical Alert Hook below).
  • START/UNSTOP: Recipients can re-subscribe by sending START, YES, or UNSTOP.
  • Opt-out is per-connector: A recipient who opts out of one connector's number can still receive messages from a different connector. Note: TCPA opt-outs attach to the recipient–brand relationship, not to a specific phone number. If your organization uses multiple SMS connectors under the same brand, operators are responsible for ensuring cross-connector suppression (e.g. by configuring all connectors to use the same sender number, or by maintaining an external suppression list). A platform-wide opt-out store is tracked in #446.

Quiet Hours

Quiet hours are not currently enforced by Floh (#445). Workflow authors should use condition steps or scheduling to avoid sending non-urgent messages outside business hours for the recipient's timezone.


Which Provider to Choose

Criteria Twilio Vonage
Global coverage Excellent Excellent
Verify API (OTP) Verify v2 (mature) Verify v2
Delivery receipts Real-time webhooks Real-time webhooks
Test credentials Yes (magic numbers) Yes (sandbox)
Pricing Current rates Current rates
A2P 10DLC support Full, integrated Full, integrated

Recommendation: Use Twilio as the primary connector for its mature ecosystem and test tooling. Add Vonage as a secondary for multi-vendor resilience or procurement diversity.


Critical Alert Hook

The CriticalAlertService reuses whichever SMS connector is named in the system settings (Admin → Critical Alerts → SMS Connector). It dispatches platform-level alerts for:

  • Connector authentication failures (consecutive 401s exceeding threshold)
  • Stuck critical-severity workflow runs
  • Webhook signature verification failure spikes

Recipients are Floh users plus optional labeled phone lines (for example a NOC line), configured in Admin → Critical Alerts. A user's phones are not copied into settings. At send time a critical alert texts every valid number on that profile: the on-call phone and the regular phone, once when they are the same number. An invalid on-call number is skipped and the valid regular number is still texted. Other SMS addressed to the person uses the on-call number when it is valid, otherwise the regular phone. A workflow send_sms step that already names a number keeps that number. Critical alerts are never drawn from workflow contact lists or the opt-out store. They bypass opt-out checks and are rate-limited per event type (default 15-minute cooldown) plus per recipient (60-second cooldown). They are off by default and must be explicitly enabled.

When a ticket queue is selected on that screen, each alert that is actually sent also opens one service ticket in that queue. The ticket is priority critical and starts open. Moving it to in progress is how someone acknowledges it. The text is sent whether or not the ticket insert succeeds. A second alert of the same type inside the quiet period does not open another ticket. With no queue selected, alerts are SMS only. The ticket does not receive the queue's SLA due date.

A list of E.164 strings already stored in settings, or CRITICAL_ALERT_RECIPIENTS when the database has no recipient list, still loads. Each string becomes a labeled line (Saved number for a stored list, Environment for the environment variable).

An invalid recipient is skipped and recorded as a critical_alert.config_drift audit row. The audit metadata contains the list index, the rejection reason, and a redacted phone number. It does not contain the raw number.

A send that fails is stored in critical_alert_retry and retried by the critical-alert-retry lifecycle job (every minute, up to five attempts). The retry sends the same message. It does not start a new event-cooldown window. After five failures the row is abandoned. Each attempt is claimed by one worker before any provider request, so two workers never send the same row. Disabling an SMS connector stops every send through it, retries included, on the next send; a retry row on that connector fails each attempt and is abandoned after five unless the connector is enabled again first. Rows stay on the connector they started with, even if the alert settings now name another one. Consecutive connector auth-failure counts are kept in memory on each process. A second API or worker process does not share that count.

The Critical Alerts tab lists the newest of those retry rows: event, recipient name, redacted number, pending / sent / abandoned, attempts, and the next attempt while the row is still pending. The name is the person's display name, or the labeled line's label, when that number is still configured. A text that succeeds on the first try is not in that list. The list does not include the message body or the full phone number.

Rollback: the migration down drops critical_alert_retry. Rows that had not been sent are discarded. That is the same outcome as a failed send before the retry table existed. No new environment variables are required.