TutorialFollow along and build something end to end. Start here if Bosbec is new to you.

Build an SMS Sending Interface

This guide combines two things you may already have tried: an SMS Gateway that sends messages over HTTP, and the Hello World Interface that serves a web page from a workflow.

Put together, they give you a small internal tool: a page where a user logs in, picks recipients from your Bosbec account, writes a message, and sends it as SMS. Everything is hosted by the workflow itself, so there is no separate web server to set up.

There is no ready-made template for this in the Workflow Library. You will build it from parts that are documented on their own, which also means you can adapt every step to your own account.


GET STARTED FOR FREE


What you will build

One workflow exposing three endpoints on the same HTTP channel:

Method Path Description
GET /sms Returns the HTML interface
GET /contacts Returns the units the user can send to
POST /send-sms Sends the message

Keeping all three on the same channel means the page and the API share a domain. That avoids CORS configuration entirely, and the token never has to travel to a third-party origin.

Before you start

  • An HTTP channel. See Channels - Incoming.
  • An API token for testing the endpoints outside the browser.
  • A few units in your account with a phone number set. The POST Units guide covers creating them.
  • Familiarity with Building Your First API. This guide assumes you know how to add a trigger and a Send API Response job.

Step 1: Add the sending endpoint

Open the Workflow Library and import API2SMS HTTP-in, as described in SMS Gateway using HTTP-in.

Configure the trigger with your channel and the path send-sms. This gives you a working POST /send-sms endpoint that accepts:

{
    "sender": "Bosbec",
    "recipient": "+46700000001",
    "message_text": "Test message"
}

Verify it with an API client before you continue. If the request does not work from Postman, it will not work from the page either, and it is much easier to debug one layer at a time.

The imported workflow sends to one recipient per request. The interface below simply sends one request per selected recipient, which keeps the workflow unchanged. If you later need to send to many recipients at once, extend the workflow with a For Each Resource loop over a recipient array instead.

Step 2: Add the contact endpoint

The page needs something to populate its recipient list with. Add a second Incoming HTTP trigger to the same workflow, using the same channel, the path contacts, and the GET method.

Then build the same chain described in GET Units from HTTP-in:

  1. A Unit Pipeline with a Find units step and a Save as resource step (found_units).
  2. A JSON Pipeline that shapes each unit into an object.
  3. A Send API Response job returning 200 OK with the JSON.

Return only what the interface needs:

[
	{
		"id": "00000000-0000-0000-0000-000000000000",
		"name": "John Doe",
		"phone": "+46700000001"
	}
]

Units without a phone number cannot receive an SMS, so filter them out in the pipeline rather than in the browser. The list the page receives should be the list the user is allowed to act on.

Step 3: Serve the interface

Add a third Incoming HTTP trigger, same channel, path sms, method GET. Connect it to a Send API Response job and set:

  • Status code: 200
  • Content type: text/html
  • Body: the HTML from the next step

This is the same pattern the Hello World Interface uses, and it is worth understanding why: because the page is served from your Bosbec channel, its fetch calls to /contacts and /send-sms are same-origin requests.

