Skip to content
CaptiFi Connect

Behind the scenes8 min read

SMS from HubSpot and Salesforce workflows

Send SMS from HubSpot and Salesforce workflows with an SMS API: HubSpot's webhook and custom code actions, Salesforce Flow HTTP Callout, and consent fields.

In short

  • HubSpot's Send a webhook and Custom code workflow actions need a Data Hub Professional or Enterprise subscription, according to HubSpot's own documentation.
  • HubSpot retries a failed workflow webhook for up to three days, the first retry one minute after the failure and the gap between later retries reaching up to eight hours.
  • A HubSpot custom code action must finish within 20 seconds, can use up to 128 MB of memory, and its secret values together must not exceed 1,000 characters.
  • Salesforce says a record-triggered flow fails if it calls an external service in its synchronous path, so an HTTP Callout from one belongs on an asynchronous path.
  • Under PECR, as the ICO explains it, a business must not send marketing texts to individuals without specific consent unless the soft opt-in for previous customers applies.
Four colleagues gathered round a laptop at a table in an office
Photo: Jud Mackrill, Unsplash

A CRM already knows when something worth a text has happened: a deal reaches a stage, a booking is confirmed, an application gets approved. Both HubSpot and Salesforce can send SMS from their workflows by calling an SMS API, with no app installed. CaptiFi Connect has no app in HubSpot's marketplace or on Salesforce's AppExchange, and you don't need one. HubSpot workflows can send a webhook or run a few lines of custom code, and Salesforce Flow has a no-code HTTP Callout action. This post sets out what each needs, including the plan or setup that unlocks it, from the vendors' documentation as it read on 4 November 2026.

Software of this kind is widespread. The ONS found that in 2023, 69% of firms with 10 or more staff used cloud-based systems and applications and 61% used specialised software, with both figures rising with firm size.

UK firms using cloud-based systems and specialised software in 2023, by employees
  • Cloud-based systems and apps
  • Specialised software
0%20%40%60%80%10 to 19, Cloud-based systems and apps: 64%10 to 19, Specialised software: 56%10 to 1920 to 49, Cloud-based systems and apps: 70%20 to 49, Specialised software: 64%20 to 4950 to 99, Cloud-based systems and apps: 77%50 to 99, Specialised software: 72%50 to 99250 or more, Cloud-based systems and apps: 79%79%250 or more, Specialised software: 73%250 or moreAll firms, Cloud-based systems and apps: 69%All firms, Specialised software: 61%All firms

Firms with 10 or more employees in production and services industries, UK, weighted; agriculture and finance excluded. ONS labels its fourth size band 100-149 in the chart data, so that band is left out here.

Source: ONS, Management and Expectations Survey technology module, figure 3 data, March 2025

See the figures as a table
 Cloud-based systems and apps (%)Specialised software (%)
10 to 196456
20 to 497064
50 to 997772
250 or more7973
All firms6961

The send itself is the same request whichever CRM makes it: POST to https://captificonnect.co.uk/api/v1/messages with Authorization: Bearer and your API key, and a JSON body holding to (the mobile number, in any common UK format) and body (the text). Two optional extras matter in a CRM. client_ref takes up to 64 characters of your own reference, the record id for instance, and comes back on every delivery event. The Idempotency-Key header makes a retried request return the first answer instead of sending again. The quickstart shows the request and response in full.

HubSpot: the Send a webhook action

HubSpot's workflow action for calling an outside system is Send a webhook, in the Data ops section of the workflow editor. Its documentation lists Data Hub Professional or Enterprise as the subscription it needs. Choose POST, then either "Include all [object] properties" or "Customize request body". Pick the second, so that you send to (mapped to the contact's mobile property) and body (the text), and not the whole contact record.

For the key, HubSpot offers an API key option: give it a name and choose whether it goes in the query parameters or a request header. Set the name to Authorization, the location to request header, and the value to Bearer, a space and your key.

Retries are generous. "HubSpot will retry failed webhooks for up to three days, starting one minute after failure", with gaps growing to at most eight hours. It doesn't retry a 4xx, except 429, where it respects the Retry-After header, and the documentation says "Retry-After is recorded in milliseconds". That last detail is worth knowing: RFC 9110 defines Retry-After as a number of seconds, and that's what we send, so after a 429 HubSpot may come back sooner than the header intended. It also means a 402 (our answer when the balance is empty) is not retried, and that text simply doesn't go.

The action has no field for a header of your choosing, so it can't send an Idempotency-Key. For a fixed text where an occasional duplicate wouldn't matter, Send a webhook is fine. For anything personal, or where a double text would look careless, use custom code.

HubSpot: custom code actions

Custom code actions run JavaScript on Node.js (Python is in beta), on the same Data Hub Professional or Enterprise tiers. The limits are tight but enough for one API call: an action must "Finish running within 20 seconds" and "Can only use up to 128 MB of memory". HubSpot's documentation also says Node.js 16 and 20 no longer get security updates and that actions should move to Node.js 24 before 17 November 2026.

Put the API key in the action's secrets. HubSpot says secrets "will be available as environment variables", read in Node.js as process.env and the secret's name, and that "the total length of all secret values must not exceed 1000 characters". An API key fits easily.

