<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eddy Odhiambo</title>
    <description>The latest articles on DEV Community by Eddy Odhiambo (@supamodo).</description>
    <link>https://dev.to/supamodo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3582851%2Fba26ffcf-4055-49b4-938e-3114985f028c.jpeg</url>
      <title>DEV Community: Eddy Odhiambo</title>
      <link>https://dev.to/supamodo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/supamodo"/>
    <language>en</language>
    <item>
      <title>Relational SQL 123s: DDL, DML, Filtering, and Turning Rows into Meaning</title>
      <dc:creator>Eddy Odhiambo</dc:creator>
      <pubDate>Sun, 12 Apr 2026 11:22:06 +0000</pubDate>
      <link>https://dev.to/supamodo/relational-sql-123s-ddl-dml-filtering-and-turning-rows-into-meaning-5345</link>
      <guid>https://dev.to/supamodo/relational-sql-123s-ddl-dml-filtering-and-turning-rows-into-meaning-5345</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical mental model for how databases are structured, how you put data in and change it, and how &lt;code&gt;WHERE&lt;/code&gt;, aggregates, and &lt;code&gt;CASE WHEN&lt;/code&gt; let you ask precise questions without getting lost in syntax.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;This article is a &lt;strong&gt;concept-first&lt;/strong&gt; tour of ideas that show up in almost every SQL course and job: &lt;strong&gt;DDL&lt;/strong&gt; versus &lt;strong&gt;DML&lt;/strong&gt;, the roles of &lt;strong&gt;&lt;code&gt;CREATE&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/strong&gt;, how &lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/strong&gt; and common operators narrow results, and how &lt;strong&gt;&lt;code&gt;CASE WHEN&lt;/code&gt;&lt;/strong&gt; adds computed labels on top of raw values. &lt;/p&gt;

&lt;h2&gt;
  
  
  DDL vs DML: structure versus contents
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DDL (Data Definition Language)&lt;/strong&gt; describes &lt;code&gt;structure&lt;/code&gt;. It answers what objects exist, what they are named, which columns they have, their data types, and which integrity rules apply (primary keys, uniqueness, nullability, and so on). Typical DDL includes &lt;strong&gt;&lt;code&gt;CREATE SCHEMA&lt;/code&gt;&lt;/strong&gt; or &lt;strong&gt;&lt;code&gt;CREATE DATABASE&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;CREATE TABLE&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;ALTER TABLE&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;DROP&lt;/code&gt;&lt;/strong&gt;. When you run DDL, you are defining the shape of storage and their dividers alongside the data that will live inside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DML (Data Manipulation Language)&lt;/strong&gt; describes &lt;code&gt;rows&lt;/code&gt;. It answers what is stored &lt;strong&gt;right now&lt;/strong&gt;. &lt;strong&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;/strong&gt; adds rows, &lt;strong&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/strong&gt; modifies existing rows, &lt;strong&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/strong&gt; removes rows, and &lt;strong&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/strong&gt; retrieves rows. &lt;strong&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/strong&gt; is sometimes treated as its own category because it does not change persisted data by default, but it is universally grouped with “working with data” alongside inserts and updates.&lt;/p&gt;

&lt;p&gt;Why distinguish them? In production, &lt;strong&gt;DDL&lt;/strong&gt; often goes through migrations, reviews, and backups; getting a column wrong is expensive to unwind. &lt;strong&gt;DML&lt;/strong&gt; is what applications and analysts run constantly. Mixing the two mentally helps you choose the right tool, “Am I changing the contract of the table, or only the rows inside it?”&lt;/p&gt;




