How it works
Moeba sits between your AI agent and the mobile app. Your agent receives webhook calls from Moeba and responds with JSON. The mobile app renders everything automatically.
Your Agent
Webhook endpoint
Moeba
Routes & authenticates
Mobile App
Renders UI
- Moeba → Your Agent: JSON-RPC 2.0 requests signed with HMAC-SHA256
- Your Agent → Moeba: JSON-RPC responses with messages, workflows, and components
- Proactive messaging: Your agent can also push messages to users via the REST API
Quickstart
Get a working agent in under 5 minutes with the TypeScript SDK.
1. Install the SDK
2. Create your webhook handler
import { WebhookHandler } from 'moeba-sdk';
const handler = new WebhookHandler({
webhookSecret: process.env.WEBHOOK_SECRET,
onMessage: async (message, ctx) => {
// message.text contains the user's message
// ctx.phoneNumber has their verified phone number
return ctx.reply(`You said: ${message.text}`);
},
});
3. Wire it up to your server
The handler is framework-agnostic. Pass the raw body string and headers, get back a JSON response.
import Fastify from 'fastify';
const app = Fastify();
// Important: pass the raw body string for signature verification
app.addContentTypeParser('application/json', { parseAs: 'string' },
(_req, body, done) => done(null, body));
app.post('/webhook', async (req, reply) => {
const response = await handler.handle(req.body, req.headers);
return reply.send(response);
});
app.listen({ port: 3001 });
4. Configure in the admin portal
Go to admin.moeba.co.za, register your agent, and set the webhook URL to your server's /webhook endpoint. Copy the webhook secret into your environment variables.
Handling messages
The WebhookHandler processes three types of incoming requests:
receive onMessage
Called when a user sends a text message, photo, or location.
onMessage: async (message, ctx) => {
message.text // User's message text
message.attachments // Array of { type, url, name, mimeType }
message.location // { latitude, longitude } if shared
ctx.phoneNumber // Verified E.164 phone number
ctx.connectionId // Unique connection identifier
ctx.userName // User's display name
return ctx.reply('Got it!');
}
receive onAction
Called when a user completes a workflow, OAuth flow, or secret submission.
onAction: async (action, ctx) => {
if (action.type === 'workflow_completed') {
action.workflowName // e.g. "Trip Booking"
action.data // { destination: "Paris", departure: "2025-06-15" }
}
if (action.actionId === 'oauth_complete') {
action.data.access_token // OAuth access token
action.data.refresh_token // OAuth refresh token
}
if (action.actionId === 'secret_submitted') {
action.data.name // Secret name
action.data.value // Secret value
}
return ctx.reply('Action processed!');
}
Rich workflows
Workflows let your agent collect structured data through native mobile UI. Define multi-step forms with validation and the app renders them automatically.
import { WorkflowBuilder } from 'moeba-sdk';
const workflow = WorkflowBuilder.create('Trip Booking', 'TRIP')
.text('destination', 'Where to?', { placeholder: 'e.g. Paris' })
.date('departure', 'Departure Date')
.select('budget', 'Budget', [
{ label: 'Budget', value: 'budget' },
{ label: 'Mid-range', value: 'mid' },
{ label: 'Luxury', value: 'luxury' },
])
.number('travelers', 'Number of Travelers')
.photo('passport', 'Passport Photo', { maxCount: 1 })
.build();
// Attach to a reply
return ctx.reply('Let me collect your booking details.')
.withWorkflow(workflow);
Available field types
.text()
Single-line input
.email()
Email keyboard
.phone()
Phone keyboard
.number()
Numeric input
.date()
Date picker
.textarea()
Multi-line input
.select()
Option buttons
.checkbox()
Yes/No toggle
.photo()
Camera/gallery
.location()
GPS capture
Each field accepts an options object for description, placeholder, required (default true), canSkip, and validation (with pattern, min, max, minLength, maxLength).
Proactive messaging
Send messages to users without waiting for them to message first. Use the MoebaClient for proactive outreach, notifications, and progress updates.
import { MoebaClient } from 'moeba-sdk';
const moeba = new MoebaClient({
apiKey: process.env.MOEBA_AGENT_KEY, // from admin portal
});
// Send by connection ID
await moeba.send('conn_abc', 'Your report is ready!');
// Send by phone number
await moeba.sendToPhone('+27821234567', 'Welcome to our service!');
// Show typing/progress indicator (auto-throttled to 1/sec)
await moeba.progress('conn_abc', 'Generating your report...');
// Send with components
await moeba.sendWithComponents('conn_abc', 'Please fill in this form', [workflow]);
OAuth & secrets
Request access to user accounts or collect API keys securely through native UI components.
import { OAuthConnect, SecretInput } from 'moeba-sdk';
// Request Gmail access
return ctx.reply('I need access to your Gmail.')
.withOAuthConnect(OAuthConnect.gmail({
scopes: ['https://www.googleapis.com/auth/gmail.readonly'],
}));
// Request an API key
return ctx.reply('I need your API key.')
.withSecretInput(SecretInput.create('weather-api', 'Weather API Key', {
description: 'Get your key at openweathermap.org',
}));
Supported OAuth providers: gmail, office365, github.
SDK reference
WebhookHandler
Framework-agnostic handler that verifies signatures and routes incoming requests.
new WebhookHandler({
webhookSecret: 'whsec_...', // from admin portal
onMessage: (message, ctx) => {}, // required
onAction: (action, ctx) => {}, // optional
onPing: (ctx) => {}, // optional
})
handler.handle(rawBody, headers) // returns Promise<JsonRpcResponse>
MoebaClient
Client for sending proactive messages to users.
new MoebaClient({ apiKey: 'mba_...' })
.send(connectionId, message) // send to connection
.sendToPhone(phoneNumber, message) // send by phone number
.sendWithComponents(id, text, comps) // send with UI components
.progress(connectionId, text) // show progress indicator
ResponseBuilder
Fluent builder returned by ctx.reply(). Chain methods to add components.
ctx.reply('message')
.withWorkflow(workflow) // attach a workflow form
.withOAuthConnect(oauth) // attach OAuth prompt
.withSecretInput(secret) // attach secret input
.escalateToOperator() // hand off to human
.operatorApprovalRequired() // require human approval
WorkflowBuilder
Fluent builder for multi-step form workflows.
WorkflowBuilder.create(name, referencePrefix)
.text(name, title, opts?) .email(name, title, opts?)
.phone(name, title, opts?) .number(name, title, opts?)
.date(name, title, opts?) .textarea(name, title, opts?)
.select(name, title, options) .checkbox(name, title, opts?)
.photo(name, title, opts?) .location(name, title, opts?)
.build()
Using without the SDK
The SDK is a convenience wrapper. You can implement the protocol directly in any language. Moeba sends JSON-RPC 2.0 requests to your webhook with these headers:
X-Moeba-Signature—t={timestamp},v1={hmac-sha256}X-Moeba-User— User's phone number (E.164)X-Moeba-Connection-Id— Connection identifierX-Moeba-User-Name— Display name
Verify the signature by computing HMAC-SHA256(secret, "{timestamp}.{rawBody}") and comparing it to the v1 value. Reject requests older than 5 minutes to prevent replay attacks.
Minimal Python example
import hmac, hashlib, json, time
from flask import Flask, request
app = Flask(__name__)
SECRET = "your_webhook_secret"
def verify(sig, body):
t, v1 = sig.split(",")
ts = t.split("=")[1]
expected = hmac.new(SECRET.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(v1.split("=")[1], expected)
@app.route("/webhook", methods=["POST"])
def webhook():
body = request.get_data(as_text=True)
if not verify(request.headers["X-Moeba-Signature"], body):
return {"error": "bad sig"}, 401
req = json.loads(body)
text = req["params"]["message"]["text"]
return {
"jsonrpc": "2.0",
"id": req["id"],
"result": {"message": {"text": f"You said: {text}"}}
}