In the code, pass the record's properties in as input fields, build the text, and send it with an Idempotency-Key made from the record and the event: the deal id plus "booking-confirmed", say. Then use HubSpot's retries to your advantage. For a 429 or 5xx answer from a call made with axios or HubSpot's own client, "HubSpot will reattempt to execute your action for up to three days, starting one minute after failure", provided you throw the error in the catch block. Every reattempt sends the same key, so a send that did get through before a timeout comes back as a replay, with meta.replayed set to true, and nothing goes twice. Our post on sending a text exactly once has the reasoning.

Three days is a long time for some texts. "See you tomorrow at 10" is wrong two days later, so compare the appointment time with the current time at the top of the code and skip the send if it has passed. Return the message id we give back in outputFields, and a later action can copy it into a property on the record.

Salesforce: Flow's HTTP Callout

Salesforce Flow's HTTP Callout lets admins "connect flows to external APIs without coding", and Salesforce says admins with the Customize Applications permission can use it. Behind the scenes, Flow Builder generates an external service registration and an invocable action, which can then be reused across the org.

Authentication comes first, in Setup. Salesforce's guidelines set the order: a permission set, an external credential, then a named credential (an auth provider only for OAuth, which we don't use). For an API with "a key or token in the header", the external credential uses the Custom protocol, and you add an authentication parameter holding the key to its permission set mapping. Then add a custom header with the exact name the API expects, here Authorization. Our header needs the word Bearer in front of the key, so its value is a formula joining the two, which means ticking Allow Formulas in the HTTP Header on the named credential. The named credential holds the base URL, https://captificonnect.co.uk/api/v1, and the HTTP Callout action is tied to it when you build the action.

Two of Salesforce's considerations shape the flow. A record-triggered flow "fails if it attempts to access external data" in its synchronous path, because "Salesforce prevents callouts to external services from running while a record is saving". Turn on Add Asynchronous Path on the Start element and put the callout there; that path "runs in the background after the original record save is successfully committed". And "a response status code of 300 or greater is returned as an error", to be handled with a fault path, which is where a 402 or a refused number should be logged on the record or sent to an admin.

The awkward limit is headers. Salesforce says "Request headers aren't supported" on the action itself; headers come from the credentials, or from editing the OpenAPI specification of the generated external service. A header that has to change with every record, like Idempotency-Key, doesn't fit the no-code route. Set client_ref to the record id instead, and before any resend, look it up with GET /messages, which can filter by client_ref. If a text already exists for that record, don't send another. Where duplicates really can't be allowed, an Apex callout gives you the header.

A booking confirmation or a delivery update is a service message. A text promoting something is marketing, and the rules are stricter. The ICO's guide to PECR says electronic mail "includes messages sent using a short message service", that "You must not send marketing emails or texts to individuals without specific consent", and that the one exception is a soft opt-in for "your own previous customers". Every marketing message also needs "a simple way to opt out".

So keep the permission on the record, where the workflow can check it: a field for SMS marketing consent, the date it was given, where (the web form, a paper form at the counter) and the exact wording the person agreed to. Make the field a filter on every marketing workflow, and leave service workflows alone. If you also keep your contacts on our side, POST /contacts takes the same record as consent_source, consent_at and consent_wording. Our post on writing a consent request that stands up covers the wording itself.

Opt-outs have to flow the other way. When someone replies STOP, we put the number on your opt-out list and refuse later sends to it, and we post an opt_out.created webhook. Send that webhook to a small endpoint of your own, check its signature, and update the consent field on the CRM record through the CRM's API, so no other workflow tries again.

Bringing replies and delivery reports back

The same endpoint can handle the rest. message.delivered and message.failed carry your client_ref, so a failed text can be logged against the deal or case it was about. Replies arrive as message.received, from a phone number with no record attached, so you match them on the sender's number; our post on handling replies with webhooks goes through that. Build all of it with a cc_test_ key first: test sends cost nothing, reach no phone and still fire signed delivery webhooks, so a sandbox org or a test portal can run the whole loop.

If your workflows live in a general automation tool instead, our post on sending SMS from Zapier, Make and Power Automate covers those.

Common questions

Can HubSpot send SMS from a workflow without an app?

Yes, on Data Hub Professional or Enterprise. The Send a webhook action can post a fixed text to an SMS API, and a custom code action can build a personal text from the record's properties and send it with an idempotency key. Both call the API directly, so no marketplace app is involved.

Can Salesforce Flow call an SMS API without code?

Yes. Flow's HTTP Callout action calls an outside API through a named credential, with the API key held in an external credential. In a record-triggered flow, the callout has to go on the asynchronous path, because callouts are blocked while the record is saving.

Why did my CRM send the same text twice?

Usually a retry. HubSpot retries failed webhooks and custom code actions for up to three days, and a request that timed out may already have sent its text. Send an Idempotency-Key built from the record and the event, and a repeat returns the first answer without sending again.

How do I stop CRM workflows texting people who opted out?

Keep an SMS consent field on each record, filter marketing workflows on it, and update it when someone opts out. CaptiFi Connect posts an opt_out.created webhook when a person texts STOP, which your own endpoint can pass to the CRM.

Sources

Read for this post on 4 November 2026.

Written by the CaptiFi Connect team. Published 4 November 2026. More in Behind the scenes

Send your first text this week

Set up your account and contacts now. Until we've approved it, a person reads each text before it goes out.