Skip to main content

Setup

This guide connects a Signal number to OctoMesh, step by step: run the signal-cli-rest-api bridge, give it a Signal identity (register a dedicated number or link an existing account), wire the two pipeline nodes, and test.

Prerequisites

  • Docker (for the bridge).
  • A running OctoMesh mesh adapter for the target tenant, built with the FromSignal@1 / SignalSender@1 nodes.
  • A dedicated phone number for the bridge (prepaid SIM / eSIM / spare number). Signal binds one account per number — do not reuse a personal number you rely on. (You can instead link the bridge to an existing account — see Step 2, Option B.)
Naming used below

The examples use the bridge account +4366012345678 and the adapter at http://localhost:8080 for the bridge and tenant salzburgdev. Replace them with your own values.


Step 1 — Run the bridge

Run the bridge in native mode — it serves GET /v1/receive/{number}, which FromSignal@1 polls. (json-rpc mode is only needed for the push-webhook style and is not used here.) Mount a volume so the registered/linked account survives restarts — treat it as a credential and never commit it.

docker-compose.yml
services:
signal-cli-rest-api:
image: bbernhard/signal-cli-rest-api:latest
container_name: signal-cli-rest-api
restart: unless-stopped
environment:
- MODE=native
ports:
- "8080:8080"
volumes:
- ./signal-cli-config:/home/.local/share/signal-cli # account secrets — do NOT commit
docker compose up -d
curl http://localhost:8080/v1/about # {"versions":[...],"mode":"native",...}

Step 2 — Give the bridge a Signal identity

The bridge is the account. Registration needs a captcha and an SMS/voice code delivered to the number.

# 1. Request registration — this returns a captcha-required error the first time.
curl -X POST http://localhost:8080/v1/register/+4366012345678 -d '{}'

Solve the captcha, then retry with the token:

  1. Open https://signalcaptchas.org/registration/generate.html in a browser.
  2. Solve the captcha; right-click the “Open Signal” link → Copy Link (do not click it). The link looks like signalcaptcha://signal-hcaptcha.<token>.
  3. Register with the token (tokens are single-use and expire in a few minutes — a stale one fails with [403] Authorization failed, just fetch a fresh one):
curl -X POST http://localhost:8080/v1/register/+4366012345678 \
-H 'Content-Type: application/json' \
-d '{"captcha":"signalcaptcha://signal-hcaptcha.<token>"}'
  1. Signal sends a 6-digit SMS code to the number. Verify it:
curl -X POST http://localhost:8080/v1/register/+4366012345678/verify/123456
curl http://localhost:8080/v1/accounts # ["+4366012345678"]

The bridge shares an existing Signal account (its number stays the account identity).

curl "http://localhost:8080/v1/qrcodelink?device_name=octo-accounting-bridge" -o link.png

Open link.png and scan it in the Signal app: Settings → Linked Devices → Link New Device. Confirm with curl http://localhost:8080/v1/accounts.

caution

A linked bridge acts as that account — replies are sent from the person's own number, and the assistant will answer every incoming message. Prefer a dedicated number (Option A) for a real assistant, and use senderFilter while testing.

Step 3 — Wire the pipeline

A Signal pipeline mirrors the e-mail assistant: one FromSignal@1 trigger, then two branches (text → answer, attachment → document ingest), and SignalSender@1 for the reply.

triggers:
- type: FromSignal@1
apiUrl: "http://localhost:8080"
number: "+4366012345678" # quote it — an unquoted +number is parsed as an integer
pollingIntervalSeconds: 5
# senderFilter: "+4366098765432" # optional allow-list (contains match)
transformations:
- type: ForEach@1
iterationPath: $.Messages
targetPath: $.results
transformations:
# capture the sender BEFORE any nested ForEach rebinds $.key
- type: SetPrimitiveValue@1
targetPath: $.msgSource
valueType: String
valuePath: $.key.Source

# --- text question -> answer ---
- type: If@1
path: $.key.Message
operator: NotEquals
valueType: String
transformations:
- type: AnthropicAiQuery@1
question: "Answer the user's accounting question."
dataPaths: [ $.key.Message ]
apiKeyConfigurationName: AnthropicAiConfig
mcpServerUrl: "https://localhost:5017"
mcpServiceAccountConfigName: ServiceAccountConfig
responseFormat: text
maxToolRounds: 12
targetPath: $.replyText
continueOnError: true
- type: SignalSender@1
apiUrl: "http://localhost:8080"
number: "+4366012345678"
recipientPath: $.msgSource
messagePath: $.replyText
Two things that bite
  • Quote the number (number: "+43…"). Unquoted, YAML parses +4366012345678 as an integer and drops the leading +, breaking every bridge call.
  • Inside a nested ForEach, a valuePath is evaluated in item scope. To reach a value set at the message level (or a request-root field), use the $.full parent alias — e.g. $.full.msgSource. This is what the SourceSender provenance attribute uses.

Attachment ingest & provenance

For the attachment branch, FromSignal@1 already downloaded the bytes into $.key.Attachments[].Data (base64). Stage them as an UploadedDocument exactly like the app's upload flow, and set the origin so a follow-up can reach the sender back:

- type: CreateUpdateInfo@1
updateKind: Insert
ckTypeId: Meshmakers.Accounting/UploadedDocument
attributeUpdates:
- { attributeName: ProcessingState, attributeValueType: Enum, value: NEW }
- { attributeName: SourceChannel, attributeValueType: Enum, value: SIGNAL }
- { attributeName: SourceSender, attributeValueType: String, valuePath: $.full.msgSource }
# … OriginalFileName / FileRtId / FileHash / UploadedAt as usual

Step 4 — Deploy & test

Import the pipeline and deploy its data flow to the adapter:

octo-cli -c ImportRt -f rt-pipeline-signal-accounting-assistant.yaml -r -w
octo-cli -c DeployDataFlow -id <dataFlowRtId>

Confirm the trigger polls the bridge (adapter log shows GET http://localhost:8080/v1/receive/+4366012345678), then from another Signal account message the bridge number — the assistant answers within a few seconds. Send a photo/PDF to exercise the document-ingest branch.

Security & compliance

Add a sender allow-list (senderFilter, or a SignalContact mapping) before production — anyone who knows the number can otherwise query live data. Remember the phone number and any invoice images pass through the bridge and, for Q&A, into the AI/MCP context — cover it in your GDPR record.