> ## Documentation Index
> Fetch the complete documentation index at: https://developers.1-funnel.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an SMS status receiver with Node.js

> Run a Node.js SMS application that persists idempotency keys, verifies webhook signatures, deduplicates receipts and retrieves message status with SQLite.

Build a small application that joins an SMS request to a saved message ID and a
persisted status observation. The receiver verifies the exact signed bytes and
stores each event before acknowledging it. A separate worker retrieves the
current SMS resource so a late notification does not overwrite status using
arrival order.

This tutorial uses Node.js 24 and SQLite on one machine. It uses ordinary HTTP
clients, not an official SDK. The [Send an SMS](/guides/sms/send-sms) and
[Message status](/guides/sms/message-status) guides retain cURL, JavaScript,
Python and Java request examples; this application tutorial focuses on Node.js.

## 1. Get the application and run the offline test

```bash theme={null}
 git clone https://github.com/1funnel/1funnel-api-samples.git
 cd 1funnel-api-samples/samples/sms/track-delivery/nodejs
 node --version
 node --test tests/app.test.mjs
```

Use Node.js 24. No `npm install` is needed for this application. SQLite may print
an experimental API warning. The test starts local HTTP servers and a temporary
database; no credentials or live SMS sends are needed. Read or download the
[complete application and README](https://github.com/1funnel/1funnel-api-samples/tree/main/samples/sms/track-delivery/nodejs).

The test exercises a lost acceptance response, database restart, duplicate and
out-of-order events, stale signatures, secret-version checks, rejected business
and event IDs, failed storage and a worker retry. A local API stub proves the
application's recovery paths; it does not establish live delivery or charging.

## 2. Prepare approved Sandbox access

Complete the [Quickstart](/getting-started/quickstart). You need a Sandbox access
token, business ID, authorized sender and permitted test recipient. Use the same
business throughout this tutorial, with access to send SMS, retrieve message
status and configure webhooks. If these features are unavailable for your
business, contact [Support](/resources/support) before continuing.

In each terminal used below, set the same environment variables:

```bash theme={null}
umask 077
export API_BASE_URL=https://api.sandbox.1-funnel.org
export BUSINESS_ID='<issued-business-id>'
export ACCESS_TOKEN='<issued-access-token>'
export DB_PATH="$PWD/sms-status.sqlite"
export WEBHOOK_KEYS_FILE="$PWD/keys.json"
```

Use a persistent local directory and a separate database per API host and
business. The database stores recipient numbers and message content: restrict
its directory and backups to authorized users. Keep secrets out of source control.

## 3. Register and start the receiver

Prepare an HTTPS URL routed to `http://127.0.0.1:8080/hooks/1funnel` by your trusted
reverse proxy or development tunnel. Preserve the request bytes and webhook
headers; expose only this path. Follow
[Configure endpoints](/guides/webhooks/configure-endpoints) and
[Create a webhook endpoint](/api-reference/webhooks/create-a-webhook-endpoint)
to register that HTTPS URL with these event types:

```json theme={null}
["sms.message.sent.v1", "sms.message.delivered.v1", "sms.message.failed.v1"]
```

Check HTTP `201`, the accepted subscriptions and any warnings before proceeding.
Save the returned endpoint ID and securely retain the signing secret. Use the
original secret version `1` until an operator coordinates a rotation.
Create private `keys.json`:

```json theme={null}
{
  "1": {
    "secret": "<issued-signing-secret>",
    "validFrom": 0,
    "validUntil": 4102444800
  }
}
```

The example end date is a configurable bound, not an API secret lifetime. For
rotation, install both versions with operator-agreed activation and retirement
windows, then restart the receiver. See
[Overlapping secret versions](/guides/webhooks/verify-signatures#accept-overlapping-secret-versions).

```bash theme={null}
chmod 600 keys.json
node src/app.mjs serve
```

The receiver checks HMAC, timestamp tolerance, trusted secret version, business,
event type and the signed envelope ID against the header. It stores the inbox row
before returning `204`; duplicates also receive `204`. Storage failure returns
`503` so delivery can retry. Verification failure returns `401`.

## 4. Submit one SMS and save its ID

In a second terminal in the same directory, create `request.json`. Replace the
sender and recipient with your approved values:

```json theme={null}
{
  "from": "ONEFUNNEL",
  "to": "+2348012345678",
  "classification": "transactional",
  "message": "Your order ORD-1001 is confirmed."
}
```

```bash theme={null}
node src/app.mjs send order-1001 request.json
node src/app.mjs inspect
```

The application saves a key and the request bytes **before** making
[Submit an individual SMS](/api-reference/sms/submit-an-individual-sms). It
requires HTTP `202`, then saves the returned `id` and `status`. Acceptance is not
delivery confirmation. The actual response supplies the ID; do not copy one from
an example.

If the connection fails, run the exact same command with the unchanged file and
database. The application reuses the saved key. Once a message ID is saved,
repeating the command returns that saved result without another POST. A changed
payload under the same application operation ID is rejected. Review HTTP errors
before retrying; do not change the key to bypass them.

The [idempotency replay window and recovery rules](/getting-started/idempotency)
apply. If acceptance remains uncertain after seven days, investigate the original
request instead of inventing a new key or deleting local state.

## 5. Process notifications and inspect the saved status

In a third terminal, with the same environment and database:

```bash theme={null}
node src/app.mjs work
node src/app.mjs inspect
```

`work` processes up to 50 pending inbox entries and exits. Repeat it periodically;
run only one worker at a time. For each known message ID it calls
[Retrieve an individual SMS](/api-reference/sms/retrieve-an-individual-sms),
requires HTTP `200`, then commits the snapshot and processed marker together.
`inspect` shows `message_id`, `status`, `checked_at` and inbox `processed_at`.
A status of `delivered` records the observed delivery outcome; `sent` alone does
not confirm handset delivery.

A failed GET leaves the event pending for another worker attempt. Respect rate
limits and `Retry-After` when scheduling retries. An event that arrives before the
acceptance response stays pending until the saved send has a message ID. The
worker reads current REST state instead of treating a late `sent` event as newer
than a `delivered` event. See the
[published delivered event](/api-reference/events/sms.message.delivered.v1)
for its actual envelope and fields.

## 6. Recover a missing notification

```bash theme={null}
node src/app.mjs reconcile order-1001
node src/app.mjs inspect
```

Run reconciliation when no worker is active. This retrieves the saved message ID
and persists its current status without sending another SMS. If the ID was never
saved, resolve the original send first. For delivery investigation, follow
[Webhook retries and recovery](/guides/webhooks/retries-duplicates#investigate-a-missing-or-failed-delivery).
A missing notification is not a reason to repeat the send.

## Before using this design in production

The application deliberately has one worker, no external business side effects
and a local SQLite database. Add supervised execution, retry backoff, monitoring
for pending and unknown-message events, storage limits, retention and tested
backups before operating it. Monitor `checked_at`: a saved snapshot is not a
promise that status will never change. Keep the receiver clock synchronized.
Scaling workers requires coordinated claims; adding business effects requires
an idempotent transaction or outbox design. See the application README for the
single-machine boundaries and [durable receipt patterns](/guides/webhooks/retries-duplicates).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.