&lt;h2&gt;
  
  
  CREATE, ALTER, INSERT, UPDATE, and DELETE in practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CREATE TABLE&lt;/code&gt;&lt;/strong&gt; (and optionally &lt;strong&gt;&lt;code&gt;CREATE SCHEMA&lt;/code&gt;&lt;/strong&gt;) establishes tables and namespaces. You declare column names, types, and constraints such as &lt;strong&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;NOT NULL&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;UNIQUE&lt;/code&gt;&lt;/strong&gt;. In systems like PostgreSQL, setting a &lt;strong&gt;&lt;code&gt;search_path&lt;/code&gt;&lt;/strong&gt; keeps multi-schema projects readable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ALTER TABLE&lt;/code&gt;&lt;/strong&gt; reflects evolving requirements - add a column you forgot at launch, rename a column when the business vocabulary changes, change a type when the data grows wider, or drop a column that is no longer collected. The important habit is to remember that &lt;strong&gt;the table your &lt;code&gt;INSERT&lt;/code&gt; targets is the table as it exists after all prior &lt;code&gt;ALTER&lt;/code&gt;&lt;/strong&gt; column names in &lt;code&gt;INSERT&lt;/code&gt; must match the new definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;/strong&gt; loads one or many rows. Values must respect constraints; duplicate keys or nulls in &lt;code&gt;NOT NULL&lt;/code&gt; columns will fail fast, which is preferable to silent corruption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/strong&gt; changes existing rows. A universal rule is &lt;strong&gt;always constrain with &lt;code&gt;WHERE&lt;/code&gt;&lt;/strong&gt; unless you truly intend to touch every row. A missing &lt;code&gt;WHERE&lt;/code&gt; on &lt;code&gt;UPDATE&lt;/code&gt; or &lt;strong&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/strong&gt; is one of the fastest ways to turn a good day into an incident review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/strong&gt; removes rows, again almost always with a &lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/strong&gt; predicate so only the intended rows disappear. After DML, aggregates and reports reflect the new truth, counts and sums change because the underlying rows changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Filtering with WHERE clause: precision over intuition
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/strong&gt; clause limits which rows are considered for &lt;strong&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/strong&gt;, and critically for &lt;strong&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/strong&gt;. Common building blocks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Comparison:&lt;/strong&gt; &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;=&lt;/code&gt; (e.g. &lt;code&gt;status = 'active'&lt;/code&gt;, &lt;code&gt;amount &amp;gt;= 100&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boolean logic:&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;AND&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;OR&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;NOT&lt;/code&gt;&lt;/strong&gt;, with parentheses when intent is ambiguous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BETWEEN&lt;/code&gt;:&lt;/strong&gt; &lt;strong&gt;inclusive&lt;/strong&gt; ranges on numbers or dates (e.g. &lt;code&gt;order_date BETWEEN '2024-03-15' AND '2024-03-18'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IN&lt;/code&gt; / &lt;code&gt;NOT IN&lt;/code&gt;:&lt;/strong&gt; membership in a fixed list (e.g. &lt;code&gt;region IN ('East', 'West', 'Central')&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LIKE&lt;/code&gt;:&lt;/strong&gt; simple pattern matching; &lt;code&gt;%&lt;/code&gt; matches any sequence, &lt;code&gt;_&lt;/code&gt; matches a single character (e.g. &lt;code&gt;name LIKE 'A%'&lt;/code&gt; for names starting with A, &lt;code&gt;description LIKE '%beta%'&lt;/code&gt; for a substring).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Used together, these turn vague questions (“show me the interesting orders”) into &lt;strong&gt;testable&lt;/strong&gt; predicates the engine can optimize. Text literals belong in &lt;strong&gt;single quotes&lt;/strong&gt; in standard SQL; numbers do not.&lt;/p&gt;




&lt;h2&gt;
  
  
  CASE WHEN: rules as readable columns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CASE WHEN&lt;/code&gt;&lt;/strong&gt; adds &lt;strong&gt;derived&lt;/strong&gt; values in the result of a query. You do not have to alter the table: you express business rules in SQL and expose them as named columns with &lt;strong&gt;&lt;code&gt;AS&lt;/code&gt;&lt;/strong&gt;. Conditions are usually evaluated &lt;strong&gt;in order&lt;/strong&gt;; the first match wins, and &lt;strong&gt;&lt;code&gt;ELSE&lt;/code&gt;&lt;/strong&gt; catches the remainder.&lt;/p&gt;

&lt;p&gt;Typical uses include &lt;strong&gt;banding&lt;/strong&gt; numeric scores (grade tiers, discount brackets), &lt;strong&gt;normalizing&lt;/strong&gt; messy codes into display labels, and &lt;strong&gt;bucketing&lt;/strong&gt; categories (e.g. “high / medium / low” from thresholds).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'Excellent'&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'Good'&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'Satisfactory'&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'Needs attention'&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;performance_band&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same pattern appears in reporting tools and spreadsheets; SQL makes it explicit and reusable in pipelines and APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;COUNT(*)&lt;/code&gt;&lt;/strong&gt; and other aggregates (&lt;code&gt;SUM&lt;/code&gt;, &lt;code&gt;AVG&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, &lt;code&gt;MAX&lt;/code&gt;) combine naturally with &lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/strong&gt;: you count or sum &lt;strong&gt;only the rows that pass the filter&lt;/strong&gt;, which is how operational questions (“how many open tickets in this queue?”) map directly to queries.&lt;/p&gt;




&lt;p&gt;The satisfying part with SQL is continuity: design the schema, load and correct data, then ask layered questions with filters, counts, and conditional labels. The database becomes a single place where **rules and questions stay aligned.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>postgres</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Transactional SMS Setup with Africa's Talking SDK For Single and Bulk messages</title>
      <dc:creator>Eddy Odhiambo</dc:creator>
      <pubDate>Sun, 12 Apr 2026 09:29:23 +0000</pubDate>
      <link>https://dev.to/supamodo/transactional-sms-setup-with-africas-talking-sdk-for-single-and-bulk-messages-16fg</link>
      <guid>https://dev.to/supamodo/transactional-sms-setup-with-africas-talking-sdk-for-single-and-bulk-messages-16fg</guid>
      <description>&lt;p&gt;&lt;em&gt;A hands-on guide to working with Africa's Talking SDK to send transactional messages and a controlled bulk batch flow.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By the end of this guide, you’ll understand the full SMS delivery flow from your application to a real mobile phone.&lt;/p&gt;

&lt;p&gt;You will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up a minimal but production-relevant TypeScript backend&lt;/li&gt;
&lt;li&gt;Connect it to Africa’s Talking SMS API using their official SDK&lt;/li&gt;
&lt;li&gt;Build a &lt;code&gt;POST /send-one&lt;/code&gt; endpoint (for transactional messages like OTPs)&lt;/li&gt;
&lt;li&gt;Build a &lt;code&gt;POST /send-bulk&lt;/code&gt; endpoint (for campaigns or notifications)&lt;/li&gt;
&lt;li&gt;Learn how to safely move from sandbox testing to real production sending&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1) Prerequisites to get started
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;a) Africa’s Talking Account.&lt;/code&gt;&lt;br&gt;
You need an account to access Africas Talking API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to: &lt;a href="https://account.africastalking.com/" rel="noopener noreferrer"&gt;https://account.africastalking.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sign up using your email and verify your account&lt;/li&gt;
&lt;li&gt;Once logged in, you’ll see a dashboard with products like SMS, Voice, Airtime, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvhlws8292wcfdiawqn19.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvhlws8292wcfdiawqn19.webp" alt="AfricasTalking Dashboard" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;b) Sandbox API Key (for testing)&lt;/code&gt;&lt;br&gt;
Africa’s Talking provides a sandbox environment so you can test without sending real SMS or being charged. To get it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to Settings → API Key&lt;/li&gt;
&lt;li&gt;Generate a new key&lt;/li&gt;
&lt;li&gt;Copy it immediately (you won’t see it again, its only shown once)
This key is what your app uses to authenticate API requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;c) Username (Sandbox vs Production)&lt;/code&gt;&lt;br&gt;
The username tells Africa’s Talking which environment to route your request to. Using the wrong combination (sandbox key + production username) will cause authentication errors.&lt;br&gt;
In sandbox mode, you will use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AT_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, you use your actual account username (e.g. &lt;code&gt;appname&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;d) Sender ID / Shortcode&lt;/code&gt;&lt;br&gt;
This is the name or number that appears as the sender of the SMS for example alphanumerics like &lt;code&gt;SMS_APP&lt;/code&gt; or shortcodes like &lt;code&gt;40123&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In sandbox testing, you will use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AT_SENDER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, your sender ID must be officially applied for through the &lt;code&gt;Product Request&lt;/code&gt; menu option after which it will be approved by telecom providers. This approval process can take time (around 3 days to 1 week based on my previous applications) and may require the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business registration details - registration certificate and KRA PIN&lt;/li&gt;
&lt;li&gt;Contact Information for notifications&lt;/li&gt;
&lt;li&gt;Use-case explanation (e.g. alerts, marketing, OTP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Africas Talking it costs KES 7,000 to register for each telecom provider (so KES 14,000 for Airtel and Safaricom)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;e) Node.js 18+&lt;/code&gt;&lt;br&gt;
Make sure you have Node installed, run node -v to confirm&lt;br&gt;
If not, Download from &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;https://nodejs.org/&lt;/a&gt;&lt;br&gt;
Node is preferred because Africa’s Talking SDK supports it well and integrates naturally with TypeScript and Express&lt;/p&gt;
&lt;h3&gt;
  
  
  2) Minimal Project Setup
&lt;/h3&gt;

&lt;p&gt;Run the following commands to prepare the environment to run the requests.  You will create a clean workspace for your SMS project, initialize a Node.js project and install the necessary dependencies needed to get stuff running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;at-sms-starter
&lt;span class="nb"&gt;cd &lt;/span&gt;at-sms-starter
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;africastalking express dotenv
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript ts-node @types/node @types/express
npx tsc &lt;span class="nt"&gt;--init&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;africastalking&lt;/code&gt; → official SDK to talk to the SMS API&lt;br&gt;
&lt;code&gt;express&lt;/code&gt; → lightweight web server for endpoints&lt;br&gt;
&lt;code&gt;dotenv&lt;/code&gt; → loads environment variables from .env&lt;br&gt;
&lt;code&gt;npm install -D typescript ts-node @types/node @types/express&lt;/code&gt; → Adds TypeScript support and enables running .ts files directly without compiling manually&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;package.json&lt;/code&gt; ensure you have this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ts-node src/at-sms.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will allow you to run your app with &lt;code&gt;npm run dev&lt;/code&gt;&lt;br&gt;
Next create the following files which wil have the main file and environment variables like &lt;code&gt;AT_USERNAME&lt;/code&gt; and &lt;code&gt;AT_SENDER_ID&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/at-sms.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3) &lt;code&gt;.env&lt;/code&gt; Environment variables template and field explanations
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AT_API_KEY=your_sandbox_or_live_api_key
AT_USERNAME=sandbox
AT_SENDER_ID=SandboxSender
PORT=5000
SMS_BATCH_SIZE=20
SMS_BATCH_DELAY_MS=500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What these fields do&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AT_API_KEY&lt;/code&gt;: authentication key used in API calls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AT_USERNAME&lt;/code&gt;: decides your account context (&lt;code&gt;sandbox&lt;/code&gt; for tests)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AT_SENDER_ID&lt;/code&gt;: sender identity recipients will see&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SMS_BATCH_SIZE&lt;/code&gt;: recipients to process per batch (for bulk sms)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SMS_BATCH_DELAY_MS&lt;/code&gt;: pause between batches to avoid spikes&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  4) Main File with the logic
&lt;/h3&gt;

