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

Quickstart

Get a working agent in under 5 minutes with the TypeScript SDK.

1. Install the SDK

npm install moeba-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:

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}"}}
    }