Skip to content

Connect 23 Telecom to HubSpot

Copy page
Setup typeCustom code · technical admin
Setup timeAbout 20 minutes
Sends SMSYes
Delivery status in HubSpotNo — check 23 Telecom
Who can configureHubSpot admin (Data Hub Professional or Enterprise)

A HubSpot custom-code workflow action calls the 23 Telecom API to send an SMS. We recommend custom code over Send a webhook because it lets you build the exact JSON body, read the key from a secret, and use a per-execution idempotency key. HubSpot records the action result; carrier delivery reports stay in 23 Telecom.

POST  /api/v1/sms/send-individual  · Permission: sms.send

  1. In your workflow choose + → Custom code.

  2. Add a secret named TELECOM23_KEY holding your restricted key, and add the contact phone as an input property.

  3. Paste the action code (below) and map the phone input.

  4. Use Test action on a test record, then check the status, output and logs.

Custom code (Node.js)
// Secret: TELECOM23_KEY Input property: phone
const axios = require('axios'); // bundled in HubSpot custom-code actions
exports.main = async (event, callback) => {
const phone = event.inputFields.phone;
try {
const { data } = await axios.post(
'https://restlink23telecom.com/api/v1/sms/send-individual',
{ messages: [{ to: phone, message: 'Your message', sender_id: 'YourBrand' }] },
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.TELECOM23_KEY,
'Idempotency-Key': event.callbackId, // unique per execution
},
}
);
// 2xx only: the body must still say "accepted" with an id. Any other body
// is not confirmed acceptance and must not be auto-retried.
const result = data && data.results && data.results[0];
if (!result || result.status !== 'accepted' || !result.message_id) {
throw new Error(`not accepted: ${result && result.status}`);
}
callback({ outputFields: { message_id: result.message_id, status: 'accepted' } });
} catch (err) {
const status = err.response && err.response.status;
// Transient (429/5xx): re-throw the original AxiosError — HubSpot only retries
// an action that throws an axios / @hubspot/api-client error for those codes.
if (status === 429 || (status >= 500 && status <= 599)) {
throw err;
}
// Permanent (other 4xx, or a 200 that wasn't "accepted"): fail with a
// plain error and do NOT ask HubSpot to retry.
throw new Error(`send failed permanently (status ${status || 'none'})`);
}
};

Run Test action on a test record with your sk_test_ key. Expect the output message_id to be set and the log to show results[0].status: "accepted".

Unlike the webhook-only platforms, the HubSpot custom code validates the response body — it fails unless results[0].status is "accepted" with a message_id, so a bare 2xx never turns the workflow green. That satisfies the production acceptance requirement without a dedicated endpoint. After the sandbox check, your 23 Telecom account manager enables production for the workspace and confirms the account limits; then swap the TELECOM23_KEY secret from the sandbox sk_test_ key to a restricted sk_prod_ key.

Accepted means queued. Read carrier delivery in Message status or Statistics, or add a live delivery webhook. If you need delivery status inside HubSpot, ask us to scope a managed contact-property or custom-event bridge.

ResponseCause & fix
400 IDEMPOTENCY_KEY_REQUIREDSend event.callbackId as Idempotency-Key
401 INVALID_API_KEYWrong or disabled key in the TELECOM23_KEY secret
403 NO_SMS_ACCESSSMS not enabled — contact 23 Telecom
429 RATE_LIMIT_EXCEEDEDSending faster than your budget — throttle the workflow
  • Workflow, custom code, secrets or limits — HubSpot support.
  • API key, sender ID, accepted-but-not-delivered — your 23 Telecom account manager, or the contact form.

Last verified: 16 July 2026.