&lt;p&gt;Add the following code block to your &lt;code&gt;src/at-sms.ts&lt;/code&gt; file that we created above;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Load environment variables from .env file into process.env&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import Express types and the Express library&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import the Africa's Talking SDK&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AfricasTalking&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;africastalking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Safely read an environment variable.
 * Throws an error if the variable is missing – this prevents the app from
 * running with incomplete configuration.
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Missing env var: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Convert any Kenyan phone number into the international format required
 * by Africa's Talking API (e.g., +2547XXXXXXXX).
 *
 * Why? The API expects numbers starting with +254. Users might enter:
 *   - 0712345678   (local)
 *   - 254712345678 (without +)
 *   - +254712345678 (already correct)
 *
 * This function handles all three cases thorugh normalization.
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeKenyanPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Remove spaces and dashes (e.g., "0712-345-678" -&amp;gt; "0712345678")&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Already international format with +?&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+254&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Starts with 254 but missing the leading +?&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;254&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`+&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Starts with 0 (common local writing) -&amp;gt; replace leading 0 with +254&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`+254&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Fallback – return as is (maybe it's already correct, or an error will occur later)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Simple sleep/pause function.
 * Used to wait between SMS batches so we don't flood the API and within 
 * the API rate limits
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The SDK needs your API key and username. We read them from .env&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AfricasTalking&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AT_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AT_USERNAME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Grab the SMS service from the initialised SDK&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// CORE SMS SENDING FUNCTION&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Sends a single SMS using the Africa's Talking SDK.
 * Handles phone normalisation and message line-ending conversion.
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendOneSms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;senderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AT_SENDER_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Convert the phone number to the format the API expects&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalizedTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeKenyanPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Africa's Talking recommends using \r\n (CRLF) as line endings.&lt;/span&gt;
  &lt;span class="c1"&gt;// This replace ensures any \n becomes \r\n.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalizedMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalizedTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalizedMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;senderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Contains status, message ID, etc.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// EXPRESS ROUTE: SEND ONE SMS&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/send-one&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Extract 'to' and 'message' from the JSON request body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Validate input – both fields are required&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;to and message are required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Send the SMS using our core function&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendOneSms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If anything fails, send a clear error message back to the client&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Sometimes the SDK provides extra error details&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// EXPRESS ROUTE: BULK SMS (WITH BATCHING &amp;amp; DELAY)&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/send-bulk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Read batch settings from .env (with sensible defaults)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batchSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMS_BATCH_SIZE&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batchDelayMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMS_BATCH_DELAY_MS&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Extract array of phone numbers and the common message&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;phones&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;phones&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Validate inputs&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;phones&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;phones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phones (array) and message are required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// We'll store the result for every phone (success or failure)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="c1"&gt;// ----------------------------------------------------------&lt;/span&gt;
    &lt;span class="c1"&gt;// BATCH PROCESSING LOOP&lt;/span&gt;
    &lt;span class="c1"&gt;// ----------------------------------------------------------&lt;/span&gt;
    &lt;span class="c1"&gt;// We split the phones array into chunks of size 'batchSize'.&lt;/span&gt;
    &lt;span class="c1"&gt;// For each batch we send SMS one by one, then wait before the next batch.&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;phones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Get the current batch (e.g., phones[0..19], then 20..39, ...)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;phones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Send an SMS to each phone in the batch (sequentially, not parallel)&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendOneSms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// If this specific phone fails, record the error but continue with others&lt;/span&gt;
          &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// After finishing a batch, wait (unless it was the last batch)&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;batchSize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;phones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batchDelayMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ----------------------------------------------------------&lt;/span&gt;
    &lt;span class="c1"&gt;// SEND SUMMARY BACK TO CLIENT&lt;/span&gt;
    &lt;span class="c1"&gt;// ----------------------------------------------------------&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batchDelayMs&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Detailed per-phone result&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// START THE SERVER&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;4200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`AT SMS starter running on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Single send: POST http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/send-one`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Bulk send: POST http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/send-bulk`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5) Run and Test the app
&lt;/h3&gt;

&lt;p&gt;Start the server with &lt;code&gt;npm run dev&lt;/code&gt;&lt;br&gt;
You should see the console messages indicating the server is running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test sending one SMS (replace the phone number with a any number)&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;On Windows (Command Prompt) run&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -X POST http://localhost:4200/send-one ^
  -H "Content-Type: application/json" ^
  -d "{\"to\":\"0712345678\",\"message\":\"Hello from AT sandbox\"}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;On macOS / Linux / Git Bash run&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:4200/send-one &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"to":"0712345678","message":"Hello from AT sandbox"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test Sending bulk SMS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:4200/send-bulk &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"phones":["0712345678","0722000000"],"message":"Monthly reminder: your account summary is ready."}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: In sandbox mode, messages are not actually delivered to real phones. On the africas Talking Dashboard under Bulk SMS -&amp;gt; Outbox, you will be able to view all messages sent through your account (sandbox/production) and their statuses whether successful or not.&lt;/p&gt;




&lt;h3&gt;
  
  
  6) When to Introduce Queues (BullMQ, RabbitMQ, etc.)
&lt;/h3&gt;

&lt;p&gt;This guide uses a simple loop because it is easier for beginners to follow. Move to a queue approach when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recipient lists become large (hundreds/thousands)&lt;/li&gt;
&lt;li&gt;request-response timeout becomes likely (HTTP requests can time out after ~30 seconds)&lt;/li&gt;
&lt;li&gt;you need retry persistence across server restarts&lt;/li&gt;
&lt;li&gt;multiple admins can trigger campaigns concurrently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that stage, keep the same &lt;code&gt;sendOneSms&lt;/code&gt; function and move the orchestration into worker jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  8) Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;401&lt;/code&gt; or auth errors&lt;/td&gt;
&lt;td&gt;Invalid key/username pair&lt;/td&gt;
&lt;td&gt;Confirm both are from same environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message rejected&lt;/td&gt;
&lt;td&gt;Sender ID not approved&lt;/td&gt;
&lt;td&gt;Use approved sender in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API says success but user reports no SMS&lt;/td&gt;
&lt;td&gt;Network/operator delivery delay&lt;/td&gt;
&lt;td&gt;Check AT message status and logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Too many failures in bulk&lt;/td&gt;
&lt;td&gt;Rate pressure or invalid numbers&lt;/td&gt;
&lt;td&gt;Reduce batch size, validate numbers first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;You do not need a complex architecture to get started. A clean single-file starter teaches the exact moving parts: credentials, send API, formatting, and rate-aware bulk execution. Once this is stable, you can scale safely with queue workers and observability.&lt;/p&gt;

&lt;p&gt;Africas Talking also have guides and tutorials on their site which you can use for further assistance and reference -  &lt;a href="https://developers.africastalking.com/" rel="noopener noreferrer"&gt;Africa's Talking Developer Docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>tutorial</category>
      <category>typescript</category>
    </item>
    <item>
      <title>A Practical Guide to Publishing and Embedding Power BI Reports with IFrames</title>
      <dc:creator>Eddy Odhiambo</dc:creator>
      <pubDate>Tue, 07 Apr 2026 11:31:08 +0000</pubDate>
      <link>https://dev.to/supamodo/a-practical-guide-to-publishing-and-embedding-power-bi-reports-with-iframes-27mc</link>
      <guid>https://dev.to/supamodo/a-practical-guide-to-publishing-and-embedding-power-bi-reports-with-iframes-27mc</guid>
      <description>&lt;p&gt;&lt;em&gt;A step-by-step beginner-friendly walkthrough for publishing a Power BI report and embedding it on a website portal.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Power BI is one of those tools that feels simple at first, then becomes very powerful as soon as you start sharing your work with other people. Building the report is one part of the process. Making it accessible and useful to others is the real final step.&lt;/p&gt;

&lt;p&gt;In this guide, I will walk through how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Power BI workspace&lt;/li&gt;
&lt;li&gt;Upload and publish your &lt;code&gt;.pbix&lt;/code&gt; report&lt;/li&gt;
&lt;li&gt;Generate an embed link (iframe)&lt;/li&gt;
&lt;li&gt;Add the report to a public to make it accessible by others&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why publishing matters in Power BI
&lt;/h2&gt;

&lt;p&gt;Creating visuals in Power BI Desktop is useful, but reports become more impactful when they can be accessed by others. Publishing gives your report a home in Power BI Service, where it can be viewed, shared, and embedded into a website.&lt;/p&gt;

&lt;p&gt;If you only keep reports in Desktop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access is limited to the local machine&lt;/li&gt;
&lt;li&gt;Collaboration is hard&lt;/li&gt;
&lt;li&gt;Stakeholders cannot view updates easily&lt;/li&gt;
&lt;li&gt;There is no web link for presentation or portfolio use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Publishing solves all that by moving your report to the cloud and giving you options to share it safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you need before starting
&lt;/h2&gt;

