marv.Inbox Developer API
Connect any CRM, point of sale, ERP, or in-house app to your team's WhatsApp and Messenger inbox. A REST API over OAuth2 for reading and writing data, plus signed webhooks that push events to you in real time. Every workspace can create its own credentials in Settings, Developer. No partner program and no sales call required.
Base URL: https://api.marvinbox.com
What you can build
Sync customers and conversations
Upsert contacts from your CRM, look them up by phone or your own id, and read conversations and message history.
Send messages and campaigns
Send a reply or an approved WhatsApp template into a conversation, or launch an outbound campaign to a contact list.
Create tasks and log events
Open tasks for your team and write CRM timeline events, such as an order placed or an invoice overdue, onto a contact.
Receive real-time webhooks
Subscribe to inbound messages, conversation changes, task updates, and campaign events. Every delivery is HMAC signed.
Base URL and access
The API is live in production today. You do not need an upgrade or a separate developer account.
- Base URL
https://api.marvinbox.com- Getting credentials
- Sign in to app.marvinbox.com and open Settings, Developer. Create an integration client to get a client id and client secret for the API, and register a webhook endpoint to receive events. A workspace admin role is required to manage both.
Quickstart
Three steps from zero to your first authenticated call.
Create an integration client
In Settings, Developer, Integration clients, create a client and pick the scopes it needs. Copy the client id and client secret. The secret is shown only once.
Exchange credentials for a token
Call the token endpoint with your client id and secret. You get back a short lived bearer token, valid for one hour.
Call the API
Send the token as an Authorization header on every request. The token already identifies your workspace, so there is no tenant header to set.
curl -X POST https://api.marvinbox.com/external/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "marv_ic_live_...",
"client_secret": "marv_cs_..."
}'Response
{
"access_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "customers:write messages:write"
}curl -X POST https://api.marvinbox.com/external/v1/customers/upsert \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <unique-key>" \
-d '{
"phone": "+972501234567",
"name": "Jane Doe",
"externalCustomerId": "crm-001"
}'Authentication
Authentication uses the OAuth2 client credentials grant. Post your client id and secret to the token endpoint and receive a bearer token, a signed JWT, valid for 3600 seconds.
Send that token as Authorization: Bearer <token> on every external/v1 request. The token is bound to the workspace that owns the client, so a token for one workspace can never read another, and you never send a tenant slug.
Scopes are checked live against the client on every call, so narrowing a client scope or rotating its secret takes effect immediately.
Scopes
Grant a client only the scopes it needs. Each endpoint below lists its required scope.
| Scope | Grants |
|---|---|
customers:read | Look up contacts |
customers:write | Create and update contacts |
conversations:read | List and read conversations |
conversations:write | Reserved for future use |
messages:read | Read message history |
messages:write | Send messages and templates |
events:write | Ingest CRM timeline events |
tasks:read | List and read tasks |
tasks:write | Create and update tasks |
campaigns:write | Create outbound campaigns |
REST endpoints
All endpoints are under the base URL above. Writes accept an Idempotency-Key header.
| Method | Endpoint | Scope |
|---|---|---|
| POST | /external/oauth/tokenExchange client credentials for an access token | none |
| POST | /external/v1/customers/upsertCreate or update a contact by phone or external id | customers:write |
| GET | /external/v1/customers/lookupFind a contact by phone or external id | customers:read |
| GET | /external/v1/contactsList contacts (org, chat-window flag, last message), by last activity | customers:read |
| POST | /external/v1/contacts/messagesRead a contact’s messages grouped per phone | messages:read |
| GET | /external/v1/templatesList approved WhatsApp templates (with template id) | messages:read |
| POST | /external/v1/messages/sendSend by phone: a text reply or an approved template | messages:write |
| POST | /external/v1/eventsLog a CRM timeline event (alias /interactions) | events:write |
| POST | /external/v1/outbound/campaignsCreate an outbound campaign | campaigns:write |
| POST | /external/v1/tasksCreate a task | tasks:write |
| GET | /external/v1/tasksList tasks | tasks:read |
| PATCH | /external/v1/tasks/:idUpdate a task | tasks:write |
Idempotency
Send an Idempotency-Key header on any write. If you retry with the same key, marv.Inbox returns the stored result of the first call instead of performing the action twice. Use a fresh, unique key per logical operation.
Rate limits and IP allowlist
Each client is limited to 120 requests per minute by default. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining, and a 429 is returned when the limit is exceeded. You can also restrict a client to a list of allowed IP ranges (CIDR) when you create it.
Webhooks
Webhooks push events to your server the moment they happen, so you do not have to poll.
Register an endpoint
In Settings, Developer, Webhooks, add your receiver URL and choose the events you want. Copy the signing secret shown on creation. You can register up to ten endpoints, enable or disable each one, and review recent delivery attempts in the same screen.
How deliveries arrive
Each event is a POST with a JSON body and these headers:
POST /your-receiver HTTP/1.1
Content-Type: application/json
X-Marv-Event: message.created
X-Marv-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Marv-Timestamp: 2026-06-15T10:23:45.123Z
X-Marv-Signature: v1=3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d...A 2xx response marks the delivery successful. marv.Inbox waits up to 10 seconds for your response and retries once after 5 seconds on failure. Receiver URLs must be public; private and loopback addresses are rejected.
Event types
Subscribe to any combination of these events.
| Event | Sent when |
|---|---|
message.created | A message was sent or received |
conversation.updated | Conversation metadata changed |
conversation.assigned | Conversation assignment changed |
conversation.closed | A conversation was closed |
conversation.reopened | A closed conversation was reopened |
Verifying a delivery
Every delivery is signed so you can trust it came from marv.Inbox. The signature is computed over the raw request body:
v1=HMAC-SHA256(secret, "{X-Marv-Timestamp}.{X-Marv-Delivery}.{rawBody}")Recompute it against the exact bytes you received, before JSON parsing, reject anything where the timestamp is more than five minutes old, dedupe by X-Marv-Delivery, and compare in constant time.
import { createHmac, timingSafeEqual } from 'crypto';
// marv.Inbox signs every delivery:
// v1=HMAC-SHA256(secret, `${timestamp}.${deliveryId}.${rawBody}`)
// Verify against the EXACT raw request body, before JSON parsing.
export function verifyMarvWebhook(headers, rawBody, secret) {
const signature = headers['x-marv-signature']; // "v1=<hex>"
const timestamp = headers['x-marv-timestamp']; // ISO 8601
const deliveryId = headers['x-marv-delivery']; // UUID, dedupe on this
// Reject anything older than 5 minutes (replay protection).
if (Math.abs(Date.now() - Date.parse(timestamp)) > 300_000) return false;
const expected = 'v1=' + createHmac('sha256', secret)
.update(`${timestamp}.${deliveryId}.${rawBody}`)
.digest('hex');
const a = Buffer.from(signature ?? '', 'utf8');
const b = Buffer.from(expected, 'utf8');
return a.length === b.length && timingSafeEqual(a, b);
}Errors
Errors use standard HTTP status codes with a JSON body describing the problem.
| Code | Meaning |
|---|---|
400 | Invalid request body or a missing required field |
401 | Missing, invalid, or expired access token |
403 | Token lacks the required scope, or the caller IP is not allowlisted |
404 | Unknown workspace or resource |
409 | Conflict, for example a colliding externalCustomerId |
429 | Rate limit exceeded (see X-RateLimit-Remaining) |
Frequently asked questions
Do I need a partner program or a sales call to use the API?+
No. Any workspace can create integration clients and webhook endpoints in Settings, Developer. There is no application process and the API is not gated to a higher plan.
How do I connect a CRM or help desk to marv.Inbox?+
Upsert your contacts with customers/upsert, log activity with events, open follow ups with tasks, and send replies or approved templates with messages/send. To receive inbound messages and updates in your own system, register a webhook endpoint and subscribe to the events you care about.
How do I verify that a webhook really came from marv.Inbox?+
Recompute the HMAC-SHA256 signature over "{timestamp}.{deliveryId}.{rawBody}" with your endpoint secret and compare it to the X-Marv-Signature header in constant time. Reject deliveries whose X-Marv-Timestamp is more than five minutes old and dedupe on X-Marv-Delivery.
What is the difference between an integration client and a webhook endpoint?+
An integration client is the credential your code uses to call the API: outbound, from you to marv.Inbox. A webhook endpoint is a URL marv.Inbox calls when something happens: inbound, from marv.Inbox to you. Most integrations use both.
How do I rotate or revoke access?+
Rotate an integration client secret or revoke the client entirely in Settings, Developer. Rotation invalidates the old secret immediately. For webhooks, disable or delete the endpoint to stop deliveries.
Does sending messages respect the WhatsApp 24 hour window?+
Yes. A free form WhatsApp message is only allowed within 24 hours of the customer last message. Outside that window, send an approved template instead. The API enforces this for you.
Start building
Create your first integration client and webhook endpoint in the app, or tell us what you are connecting and we will help you scope it.