Webhooks
Ozzie exposes a webhook endpoint that receives inbound WhatsApp messages routed through Evolution API. When a user sends a message to your WhatsApp instance, Evolution API forwards it to this endpoint, Ozzie processes it, and automatically replies to the user in their language.
This is the foundation for building a WhatsApp-first financial coach without writing any message routing logic yourself.
How it worksβ
User (WhatsApp) β Evolution API β POST /api/webhooks/evolution β Ozzie processes + replies
β
User receives reply on WhatsApp
- A user sends a WhatsApp message to your Evolution API instance
- Evolution API forwards the message payload to your configured webhook URL
- Ozzie verifies the request signature, identifies the user by their phone number, and processes the message
- Ozzie sends an automatic reply back through Evolution API in the user's configured language
POST /api/webhooks/evolutionβ
This endpoint is called by Evolution API, not by your server. Your job is to configure Evolution API to point at it. Ozzie handles everything else.
Note the path: /api/webhooks/evolution β not /v1/.... This endpoint is outside the versioned REST namespace because it conforms to Evolution API's webhook contract, not Ozzie's standard request format.
Setup guideβ
Step 1: Configure Evolution APIβ
In your Evolution API dashboard or configuration, set the webhook URL for your instance to:
https://your-domain.com/api/webhooks/evolution
Replace your-domain.com with the domain where your Ozzie-integrated backend is hosted.
Evolution API will only forward webhook events to HTTPS endpoints. Local development requires a tunnel (e.g., ngrok, Cloudflare Tunnel, or similar).
Step 2: Set the webhook secretβ
In your environment configuration, set:
EVOLUTION_WEBHOOK_SECRET=your_shared_secret_here
This must be the same value you configure in Evolution API as the apikey or webhook secret for your instance. Ozzie uses this to verify that incoming requests are genuinely from Evolution API and not spoofed.
Step 3: Register your users with their phone numbersβ
For Ozzie to route an inbound WhatsApp message to the right user, the sender's phone number must match a user's phone_e164 field in Ozzie.
When creating users who will interact via WhatsApp, include their phone number in E.164 format:
curl -X POST https://api.ozzieapp.com/v1/users \
-H "Authorization: Bearer ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "user_789",
"name": "Sofia Mendes",
"language": "pt",
"phone_e164": "+5511987654321"
}'
If a message arrives from a phone number that doesn't match any registered user, Ozzie will return a 404 to Evolution API and no reply will be sent.
Supported message typesβ
Evolution API messageType | What the user sends | How Ozzie processes it |
|---|---|---|
conversation | A plain text message | Parsed as a chat message or transaction log |
imageMessage | A photo (e.g., a receipt) | Processed by Ozzie's image parser to extract transaction data |
PDF and spreadsheet parsing is available via the REST transactions endpoint but is not currently supported through the WhatsApp webhook. For document uploads, direct your users to a web interface that calls POST /transactions directly.
Webhook payload formatβ
This is the payload Evolution API sends to your endpoint. Ozzie parses this format automatically β you do not need to transform it.
{
"event": "messages.upsert",
"instance": "your_instance_name",
"data": {
"key": {
"remoteJid": "5511987654321@s.whatsapp.net",
"fromMe": false,
"id": "3EB0A1B2C3D4E5F6A7B8"
},
"messageType": "conversation",
"message": {
"conversation": "spent $45 on groceries at Whole Foods"
}
}
}
Image message payloadβ
{
"event": "messages.upsert",
"instance": "your_instance_name",
"data": {
"key": {
"remoteJid": "5511987654321@s.whatsapp.net",
"fromMe": false,
"id": "3EB0A1B2C3D4E5F6FFFF"
},
"messageType": "imageMessage",
"message": {
"imageMessage": {
"caption": "my grocery receipt",
"mimetype": "image/jpeg",
"url": "https://media.evolution.example.com/media/abc123.jpg"
}
}
}
}
Key fieldsβ
| Field | Description |
|---|---|
data.key.remoteJid | The sender's WhatsApp ID. Ozzie strips the @s.whatsapp.net suffix to get the phone number. |
data.key.fromMe | If true, this message was sent by your instance (outbound). Ozzie ignores fromMe: true messages. |
data.messageType | Determines how Ozzie processes the message body. |
data.message.conversation | The text content (for conversation type). |
Securityβ
Ozzie verifies every inbound webhook request by checking the apikey header against the value stored in your EVOLUTION_WEBHOOK_SECRET environment variable.
POST /api/webhooks/evolution
apikey: your_shared_secret_here
Content-Type: application/json
If the header is missing or the value does not match, Ozzie returns 401 Unauthorized and discards the message.
Security best practicesβ
Use a strong, random secret. Generate at least 32 bytes of random data:
openssl rand -hex 32
# e.g.: a7f3c8e1b2d4e5f60718293a4b5c6d7e8f901234567890abcdef01234567890
Rotate secrets periodically. Update EVOLUTION_WEBHOOK_SECRET and your Evolution API configuration at the same time during a low-traffic window to avoid dropped messages during the transition.
Use HTTPS only. Never expose the webhook endpoint over plain HTTP in production. All webhook traffic should be encrypted in transit.
Restrict inbound traffic by IP (optional). If Evolution API has a fixed egress IP range for your instance, configure your firewall or reverse proxy to only allow requests from those IPs.
Ozzie's automatic repliesβ
When Ozzie successfully processes an inbound message, it sends a reply back through Evolution API automatically. The reply is generated by the same AI coach as POST /chat/messages and is returned in the user's configured language.
Example: text message in β coach reply out
User sends (WhatsApp):
I just paid my electricity bill β $120
Ozzie replies (WhatsApp):
Got it! I've logged $120 for Electricity under Utilities π‘ Your utilities total for May is now $195, which is within your budget. You're doing great this month β keep it up!
Example: receipt photo in β parsed transaction out
User sends a photo of a grocery receipt.
Ozzie replies:
I parsed your receipt from Trader Joe's: $87.40 logged under Groceries for May 5th. Your grocery total for May is now $302. You have about $98 left in your food budget for the month.
Handling unknown usersβ
If a WhatsApp message arrives from a phone number that does not match any registered user:
- Ozzie returns
404to Evolution API - No reply is sent to the user
- The event is logged for debugging
To onboard users via WhatsApp, you need a separate registration flow. See the WhatsApp Bot use case guide for a complete implementation that handles first-time messages and triggers user registration.
Errorsβ
| HTTP Status | When it occurs |
|---|---|
200 OK | Message processed successfully β Ozzie has sent or queued a reply |
401 Unauthorized | The apikey header is missing or does not match EVOLUTION_WEBHOOK_SECRET |
404 Not Found | No Ozzie user found with a matching phone_e164 |
422 Unprocessable Entity | The payload is malformed or the messageType is not supported |
500 Internal Server Error | Processing failed β Evolution API will retry depending on its configuration |
Evolution API typically retries failed webhook deliveries. Make sure your endpoint is idempotent β processing the same message twice should not create duplicate transactions. Ozzie deduplicates by the Evolution API message id field.