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@1nodes. - 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.)
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.
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
Option A — Register a dedicated number (recommended)
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:
- Open
https://signalcaptchas.org/registration/generate.htmlin a browser. - Solve the captcha; right-click the “Open Signal” link → Copy Link (do not
click it). The link looks like
signalcaptcha://signal-hcaptcha.<token>. - 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>"}'
- 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"]
Option B — Link as a secondary device (no new number)
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.
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
- Quote the number (
number: "+43…"). Unquoted, YAML parses+4366012345678as an integer and drops the leading+, breaking every bridge call. - Inside a nested
ForEach, avaluePathis evaluated in item scope. To reach a value set at the message level (or a request-root field), use the$.fullparent alias — e.g.$.full.msgSource. This is what theSourceSenderprovenance 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.
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.