Your own WhatsApp API Gateway, self-hosted, one command.
Your own WhatsApp infrastructure — OTPs, Transactional Alerts, and Webhooks. Includes API, beautiful Dashboard, and SDKs in Go, TypeScript, and Python.
Caution
Wotp relies on whatsmeow, an unofficial library that reverse-engineers the WhatsApp Web protocol. This is not the official Meta Cloud API. This means:
- Risk of phone number ban — Meta may ban your number without warning if usage patterns are deemed abnormal. A ban permanently cuts the WhatsApp channel.
- No SLA, no official support — If WhatsApp changes its protocol, Wotp may break until the community (whatsmeow or Wotp) ships a fix.
- Recommended for: MVPs, side-projects, products in validation phase with moderate user volume.
- Not recommended for: Production at scale with zero downtime tolerance — in that case, use the official Meta Cloud API once volume/budget justifies it.
This transparency is intentional. It builds more trust than silence and prevents bad surprises for early adopters.
npm install -g wotp-cli
wotp init my-project
cd my-project
wotp start
# Scan the QR code with WhatsApp → Done.┌─────────────────────────────────────────────────────────────┐
│ wotp-core (single Go binary) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ whatsmeow │ │ OTP engine │ │ REST API (Chi) │ │
│ │ WA client │ │ gen / verify│ │ + WebSocket hub │ │
│ └──────────────┘ └──────────────┘ └───────────────────┘ │
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
│ │ SQLite │ │ Dashboard (static, embed.FS) │ │
│ │ storage │ │ served on /dashboard │ │
│ └──────────────┘ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲
│ HTTP + WebSocket, port 54321
▼
Browser / SDK client
One binary. One image. One port. One process.
All endpoints require an apikey header with your anon or service key.
Send an OTP to a phone number. The code is generated internally — the client only receives an opaque token.
curl -X POST http://localhost:54321/v1/otp/send \
-H "Content-Type: application/json" \
-H "apikey: wotp_anon_xxx" \
-d '{"phone": "+212600000000"}'{
"token": "otp_tok_9f8e7d6c",
"expires_at": "2026-07-13T10:35:00Z"
}Verify an OTP code against a token.
curl -X POST http://localhost:54321/v1/otp/verify \
-H "Content-Type: application/json" \
-H "apikey: wotp_anon_xxx" \
-d '{"token": "otp_tok_9f8e7d6c", "code": "483920"}'Success:
{ "verified": true, "phone": "+212600000000" }Failure:
{ "verified": false, "error": "invalid_code", "attempts_remaining": 3 }Send a generic transactional message (text or media) to a phone number.
curl -X POST http://localhost:54321/v1/messages/send \
-H "Content-Type: application/json" \
-H "apikey: wotp_anon_xxx" \
-d '{"phone": "+212600000000", "type": "text", "text": "Your order #1234 has shipped!"}'{
"message_id": "3EB00436D840039D465DC3"
}Wotp supports two-way communication. You can configure a Webhook endpoint in your config.toml to instantly receive HTTP POST payloads when:
message.received: A user replies to your WhatsApp number (Perfect for connecting AI Support Agents like n8n/Make).message.sent,message.delivered,message.read: Status updates for your OTPs and outbound messages.session.disconnected: If your phone loses connection.
Check instance health and WhatsApp connection status.
{
"status": "connected",
"phone": "+212600000000",
"uptime_seconds": 123456
}| HTTP Code | Error | Description |
|---|---|---|
400 |
invalid_code |
OTP code is incorrect |
400 / 410 |
expired_token |
Token has expired |
429 |
— | Rate limit exceeded |
npm install wotp-clientimport { createClient, RateLimitError, InvalidCodeError } from 'wotp-client'
const wotp = createClient('http://localhost:54321', 'wotp_anon_xxx')
// Send OTP
const { token, expiresAt } = await wotp.sendOTP('+212600000000')
// Verify
try {
const { verified } = await wotp.verifyOTP(token, '483920')
console.log(verified ? '✅ Verified!' : '❌ Failed')
} catch (err) {
if (err instanceof InvalidCodeError) {
console.log(`Wrong code. ${err.attemptsRemaining} attempts left`)
}
}go get github.com/wotp/wotp-goimport wotp "github.com/wotp/wotp-go"
client := wotp.NewClient("http://localhost:54321", wotp.WithApiKey("wotp_anon_xxx"))
resp, err := client.SendOTP(ctx, "+212600000000")
if err != nil {
log.Fatal(err)
}
result, err := client.VerifyOTP(ctx, resp.Token, "483920")
if wotp.IsInvalidCodeError(err) {
fmt.Println("Wrong code")
}pip install wotpfrom wotp import create_client, InvalidCodeError
client = create_client("http://localhost:54321", "wotp_anon_xxx")
resp = client.send_otp("+212600000000")
try:
result = client.verify_otp(resp.token, "483920")
print("✅ Verified!" if result.verified else "❌ Failed")
except InvalidCodeError as e:
print(f"Wrong code. {e.attempts_remaining} attempts left")All configuration lives in wotp/config.toml:
[project]
name = "my-project"
[api]
port = 54321
enable_dashboard = true
[otp]
code_length = 6
expiry_minutes = 5
max_attempts = 5
rate_limit_per_phone_per_hour = 3
[whatsapp]
device_name = "Wotp - my-project"
reconnect_backoff_seconds = [5, 15, 60, 300]
[storage]
driver = "sqlite" # "sqlite" | "postgres"
[templates]
default_locale = "en"Message templates in wotp/seed/templates.toml:
[en]
otp_message = "Your verification code: {{code}}. Valid for {{expiry}} minutes."
[fr]
otp_message = "Votre code de vérification : {{code}}. Valable {{expiry}} minutes."
[magic_link]
# You can use templates to send Magic Links instead of raw codes!
otp_message = "Click here to securely login: https://your-app.com/verify?code={{code}}"wotp/
├── core/ # Go API server + dashboard
│ └── Dockerfile
├── sdks/
│ ├── typescript/ # wotp-client (npm)
│ ├── go/ # wotp-go (Go module)
│ └── python/ # wotp (PyPI)
├── .github/workflows/ # CI/CD
├── LICENSE
└── README.md
- Two-tier API keys:
anon(send/verify only, rate-limited) andservice(admin ops) - Rate limiting per IP and per phone number, enforced at the API level
- OTP codes are never stored in plain text — hashed, never logged, never returned
- Dashboard local-only by default — explicit config required to expose publicly
.envis gitignored with warnings never to commit it
| Version | What's coming |
|---|---|
| v1.0 | Single-number instance, dashboard, 3 SDKs, CLI |
| v1.1 | Dashboard authentication when exposed publicly |
| v1.2 | Full Postgres support alongside SQLite |
| v1.3 | Multi-language templates editable from dashboard |
| v2.0 | Multi-number per instance (failover/round-robin) |
| v2.x | Optional SMS fallback |
MIT — Use it, fork it, ship it.
Built by raucheacho · Star ⭐ if this saves you from a BSP invoice