&lt;p&gt;Before you begin, make sure these are ready:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Power BI account (work or school account, depending on your setup)&lt;/li&gt;
&lt;li&gt;A finished &lt;code&gt;.pbix&lt;/code&gt; file that has the dashboards and reports&lt;/li&gt;
&lt;li&gt;Access to Power BI Service at &lt;a href="https://app.powerbi.com" rel="noopener noreferrer"&gt;https://app.powerbi.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A GitHub account (for hosting the website and uploading the &lt;code&gt;.pbix&lt;/code&gt; file)&lt;/li&gt;
&lt;li&gt;Your report iframe code gotten from Power BI Service site&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 1: Creating your workspace
&lt;/h2&gt;

&lt;p&gt;In Power BI Service, workspaces are like project rooms where reports and dashboards live.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to do it
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://app.powerbi.com" rel="noopener noreferrer"&gt;https://app.powerbi.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;On the left sidebar, click &lt;strong&gt;Workspaces&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New workspace&lt;/strong&gt; and give it a preferred name and description of your liking&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmxcgkvu3ohykywramb1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmxcgkvu3ohykywramb1.webp" alt="workspace creation" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This step matters because it keeps your work, reports and content organized&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tip: Keep the workspace name professional and clear, for example &lt;code&gt;North America Region - Sales Reports&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Uploading and publishing your reports
&lt;/h2&gt;

&lt;p&gt;Now you need to place your &lt;code&gt;.pbix&lt;/code&gt; file in the workspace so it can be opened in the browser. We are already assuming you have the report and dashboard report ready as a &lt;code&gt;.pbix&lt;/code&gt; file&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Publish directly from Power BI Desktop (recommended &amp;amp; faster)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open your report in Power BI Desktop&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Publish&lt;/strong&gt; from the menu options in top bar&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpq5qxukkj9okloc4m01.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpq5qxukkj9okloc4m01.jpg" alt="publishing workspace 1" width="594" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose the workspace name you created above&lt;/li&gt;
&lt;li&gt;Wait for the success confirmation&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option B: Upload from Power BI Service
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the target workspace in Power BI Service&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Import&lt;/strong&gt; or &lt;strong&gt;New &amp;gt; Report or Workbook &amp;gt; From this Computer&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77g32nxhmyhvavjthuq0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77g32nxhmyhvavjthuq0.jpg" alt="publishing workspace 2" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select your Reports/Dashboard &lt;code&gt;.pbix&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Wait for import completion&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What to check after upload
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The report appears under the workspace content&lt;/li&gt;
&lt;li&gt;The dataset/model is visible&lt;/li&gt;
&lt;li&gt;Opening the report shows all visuals correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your report should display like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F085t90lk2fa9d9cdiz3t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F085t90lk2fa9d9cdiz3t.jpg" alt="published report" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Generating the embed code (iframe)
&lt;/h2&gt;

&lt;p&gt;Once your report is in Power BI Service, you can generate code that lets the report appear inside a web page.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to generate iframe embed code
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the report in Power BI Service&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;File&lt;/strong&gt; or &lt;strong&gt;Share&lt;/strong&gt; (depends on UI version)&lt;/li&gt;
&lt;li&gt;Look for &lt;strong&gt;Embed report&lt;/strong&gt; or &lt;strong&gt;Publish to web&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose the appropriate embed option&lt;/li&gt;
&lt;li&gt;Copy the generated &lt;code&gt;&amp;lt;iframe ... &amp;gt;&amp;lt;/iframe&amp;gt;&lt;/code&gt; code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If prompted, confirm that you understand visibility settings. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffiih8ex1d6ufrw3ad8it.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffiih8ex1d6ufrw3ad8it.webp" alt="embedding report" width="768" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Important note on privacy
&lt;/h3&gt;

&lt;p&gt;There are different embedding methods in Power BI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publish to web&lt;/strong&gt;: public access, good for portfolio/demos, not for sensitive data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure embedding/organization methods&lt;/strong&gt;: for internal/private use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always confirm data is safe to make public before using it.&lt;/p&gt;




&lt;p&gt;Now that you have the iframe lik, you can share it with your developer or add it yourself to the index.html file of your public site for others to view.&lt;/p&gt;

&lt;p&gt;You can use a clean layout with a heading, short description, and responsive iframe container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common issues and how to fix them quickly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Report not showing on website
&lt;/h3&gt;

&lt;p&gt;Possible causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong iframe copied&lt;/li&gt;
&lt;li&gt;Embed permission not set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Re-copy iframe from Power BI Service&lt;/li&gt;
&lt;li&gt;Check on the permissions applied and confirm if accurate&lt;/li&gt;
&lt;li&gt;Refresh with hard reload&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Blank area where report should appear
&lt;/h3&gt;

&lt;p&gt;Possible causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser blocked mixed content&lt;/li&gt;
&lt;li&gt;Iframe width/height too small&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure &lt;code&gt;https&lt;/code&gt; is used&lt;/li&gt;
&lt;li&gt;Set iframe width to &lt;code&gt;100%&lt;/code&gt; and enough height (700px+)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Wrong workspace or missing report
&lt;/h3&gt;

&lt;p&gt;Possible causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uploaded to a different workspace&lt;/li&gt;
&lt;li&gt;Report upload failed silently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirm workspace name&lt;/li&gt;
&lt;li&gt;Re-upload the &lt;code&gt;.pbix&lt;/code&gt; file and wait for completion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;That's all you need to publish and embed your POwer BI Dashboards and Reports online, easy as 123&lt;/em&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>beginners</category>
      <category>microsoft</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>M-Pesa B2B API Integration Guide (TypeScript): Build, Run, and Understand It End-to-End</title>
      <dc:creator>Eddy Odhiambo</dc:creator>
      <pubDate>Mon, 30 Mar 2026 14:53:08 +0000</pubDate>
      <link>https://dev.to/supamodo/m-pesa-b2b-api-integration-guide-typescript-build-run-and-understand-it-end-to-end-3hm2</link>
      <guid>https://dev.to/supamodo/m-pesa-b2b-api-integration-guide-typescript-build-run-and-understand-it-end-to-end-3hm2</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical, beginner-friendly walkthrough for sending M-Pesa B2B payments from your M-Pesa Paybill/Till to another business's Paybill/Till Accounts through Node.js and Express&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  What You Will Build
&lt;/h3&gt;

&lt;p&gt;By the end of this guide, you will have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A minimal TypeScript app that requests a Daraja OAuth token&lt;/li&gt;
&lt;li&gt;A B2B payment request (&lt;code&gt;BusinessPayBill&lt;/code&gt;) sender&lt;/li&gt;
&lt;li&gt;Local webhook endpoints for &lt;code&gt;ResultURL&lt;/code&gt; and &lt;code&gt;QueueTimeOutURL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A simple correlation store so you can track request-to-callback flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide intentionally uses a &lt;em&gt;simple standalone structure&lt;/em&gt; so you can run it without any custom internal framework.&lt;/p&gt;




&lt;h3&gt;
  
  
  1) How M-Pesa B2B API Works (Mental Model)
&lt;/h3&gt;

&lt;p&gt;The M-Pesa B2B API is asynchronous (you do not have to wait for response and can proceed to other taks). The first API response you will receive confirms acceptance for processing, not final settlement of the transaction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2im1oaw09zz8uoov6rda.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2im1oaw09zz8uoov6rda.webp" alt="API mermaid ilustration image" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Your source of truth is the callback payload sent back to (&lt;code&gt;ResultURL&lt;/code&gt; / &lt;code&gt;QueueTimeOutURL&lt;/code&gt;), not only the initial POST response.&lt;/p&gt;




&lt;h3&gt;
  
  
  2) Prerequisites
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A Daraja app with &lt;code&gt;consumer key&lt;/code&gt; , &lt;code&gt;consumer secret&lt;/code&gt; and b2b api enabled. You can create and enable these on daraja portal.&lt;/li&gt;
&lt;li&gt;A working Paybill from Safaricom (Use test credentials for sandbox)&lt;/li&gt;
&lt;li&gt;Initiator name + security credential (or initiator password if you are generating credential externally) - this is the user set in the mpesa org portal with businessAPIOrgInitiator role enabled for the user.&lt;/li&gt;
&lt;li&gt;Public callback URLs for local development (for example, ngrok)&lt;/li&gt;
&lt;li&gt;Node.js 18+ installed&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.1) Setting up the initiator credentials &amp;amp; certificates for encryption
&lt;/h3&gt;

