Build with AI? Download the agent spec
Get a complete markdown specification for your agent -- ready to paste into ChatGPT, Claude, Cursor, or any coding assistant. Includes your webhook secret, all endpoints, and protocol details.
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.sendToUser('+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 and office365.
Email & calendar
Agents can search, read, and send email, and manage calendar events on behalf of connected users. Users connect their account once via OAuth; Moeba handles token refresh automatically.
// Search emails
const results = await moeba.searchEmail(connectionId, {
query: 'from:john',
maxResults: 5,
});
// Read a specific email
const email = await moeba.readEmail(connectionId, {
messageId: 'msg_123',
platform: 'gmail',
});
// Send an email
await moeba.sendEmail(connectionId, {
to: 'alice@example.com',
subject: 'Hi',
body: 'Message body here',
});
Calendar
// List upcoming events
const events = await moeba.getCalendar(connectionId, { maxResults: 10 });
// Create a new event
await moeba.createCalendarEvent(connectionId, {
summary: 'Team standup',
startTime: '2026-04-11T09:00:00+02:00',
endTime: '2026-04-11T09:30:00+02:00',
});
Contacts
const contacts = await moeba.getContacts(connectionId, { search: 'dana' });
Scheduled tasks
Create cron jobs or one-shot scheduled tasks. When a schedule fires, Moeba calls the agent.cron method on your webhook.
Creating schedules
// Recurring: every day at 08:00
await moeba.createCronJob(connectionId, {
cron: '0 8 * * *',
prompt: 'Daily summary',
timezone: 'Africa/Johannesburg',
});
// One-shot: run once at a specific time
await moeba.createCronJob(connectionId, {
runAt: '2026-04-05T09:00:00+02:00',
prompt: 'Send report',
});
// List and delete
const jobs = await moeba.listCronJobs(connectionId);
await moeba.deleteCronJob(connectionId, cronJobId);
Handling cron callbacks
Add an onCron callback to your WebhookHandler:
const handler = new WebhookHandler({
webhookSecret: process.env.WEBHOOK_SECRET,
onMessage: async (message, ctx) => { /* ... */ },
onCron: async (cron, ctx) => {
cron.cronJobId // ID of the cron job
cron.prompt // The prompt string you set
cron.data // Optional data payload
cron.runCount // How many times this job has fired
return ctx.reply('Here is your daily summary...');
},
});
Memory & storage
Persistent key-value store and user memories, scoped per connection. No external database needed.
Key-value storage
// Store a value
await moeba.kvSet(connectionId, 'timezone', 'Africa/Johannesburg');
// Retrieve it
const tz = await moeba.kvGet(connectionId, 'timezone');
User memories
// Add a memory
await moeba.addMemory(connectionId, 'User prefers morning meetings');
// List all memories for this connection
const memories = await moeba.listMemories(connectionId);
Chat history
Retrieve Moeba-managed conversation history for a connection. Useful for providing context to your LLM.
const { messages } = await moeba.getHistory(connectionId, 20);
// messages: [{ role: 'user'|'agent'|'operator'|'system', text, timestamp }]
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
onCron: (cron, ctx) => {}, // optional — scheduled task callback
onPing: (ctx) => {}, // optional
})
handler.handle(rawBody, headers) // returns Promise<JsonRpcResponse>
MoebaClient
Client for proactive messaging and platform APIs.
new MoebaClient({ apiKey: 'mba_...' })
// Messaging
.send(connectionId, message) // send to connection
.sendToUser(phoneNumber, message) // send by phone number
.sendWithComponents(id, text, comps) // send with UI components
.progress(connectionId, text) // show progress indicator
// Email
.searchEmail(connId, opts) // search user's inbox
.readEmail(connId, opts) // read a specific email
.sendEmail(connId, opts) // send email on behalf of user
// Calendar
.getCalendar(connId, opts) // list calendar events
.createCalendarEvent(connId, opts) // create a calendar event
// Contacts
.getContacts(connId, opts) // search user's contacts
// Connections
.getConnections() // list all connections
// Cron
.createCronJob(connId, opts) // create scheduled task
.listCronJobs(connId) // list scheduled tasks
.updateCronJob(connId, id, opts) // update a cron job
.deleteCronJob(connId, id) // delete a cron job
// Storage
.kvSet(connId, key, value) // store a value
.kvGet(connId, key) // retrieve a value
.kvDelete(connId, key) // delete a key
.kvList(connId) // list all keys
// Memory
.addMemory(connId, text) // store a user memory
.listMemories(connId) // list memories
.deleteMemory(connId, id) // delete a memory
// History
.getHistory(connId, limit) // retrieve chat history
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}"}}
}