One thing to watch out for: the response body is processed for Bosbec variables, so any literal {{ in your HTML or JavaScript will be interpreted as a variable reference. Avoid double curly braces in the page, or keep the affected code in a separate file that the page loads.

Step 4: The interface

Paste this into the Send API Response body. It is intentionally plain - one select, one textarea, one button.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Send SMS</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; padding: 0 1rem; }
    label { display: block; margin: 1rem 0 0.25rem; font-weight: 600; }
    select, textarea, button { width: 100%; padding: 0.5rem; font: inherit; }
    #status { margin-top: 1rem; }
  </style>
</head>
<body>
  <h1>Send SMS</h1>

  <label for="recipients">Recipients</label>
  <select id="recipients" multiple size="8"></select>

  <label for="message">Message</label>
  <textarea id="message" rows="4" maxlength="459"></textarea>
  <small id="count">0 characters</small>

  <p><button id="send">Send</button></p>
  <div id="status" role="status"></div>

  <script>
    const token = sessionStorage.getItem('bosbec_token');
    const headers = { 'Authorization': token, 'Content-Type': 'application/json' };
    const status = document.getElementById('status');
    const message = document.getElementById('message');
    const recipients = document.getElementById('recipients');

    message.addEventListener('input', () => {
      document.getElementById('count').textContent = message.value.length + ' characters';
    });

    async function loadContacts() {
      const res = await fetch('/contacts', { headers });
      if (!res.ok) { status.textContent = 'Could not load contacts.'; return; }
      for (const c of await res.json()) {
        const option = new Option(c.name + ' (' + c.phone + ')', c.phone);
        recipients.add(option);
      }
    }

    document.getElementById('send').addEventListener('click', async () => {
      const selected = [...recipients.selectedOptions].map(o => o.value);
      if (!selected.length || !message.value.trim()) {
        status.textContent = 'Select at least one recipient and write a message.';
        return;
      }

      status.textContent = 'Sending...';
      const results = await Promise.all(selected.map(phone =>
        fetch('/send-sms', {
          method: 'POST',
          headers,
          body: JSON.stringify({ sender: 'Bosbec', recipient: phone, message_text: message.value })
        })
      ));

      const failed = results.filter(r => !r.ok).length;
      status.textContent = failed
        ? failed + ' of ' + results.length + ' messages failed.'
        : 'Sent ' + results.length + ' message(s).';
    });

    loadContacts();
  </script>
</body>
</html>

The page reads its token from sessionStorage, which is filled by the login step below. Nothing about the message or the recipients is hard-coded in the workflow.

Make it look like your own tool

The markup above is deliberately unstyled so the logic is easy to follow, but this is an internal tool that you and your colleagues may use every day, and it is worth making it look like it belongs to you.

Everything you need sits in the <style> block:

  • Swap the colours for your brand palette, and use your accent colour for the send button.
  • Point font-family at your corporate typeface, with a system font as fallback.
  • Add your logo above the heading with an <img> tag referencing a hosted image.

If you prefer not to write the CSS yourself, paste the HTML into a local file and let an AI assistant restyle it against your brand guidelines, as described in Hello World Interface. Then paste the result back into the Send API Response job. Keep the element IDs unchanged, since the JavaScript looks them up by ID, and remember to avoid double curly braces in anything you paste back.

Step 5: Authentication

Do not put an account API token in the HTML. Anyone who opens the page can read it in the browser.

Use the login pattern from the Hello World Interface instead:

  1. A POST endpoint that takes the user's Bosbec credentials.
  2. The workflow authenticates the user and generates a token for that user.
  3. The page stores the token with sessionStorage.setItem('bosbec_token', token) and sends it in the Authorization header, as the code above does.

Import that workflow into the same account and reuse its login part. Because the token belongs to the logged-in user rather than the account, revoking access is a matter of disabling the user.

Step 6: Test it

  1. Save and activate the workflow.
  2. Open https://yoursubdomain.in.bosbec.io/sms in a browser.
  3. Log in, and confirm that the recipient list is populated.
  4. Select yourself, send a short message, and check that it arrives.

If something does not work, open the browser's developer tools and look at the network tab before you open the workflow. The status code tells you which layer failed.

Status Likely cause
404 The path in the trigger does not match the path in the fetch call
503 The workflow is not activated, or no trigger matches the method used
401 The token is missing or expired - check what sessionStorage actually contains
500 The workflow started but a job failed. See Troubleshooting your workflow

An empty recipient list with a 200 response usually means the Find units step matched nothing, or the JSON Pipeline produced a different shape than the page expects.

Next steps

The same three-endpoint structure works for most small internal tools. Once the SMS interface works, useful extensions include:

  • Sending to a group instead of individual units.
  • Adding a search field, backed by a query parameter on /contacts as shown in GET Units from HTTP-in.
  • Logging each send to a resource so the page can show a history.
  • Letting an AI assistant iterate on the interface, as described in Working with Bosbec and AI.

GET STARTED FOR FREE