&lt;p&gt;We have already determined that you need the initiator credentials for you to make B2B payment requests through the API. An initiator is like the user authorized to make transactions on the paybill/till.&lt;/p&gt;

&lt;p&gt;To get these credentials, you will have to login to the &lt;a href="https://org.ke.m-pesa.com/" rel="noopener noreferrer"&gt;Mpesa Org Portal&lt;/a&gt; with the login credentials you received from Safaricom Mpesa via email and create a new user with the following role:&lt;br&gt;
`BusinessAPIOrgInitiator' - this roles allows the created user to make API requests on the paybill/till i.e. the B2B API requests.&lt;br&gt;
Below is the roles your user should have to be able to make B2B API requests:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszwsgd4pojtyqq1qk86c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszwsgd4pojtyqq1qk86c.webp" alt="Mpesa Roles" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The username for this user becomes the initiator name and password for this user is what will be encrypted to get the securityCredential.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now onto the security credential encryption, please note that this is not the initiator password itself but an encrypted version of it. To encrypt you must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Download Safaricom public certificate from the Daraja API documentation page. Here is the link - &lt;a href="https://developer.safaricom.co.ke/apis/GettingStarted" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Encrypt your initiator password using it. The steps for encrytption are on the API Documentation page&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1erwnbc8m3mn8oevx92.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1erwnbc8m3mn8oevx92.png" alt=" " width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you're testing you can use sandbox provided credential in the simulator&lt;/li&gt;
&lt;li&gt;In production you will have to generate it using OpenSSL&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3) Minimal Project Setup
&lt;/h3&gt;

&lt;p&gt;Go to an empty directory and run the following to create an app directory and setup express and typescript:&lt;br&gt;
  `&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;mkdir &lt;/span&gt;mpesa-b2b-starter
   &lt;span class="nb"&gt;cd &lt;/span&gt;mpesa-b2b-starter
   npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
   npm &lt;span class="nb"&gt;install &lt;/span&gt;axios express dotenv
   npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript ts-node @types/node @types/express
   npx tsc &lt;span class="nt"&gt;--init&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;Update your &lt;code&gt;package.json&lt;/code&gt; scripts:&lt;/p&gt;

&lt;p&gt;`&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ts-node src/mpesa-b2b.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;Create your environment file and the main file (always remember not to deploy these to GitHub - add the .env to gitignore).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/mpesa-b2b.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  4) Environment Variables Explained
&lt;/h3&gt;

&lt;p&gt;You can use this &lt;code&gt;.env&lt;/code&gt; template and just add your own values:&lt;/p&gt;

&lt;p&gt;`&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Daraja credentials
MPESA_ENV=sandbox
MPESA_CONSUMER_KEY=your_consumer_key
MPESA_CONSUMER_SECRET=your_consumer_secret

# B2B initiator setup
MPESA_INITIATOR_NAME=testapi (this is the test user for sandbox testing)
MPESA_SECURITY_CREDENTIAL=your_pre_encrypted_security_credential

# Shortcodes and payment details
MPESA_PARTY_A=600000
MPESA_PARTY_B=600111
MPESA_AMOUNT=100
MPESA_ACCOUNT_REF=INV10001
MPESA_REMARKS=Sandbox B2B test payment
MPESA_REQUESTER=254700000000

# Local webhook server
PORT=5000
PUBLIC_BASE_URL=https://your-ngrok-subdomain.ngrok.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;
&lt;h3&gt;
  
  
  Field meanings
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;&lt;code&gt;MPESA_PARTY_A&lt;/code&gt;&lt;/em&gt;: Sender shortcode/paybill (your business)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;&lt;code&gt;MPESA_PARTY_B&lt;/code&gt;&lt;/em&gt;: Receiving shortcode/paybill (destination business)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;&lt;code&gt;MPESA_SECURITY_CREDENTIAL&lt;/code&gt;&lt;/em&gt;: Encrypted initiator secret expected by Daraja. (for testing use value presented in daraja's simulator.)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;&lt;code&gt;PUBLIC_BASE_URL&lt;/code&gt;&lt;/em&gt;: Must be publicly reachable for callbacks from Mpesa daraja&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  5) This is the main file that will create and send the B2B payment request:
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;src/mpesa-b2b.ts&lt;/code&gt; if not already and add the following code:&lt;/p&gt;

&lt;p&gt;`&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// IMPORTANT: The M-Pesa API is ASYNCHRONOUS. The initial POST response only tells you if the request was accepted for processing. &lt;/span&gt;
&lt;span class="c1"&gt;//The final settlement status comes via a callback (ResultURL) a few seconds later.&lt;/span&gt;
&lt;span class="c1"&gt;// ------------------------------------------&lt;/span&gt;

&lt;span class="c1"&gt;// Load environment variables from .env file&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Axios for HTTP requests to Daraja&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Express for our local webhook server (receives callbacks)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 1. TYPE DEFINITIONS&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;

&lt;span class="c1"&gt;// We store pending transactions in memory so we can match incoming callbacks with the original request details.&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PendingTx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// When the payment request was sent&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// Amount requested (string for logging)&lt;/span&gt;
  &lt;span class="nl"&gt;partyA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// Your shortcode (sender)&lt;/span&gt;
  &lt;span class="nl"&gt;partyB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// Destination shortcode (receiver)&lt;/span&gt;
  &lt;span class="nl"&gt;accountReference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Invoice or order reference&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 2. SETUP EXPRESS SERVER&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// Automatically parse JSON request bodies&lt;/span&gt;

&lt;span class="c1"&gt;// In-memory store: maps OriginatorConversationID -&amp;gt; PendingTx&lt;/span&gt;
&lt;span class="c1"&gt;// This allows us to correlate the callback with the original request.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pendingByOriginatorConversationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PendingTx&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Determine if we are in sandbox or production mode&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MPESA_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sandbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Daraja base URL changes depending on environment&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.safaricom.co.ke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://sandbox.safaricom.co.ke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 3. HELPER FUNCTION: SAFELY READ ENVIRONMENT VARIABLES&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Missing env var: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 4. OBTAIN OAUTH ACCESS TOKEN FROM DARAJA&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * Daraja requires a Bearer token for all API calls (except OAuth itself).
 * The token is obtained by sending your Consumer Key and Consumer Secret
 * as a Basic Auth header.
 *
 * Token expires after ~1 hour; in production you should cache and refresh it.
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;consumerKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_CONSUMER_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;consumerSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_CONSUMER_SECRET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Combine key and secret with a colon, then Base64 encode&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;consumerKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;consumerSecret&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Make GET request to the OAuth endpoint&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/oauth/v1/generate?grant_type=client_credentials`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Basic &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 20 seconds timeout&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No access_token returned from Daraja OAuth endpoint.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 5. CORE B2B PAYMENT FUNCTION&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * Sends a B2B payment request to Daraja.
 * Steps:
 *   1. Get a fresh access token.
 *   2. Build the B2B request payload according to Daraja spec.
 *   3. POST to /mpesa/b2b/v1/paymentrequest.
 *   4. Store pending transaction details for later callback correlation.
 *   5. Return the immediate API response.
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendB2BPayment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ---- Step 1: Get token ----&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// ---- Step 2: Read required fields from .env ----&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initiator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_INITIATOR_NAME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;securityCredential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_SECURITY_CREDENTIAL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_PARTY_A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// Your shortcode&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_PARTY_B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// Destination shortcode&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_AMOUNT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accountReference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_ACCOUNT_REF&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Max 13 chars&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remarks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_REMARKS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// Max 100 chars&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requester&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MPESA_REQUESTER&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// Mobile number of person requesting&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUBLIC_BASE_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// e.g., https://abc123.ngrok.io&lt;/span&gt;

  &lt;span class="c1"&gt;// Build callback URLs that M-Pesa will call after processing&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;publicBaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/callbacks/mpesa/b2b/result`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeoutUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;publicBaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/callbacks/mpesa/b2b/timeout`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// ---- Step 3: Construct the B2B payload ----&lt;/span&gt;
  &lt;span class="c1"&gt;// Field explanations:&lt;/span&gt;
  &lt;span class="c1"&gt;// - CommandID: "BusinessPayBill" means Paybill to Paybill.&lt;/span&gt;
  &lt;span class="c1"&gt;// - SenderIdentifierType: "4" = Shortcode (Paybill/Till)&lt;/span&gt;
  &lt;span class="c1"&gt;// - RecieverIdentifierType: "4" = Shortcode (note the typo in API spec "Reciever")&lt;/span&gt;
  &lt;span class="c1"&gt;// - SecurityCredential: Encrypted initiator password (NOT plain text)&lt;/span&gt;
  &lt;span class="c1"&gt;// - Initiator: Username of the B2B initiator (set in M-Pesa Org Portal)&lt;/span&gt;
  &lt;span class="c1"&gt;// - Requester: Optional mobile number for audit&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Initiator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initiator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;SecurityCredential&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;securityCredential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;CommandID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BusinessPayBill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;SenderIdentifierType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;RecieverIdentifierType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt; &lt;span class="c1"&gt;// Ensure integer&lt;/span&gt;
    &lt;span class="na"&gt;PartyA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;partyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;PartyB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;partyB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;AccountReference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;accountReference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Requester&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requester&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Remarks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;remarks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;QueueTimeOutURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeoutUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ResultURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resultUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// ---- Step 4: Send the request ----&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mpesa/b2b/v1/paymentrequest`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30 seconds&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ---- Step 5: Store pending transaction for callback correlation ----&lt;/span&gt;
  &lt;span class="c1"&gt;// The immediate response contains an OriginatorConversationID.&lt;/span&gt;
  &lt;span class="c1"&gt;// When the callback arrives, it will include the same ID, allowing us to&lt;/span&gt;
  &lt;span class="c1"&gt;// link the callback to the original request.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originatorConversationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;OriginatorConversationID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originatorConversationId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pendingByOriginatorConversationId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originatorConversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;partyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;partyB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;accountReference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Return the raw API response (for logging / debugging)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 6. WEBHOOK: RESULT URL (FINAL TRANSACTION STATUS)&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * M-Pesa calls this endpoint after processing the B2B payment.
 * It contains the final ResultCode (0 = success, others = error).
 *
 * IMPORTANT: This is your source of truth for transaction settlement.
 * Do NOT rely only on the initial POST response.
 */&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/callbacks/mpesa/b2b/result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Daraja sometimes nests the result inside Result or Body.Result.&lt;/span&gt;
  &lt;span class="c1"&gt;// This line tries all common locations.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originatorConversationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;OriginatorConversationID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ResultCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// "0" = success&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultDesc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ResultDesc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Human-readable message&lt;/span&gt;

  &lt;span class="c1"&gt;// Look up the pending transaction using the ID from the callback&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originatorConversationId&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;pendingByOriginatorConversationId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originatorConversationId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Log everything – in production you'd store this in a database&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B2B Result callback:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;originatorConversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;resultDesc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// Shows original request details&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// Full raw payload&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// You must respond with a specific JSON object to acknowledge receipt.&lt;/span&gt;
  &lt;span class="c1"&gt;// ResultCode "0" means your server accepted the callback.&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ResultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ResultDesc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accepted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 7. WEBHOOK: TIMEOUT URL (WHEN DARAJA CAN'T REACH YOUR RESULT URL)&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * If M-Pesa attempts to call your ResultURL but gets a timeout or network error,
 * it will call this TimeoutURL instead. This usually indicates a problem with
 * your server's availability.
 */&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/callbacks/mpesa/b2b/timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B2B Timeout callback – check your ResultURL server!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Still acknowledge to prevent repeated retries&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ResultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ResultDesc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accepted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 8. EXPRESS ROUTE: TRIGGER A B2B PAYMENT&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * This endpoint is called by you (or your frontend) to initiate a B2B payment.
 * POST /send-b2b
 *
 * Example using curl:
 *   curl -X POST http://localhost:5000/send-b2b
 */&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/send-b2b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendB2BPayment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log and return a user-friendly error&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Often contains Daraja's error details&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 9. START THE SERVER&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`M-Pesa B2B server running on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Trigger B2B by POST http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/send-b2b`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`ResultURL: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/callbacks/mpesa/b2b/result`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`TimeoutURL: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/callbacks/mpesa/b2b/timeout`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;




&lt;h3&gt;
  
  
  6) Run and Test
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Start your public tunnel:

&lt;ul&gt;
&lt;li&gt;ngrok example: &lt;code&gt;ngrok http 5000&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Put the HTTPS tunnel URL in &lt;code&gt;PUBLIC_BASE_URL&lt;/code&gt; inside &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start app:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm run dev&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Trigger payment:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;curl -X POST http://localhost:5000/send-b2b&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Watch logs for:

&lt;ul&gt;
&lt;li&gt;immediate request response (accept/reject)&lt;/li&gt;
&lt;li&gt;later callback payload on &lt;code&gt;/callbacks/mpesa/b2b/result&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  7) Common Errors (and What They Mean)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Most likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;401&lt;/code&gt; on token request&lt;/td&gt;
&lt;td&gt;Wrong consumer key/secret&lt;/td&gt;
&lt;td&gt;Regenerate and update &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Invalid SecurityCredential&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Credential does not match initiator/certificate&lt;/td&gt;
&lt;td&gt;Regenerate credential correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial call succeeds but no callback&lt;/td&gt;
&lt;td&gt;Callback URL unreachable&lt;/td&gt;
&lt;td&gt;Verify public HTTPS URL and firewall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ResponseCode: 0&lt;/code&gt; but transfer not complete&lt;/td&gt;
&lt;td&gt;Async model misunderstood&lt;/td&gt;
&lt;td&gt;Wait for result callback before marking success&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  8) Production Hardening Checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Store secrets in a vault/Secret Key Managers, not in files committed to git&lt;/li&gt;
&lt;li&gt;Make callback handlers idempotent (same callback may retry)&lt;/li&gt;
&lt;li&gt;Persist &lt;code&gt;ConversationID&lt;/code&gt; and &lt;code&gt;OriginatorConversationID&lt;/code&gt; for reconciliation&lt;/li&gt;
&lt;li&gt;Redact sensitive fields from your logs&lt;/li&gt;
&lt;li&gt;Add retries with backoff only for safe transient failures&lt;/li&gt;
&lt;li&gt;Set system admin alerts on spikes in timeout/error result codes&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;You can get more detailed info from safaricom's portal on the link below&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.safaricom.co.ke/" rel="noopener noreferrer"&gt;Safaricom Daraja Developer Portal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mpesa</category>
      <category>darajaapis</category>
      <category>fintech</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Data Modeling in Power BI Explained (Without the Headache)</title>
      <dc:creator>Eddy Odhiambo</dc:creator>
      <pubDate>Sun, 29 Mar 2026 23:39:09 +0000</pubDate>
      <link>https://dev.to/supamodo/data-modeling-in-power-bi-explainedwithout-the-headache-9cf</link>
      <guid>https://dev.to/supamodo/data-modeling-in-power-bi-explainedwithout-the-headache-9cf</guid>
      <description>&lt;p&gt;&lt;em&gt;What really happens when tables “talk” to each other in Power BI. Joins, relationships, star schemas, and the mistakes that quietly break your dashboards.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7x9le9ysdpo6xebbfuv1.webp" alt="Data modelling illustration" width="800" height="357"&gt;
&lt;/h2&gt;

&lt;p&gt;As I continue my Data Science &amp;amp; Engineering journey at &lt;strong&gt;LuxDev HQ&lt;/strong&gt;, I’m noticing the same pattern everywhere: people rush to pretty charts, then spend hours wondering why the numbers don’t line up. Nine times out of ten, the issue isn’t the visual. It’s the model underneath.&lt;/p&gt;

&lt;p&gt;This article is my attempt to explain data modeling in Power BI in plain language: what it is, how &lt;code&gt;joins&lt;/code&gt; and &lt;code&gt;relationships&lt;/code&gt; differ, how &lt;code&gt;fact&lt;/code&gt; and &lt;code&gt;dimension&lt;/code&gt; tables fit in, and how &lt;code&gt;star&lt;/code&gt;, &lt;code&gt;snowflake&lt;/code&gt;, and &lt;code&gt;flat&lt;/code&gt; layouts show up in real work. I’ll also walk through &lt;strong&gt;where&lt;/strong&gt; in Power BI you actually click to build these things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why data modeling matters before you touch a chart
&lt;/h3&gt;

&lt;p&gt;Power BI makes it easy to drag fields onto a canvas. That’s the fun part. The hard part is making sure that when you filter by “March” or “Customer A,” the engine is counting the &lt;strong&gt;right rows&lt;/strong&gt; the &lt;strong&gt;right number of times&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When the model is wrong, you get symptoms like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Totals that don’t match your source system
&lt;/li&gt;
&lt;li&gt;Duplicates that appear out of nowhere
&lt;/li&gt;
&lt;li&gt;Slicers that seem to “half work”
&lt;/li&gt;
&lt;li&gt;Reports that were fine in Excel but fall apart in Power BI
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost always, that traces back to how tables are connected, either in &lt;strong&gt;Power Query&lt;/strong&gt; (merges/joins) or in the &lt;strong&gt;Model&lt;/strong&gt; (relationships). Fixing visuals won’t fix that but fixing the model might.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Data modeling decides how your tables are structured and how they connect, so that measures and filters behave correctly.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Power BI, you’re usually doing two related things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Shaping data&lt;/code&gt; — cleaning, renaming, sometimes &lt;strong&gt;merging&lt;/strong&gt; tables into one wider table (Power Query).
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Connecting tables&lt;/code&gt; — leaving tables separate but linking them with relationships so DAX can aggregate across them (Model view).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both matter. They’re not the same tool, and mixing them up is where most people often get stuck.&lt;/p&gt;




&lt;h3&gt;
  
  
  Part 1 — Joins: combining tables in Power Query
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;join&lt;/strong&gt; answers: &lt;em&gt;“If I stack these two tables side by side on a key, which rows do I keep?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In Power BI Desktop, joins show up when you &lt;strong&gt;merge&lt;/strong&gt; queries, You pick two tables, the matching column(s), and a &lt;strong&gt;join kind&lt;/strong&gt;. That’s the same logic used in SQL joins.&lt;/p&gt;

&lt;p&gt;Below is a simple mental picture: &lt;strong&gt;Table A&lt;/strong&gt; (left) and &lt;strong&gt;Table B&lt;/strong&gt; (right), matched on something like &lt;code&gt;CustomerID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjru6mz2d92y1ryv5cznz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjru6mz2d92y1ryv5cznz.png" alt="JOIN Illustration" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  INNER JOIN — only the overlap
&lt;/h4&gt;

&lt;p&gt;This keeps rows where the key exists in both tables. For example you have &lt;em&gt;Orders&lt;/em&gt; and &lt;em&gt;Customers&lt;/em&gt;. You only want orders where you actually have a customer record (no “orphan” orders).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;When it’s useful:&lt;/em&gt; Clean reporting, official numbers, “show me only valid combinations.”&lt;/p&gt;

&lt;h4&gt;
  
  
  LEFT JOIN — everything on the left, plus matches from the right
&lt;/h4&gt;

&lt;p&gt;This keeps all rows from the left table. If there’s no match on the right, you still keep the row; the extra columns from the right show as &lt;em&gt;null&lt;/em&gt; (blank).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;When it’s useful:&lt;/em&gt; Operational reality — you rarely throw away transactions just because someone forgot to create the customer card.&lt;/p&gt;

&lt;h4&gt;
  
  
  RIGHT JOIN — everything on the right, plus matches from the left
&lt;/h4&gt;

&lt;p&gt;Maintains the same idea as LEFT JOIN, but the “keep everything” side is the right table.&lt;/p&gt;

&lt;p&gt;In practice, many people &lt;em&gt;swap which table is left vs right&lt;/em&gt; and use a &lt;em&gt;LEFT&lt;/em&gt; join instead, because it’s easier to read. RIGHT isn’t wrong; it’s just less common.&lt;/p&gt;

&lt;h4&gt;
  
  
  FULL OUTER JOIN — everything from both sides
&lt;/h4&gt;

&lt;p&gt;This keeps all rows from both tables. Where there’s no match, you get blanks on the missing side. For example during reconciliation in &lt;em&gt;System A payments&lt;/em&gt; vs &lt;em&gt;System B payments&lt;/em&gt;*, you would want to see what’s in A only, what’s in B only, and what’s in both.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;When it’s useful:&lt;/em&gt; During audits, matching two different sources, finding gaps.&lt;/p&gt;

&lt;h4&gt;
  
  
  LEFT ANTI JOIN — “only in the left, not in the right”
&lt;/h4&gt;

&lt;p&gt;Returns rows from the left table that have &lt;em&gt;no&lt;/em&gt; match in the right table. For example payments that appear in &lt;em&gt;M-Pesa&lt;/em&gt; (or a bank file) but never landed in your internal system.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;When it’s useful:&lt;/em&gt; Exception lists, “what are we missing?”, data quality checks.&lt;/p&gt;

&lt;h4&gt;
  
  
  RIGHT ANTI JOIN — “only in the right, not in the left”
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Same as LEFT ANTI, but flipped:&lt;/em&gt; rows that exist only in the &lt;em&gt;right&lt;/em&gt; table for example customer records that exist in a CRM but have &lt;em&gt;never&lt;/em&gt; placed an order.&lt;/p&gt;




&lt;h3&gt;
  
  
  Part 2 — Relationships: connecting tables in the Model
&lt;/h3&gt;

&lt;p&gt;Relationships are built on the &lt;strong&gt;Model view&lt;/strong&gt; (the diagram with boxes and lines). Drag from one column to another, or click on &lt;em&gt;Modeling → Manage Relationships&lt;/em&gt; then add/edit/delete in a list. You will need to set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Cardinality&lt;/em&gt; (one-to-many, many-to-many, one-to-one)
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Cross-filter direction&lt;/em&gt; (single or both)
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Active vs inactive&lt;/em&gt; when you have more than one path between the same tables
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cardinality in everyday terms
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;One-to-many (1:M)&lt;/code&gt;&lt;/em&gt; — this is the default star-schema pattern for example one customer having many orders. The &lt;em&gt;“many”&lt;/em&gt; side is usually your &lt;em&gt;fact&lt;/em&gt; table (transactions). The &lt;em&gt;“one”&lt;/em&gt; side is the &lt;em&gt;dimension&lt;/em&gt; (customer, product, date).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;Many-to-many (M:M)&lt;/code&gt;&lt;/em&gt; — this duplicates on &lt;em&gt;both&lt;/em&gt; sides of the relationship. Power BI can model this, but it’s easier to get wrong measures if you don’t understand what’s happening. Often the fix is a &lt;em&gt;bridge table&lt;/em&gt; or reshaping the data.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;One-to-one (1:1)&lt;/code&gt;&lt;/em&gt; mostly rare but sometimes used when one entity was split across two tables for maintenance or row size reasons.&lt;/p&gt;

&lt;h4&gt;
  
  
  Active vs inactive relationships
&lt;/h4&gt;

&lt;p&gt;You can have &lt;em&gt;multiple relationships&lt;/em&gt; between the same two tables, but &lt;em&gt;only one is active&lt;/em&gt; at a time for a given path.&lt;/p&gt;

&lt;h4&gt;
  
  
  Joins vs relationships — side by side
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Joins (Power Query merge)&lt;/th&gt;
&lt;th&gt;Relationships (Model)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where&lt;/td&gt;
&lt;td&gt;Power Query Editor&lt;/td&gt;
&lt;td&gt;Model view / Manage Relationships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect&lt;/td&gt;
&lt;td&gt;Can combine into one table (wider result)&lt;/td&gt;
&lt;td&gt;Tables stay separate; engine knows how they link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When it runs&lt;/td&gt;
&lt;td&gt;During &lt;em&gt;refresh&lt;/em&gt; (data load)&lt;/td&gt;
&lt;td&gt;At &lt;em&gt;query time&lt;/em&gt; for visuals and DAX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flexibility&lt;/td&gt;
&lt;td&gt;Fixed shape after refresh&lt;/td&gt;
&lt;td&gt;Multiple measures can use different logic (e.g. &lt;code&gt;USERELATIONSHIP&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>powerbi</category>
      <category>data</category>
      <category>luxdevhq</category>
      <category>analytics</category>
    </item>
    <item>
      <title>From Spreadsheets to Insights: How Excel Powers Real-World Data Analysis</title>
      <dc:creator>Eddy Odhiambo</dc:creator>
      <pubDate>Sun, 29 Mar 2026 16:09:44 +0000</pubDate>
      <link>https://dev.to/supamodo/from-spreadsheets-to-insights-how-excel-powers-real-world-data-analysis-50ie</link>
      <guid>https://dev.to/supamodo/from-spreadsheets-to-insights-how-excel-powers-real-world-data-analysis-50ie</guid>
      <description>&lt;p&gt;&lt;em&gt;A technical look at what Excel really is, where it wins in production workflows, and the formulas analysts lean on when money and operations are on the line.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcf8x0uae3r72m60s2ed3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcf8x0uae3r72m60s2ed3.webp" alt="Excel Workbook image" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why excel still matters
&lt;/h2&gt;

&lt;p&gt;In a world of warehouses, notebooks, and orchestration pipelines, &lt;strong&gt;Microsoft Excel&lt;/strong&gt; remains the default “operating system” for ad‑hoc analysis, financial reconciliations, regulatory exports, and operational reporting. It is not nostalgia, it is &lt;strong&gt;latency&lt;/strong&gt;: the time from question to answer when a stakeholder or manager needs a number &lt;em&gt;today&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This article is gives a crisp mental model of Excel as an analytical tool, plus &lt;strong&gt;concrete patterns&lt;/strong&gt; that show up in real financial and operational systems from my observations as I begin on my Data Science &amp;amp; Engineering Journey at &lt;strong&gt;LuxDev HQ&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Excel is (and is not)
&lt;/h2&gt;

&lt;p&gt;Excel is a grid-based calculation environment: rows, columns, cells that can hold values, text, or formulas that reference other cells. Under the hood you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Recalculation&lt;/em&gt; when inputs change
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Built-in functions&lt;/em&gt; (statistical, financial, text, logical, lookup)
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;PivotTables&lt;/em&gt; for aggregation without writing formulas for every slice
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;What-if tools&lt;/em&gt; (Goal Seek, Scenario Manager, Data Tables)
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Lightweight automation&lt;/em&gt; via Office Scripts / VBA (platform-dependent)
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Integration&lt;/em&gt; with external data (Power Query on desktop, connectors in Microsoft 365)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Excel is &lt;strong&gt;not&lt;/strong&gt; a production database, a source of truth for concurrent writes at scale, or a substitute for versioned ELT. It &lt;strong&gt;is&lt;/strong&gt; unbeatable for exploration, reconciliation, and human-in-the-loop workflows that bridge raw extracts and decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some practical real-world use cases of excel
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Sales and revenue operations
&lt;/h4&gt;

&lt;p&gt;Imagine downloading thousands of lines of sales data from CRM platforms and management asks for breakdowns by product or region or comparisons between months. This is where excel comes in, instead of going back to the IT team to help you add those factors in the reports or waiting for a dashboard update, you just open Excel and reshape the data yourself.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Managing Stocks, Inventory and supply chain
&lt;/h4&gt;

&lt;p&gt;Even in businesses with proper systems, people still fall back to Excel to answer questions like what should be reordered this week, what product is running out? Most stores do not have ERP systems to automate such so they rely on excel to keep simple sheets with Item names, Quantities, Minimum levels etc.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Internal dashboards (the “manager view”)
&lt;/h4&gt;

&lt;p&gt;Sometimes, people don’t want dashboards or tools, they just need a file they can open and understand immediately. Excel makes this easy through pivot-driven summaries + conditional formatting with a summary at the top,  a few highlighted numbers, and maybe some color to show what’s good or bad&lt;/p&gt;

&lt;p&gt;No logins, no loading screens, just open and see what’s going on.&lt;/p&gt;




&lt;h3&gt;
  
  
  Making sense of data with lookups in excel
&lt;/h3&gt;

&lt;p&gt;A very common situation in Excel is this; You have one list with basic data, and another list with extra details and you want to connect them. For example, one sheet has product codes and another sheet has the product names and prices. Excel can help you “fill in the gaps” automatically through its lookup functions&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;VLOOKUP&lt;/code&gt; — vertical join in one formula
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Syntax:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;VLOOKUP(lookup_value, table_array, col_index_num, [range_lookup])&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lookup_value&lt;/code&gt;&lt;/strong&gt;: what you’re matching (e.g. customer ID in the transaction row).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;table_array&lt;/code&gt;&lt;/strong&gt;: the lookup table; &lt;strong&gt;the first column must contain the key&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;col_index_num&lt;/code&gt;&lt;/strong&gt;: which column to return (1 = key column).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;range_lookup&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;FALSE&lt;/code&gt; for exact match (almost always what you want in finance). &lt;code&gt;TRUE&lt;/code&gt; means approximate match (sorted data—for tax brackets, depreciation bands, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it like this - “Find this item in another table, and bring me the information next to it.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:  “You have a product code, and you want Excel to return the price.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Assume &lt;code&gt;Transactions!A2&lt;/code&gt; has PID &lt;code&gt;P-1001&lt;/code&gt;. Tariffs are on sheet &lt;code&gt;Tariffs&lt;/code&gt; with columns &lt;code&gt;PID | ListPrice | Cost | MarginPct&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=VLOOKUP(A2, Tariffs!$A$2:$D$500, 4, FALSE)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  &lt;code&gt;HLOOKUP&lt;/code&gt; — follows the same idea as VLOOKUP but with horizontal keys
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Syntax:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;HLOOKUP(lookup_value, table_array, row_index_num, [range_lookup])&lt;/code&gt;&lt;/p&gt;


&lt;h4&gt;
  
  
  &lt;code&gt;INDEX&lt;/code&gt; + &lt;code&gt;MATCH&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;VLOOKUP&lt;/code&gt; breaks when someone inserts a column to the left of the return field and that's why most people use the INDEX + MATCH. It decouples &lt;strong&gt;where the key is&lt;/strong&gt; from &lt;strong&gt;where the value is&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=INDEX(Tariffs!$D$2:$D$500, MATCH(A2, Tariffs!$A$2:$A$500, 0))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MATCH&lt;/code&gt; finds the &lt;strong&gt;row&lt;/strong&gt; of the exact PID.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INDEX&lt;/code&gt; returns the margin from column D.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On modern Excels after 2021, &lt;strong&gt;&lt;code&gt;XLOOKUP&lt;/code&gt;&lt;/strong&gt; replaces most &lt;code&gt;VLOOKUP&lt;/code&gt;/&lt;code&gt;HLOOKUP&lt;/code&gt;/&lt;code&gt;INDEX/MATCH&lt;/code&gt; patterns with one readable function which makes it easier.&lt;/p&gt;

&lt;p&gt;Other functions like &lt;code&gt;SUMIFS&lt;/code&gt; and &lt;code&gt;COUNTIFS&lt;/code&gt; come in when doing multi-criteria analysis, or when dealing with conditions and/or adding filters to your numbers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I learned: a personal reflection
&lt;/h3&gt;

&lt;p&gt;When I started working with data to view and manage reports, I treated Excel as “the boring office app” and chased fancier tools first. That was backwards. Learning Excel forced me to ask better questions, understand joins between data (better than how SQL did) and those questions didn’t go away when I move to bigger tools, they just improved my understanding.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>datascience</category>
      <category>microsoft</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
