Maranduva API
Maranduva is a real-time messaging platform built around a simple model: publish an event to a channel via HTTP, and any connected WebSocket client subscribed to that channel receives it instantly.
Two endpoints, that's it. A POST /push to publish, and a WebSocket URL to subscribe. Messages are delivered in real time.
Here's the full flow:
- Your server (or any HTTP client) publishes an event to
POST /pushwith an API key. - Maranduva broadcasts the event to every WebSocket client connected to that channel.
- Clients receive the message in real time, typically in under 10 ms.
Authentication
All HTTP requests must include your API key in the x-api-key header. Contact us to get your API key.
x-api-key: YOUR_API_KEY
Keep your key secret. Never expose your API key in client-side or public code. It is intended for server-side use only.
Publish Event
Creates and publishes an event to the configured broker. All subscribers connected to the specified channel will receive the message in real time.
Request body
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | UUID | Optional | Unique event identifier. Auto-generated if not provided. |
| channel | string | Required | Channel where the message will be published. Subscribers on this channel receive the event. |
| message | string | Required | Message payload to be sent to all subscribers. |
| app | string | Required | Application identifier. Scopes the channel to your app. |
Example request
curl -X POST 'https://events.maranduva.com/push' \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"channel": "orders",
"message": "order_created",
"app": "my-app"
}'
const response = await fetch('https://events.maranduva.com/push', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': 'YOUR_API_KEY',
},
body: JSON.stringify({
channel: 'orders',
message: 'order_created',
app: 'my-app',
}),
});
const data = await response.json();
console.log(data);
import requests
response = requests.post(
'https://events.maranduva.com/push',
headers={
'content-type': 'application/json',
'x-api-key': 'YOUR_API_KEY',
},
json={
'channel': 'orders',
'message': 'order_created',
'app': 'my-app',
},
)
print(response.json())
<?php
$ch = curl_init('https://events.maranduva.com/push');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: YOUR_API_KEY',
],
CURLOPT_POSTFIELDS => json_encode([
'channel' => 'orders',
'message' => 'order_created',
'app' => 'my-app',
]),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"channel": "orders",
"message": "order_created",
"app": "my-app",
})
req, _ := http.NewRequest("POST", "https://events.maranduva.com/push", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", "YOUR_API_KEY")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
}
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpRequest.BodyPublishers;
var body = """
{"channel":"orders","message":"order_created","app":"my-app"}
""";
var request = HttpRequest.newBuilder()
.uri(URI.create("https://events.maranduva.com/push"))
.header("Content-Type", "application/json")
.header("x-api-key", "YOUR_API_KEY")
.POST(BodyPublishers.ofString(body))
.build();
var response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
require 'net/http'
require 'json'
uri = URI('https://events.maranduva.com/push')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request['x-api-key'] = 'YOUR_API_KEY'
request.body = {
channel: 'orders',
message: 'order_created',
app: 'my-app'
}.to_json
response = http.request(request)
puts response.body
using System.Net.Http;
using System.Text;
using System.Text.Json;
var payload = JsonSerializer.Serialize(new {
channel = "orders",
message = "order_created",
app = "my-app"
});
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key", "YOUR_API_KEY");
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://events.maranduva.com/push", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"channel": "orders",
"message": "order_created",
"app": "my-app"
}
If id is omitted, a UUID v4 is automatically generated and returned in the response.
Subscribe via WebSocket
Clients subscribe to one or more channels by opening a WebSocket connection. Any event published to those channels is delivered instantly over the open connection. Channels can also be added or removed dynamically after the connection is open — see Dynamic subscribe & unsubscribe below.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| app | string | Required | Your application identifier. |
| channel | string | Required | Channel ID to subscribe to. Repeat the parameter to subscribe to multiple channels. E.g. ?app=my-app&channel=orders&channel=notifications |
Example connection
wscat -c "wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications"
const socket = new WebSocket(
'wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications'
)
socket.addEventListener('open', () => {
console.log('Connected to Maranduva')
})
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data)
console.log('Received:', data)
})
socket.addEventListener('close', () => {
console.log('Disconnected')
})
// npm install ws
import WebSocket from 'ws'
const socket = new WebSocket(
'wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications'
)
socket.on('open', () => {
console.log('Connected to Maranduva')
})
socket.on('message', (data) => {
console.log('Received:', JSON.parse(data))
})
socket.on('close', () => {
console.log('Disconnected')
})
# pip install websocket-client
import json
import websocket
def on_open(ws):
print('Connected to Maranduva')
def on_message(ws, message):
data = json.loads(message)
print('Received:', data)
def on_close(ws, code, msg):
print('Disconnected')
ws = websocket.WebSocketApp(
'wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications',
on_open=on_open,
on_message=on_message,
on_close=on_close,
)
ws.run_forever()
<?php
// composer require textalk/websocket
use WebSocket\Client;
$client = new Client(
'wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications'
);
try {
while (true) {
$message = $client->receive();
$data = json_decode($message, true);
echo 'Received: ' . print_r($data, true) . PHP_EOL;
}
} finally {
$client->close();
}
// go get github.com/gorilla/websocket
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/gorilla/websocket"
)
func main() {
url := "wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications"
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
fmt.Println("Connected to Maranduva")
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("Disconnected:", err)
return
}
var data map[string]interface{}
json.Unmarshal(msg, &data)
fmt.Println("Received:", data)
}
}
// Java 11+ — jakarta.websocket or tyrus client
import jakarta.websocket.*;
import java.net.URI;
@ClientEndpoint
public class MaranduvaClient {
@OnOpen
public void onOpen(Session session) {
System.out.println("Connected to Maranduva");
}
@OnMessage
public void onMessage(String message) {
System.out.println("Received: " + message);
}
@OnClose
public void onClose(Session session) {
System.out.println("Disconnected");
}
public static void main(String[] args) throws Exception {
var url = "wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications";
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
container.connectToServer(MaranduvaClient.class, URI.create(url));
Thread.currentThread().join(); // keep alive
}
}
# gem install faye-websocket
require 'faye/websocket'
require 'eventmachine'
require 'json'
EM.run do
ws = Faye::WebSocket::Client.new(
'wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications'
)
ws.on :open do
puts 'Connected to Maranduva'
end
ws.on :message do |event|
data = JSON.parse(event.data)
puts "Received: #{data}"
end
ws.on :close do
puts 'Disconnected'
EM.stop
end
end
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
var uri = new Uri("wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications");
var buffer = new byte[4096];
using var ws = new ClientWebSocket();
await ws.ConnectAsync(uri, CancellationToken.None);
Console.WriteLine("Connected to Maranduva");
while (ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(buffer, CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close) break;
var json = Encoding.UTF8.GetString(buffer, 0, result.Count);
var data = JsonSerializer.Deserialize<object>(json);
Console.WriteLine($"Received: {data}");
}
Console.WriteLine("Disconnected");
// build.gradle: implementation("com.squareup.okhttp3:okhttp:4.12.0")
import okhttp3.*
import okio.ByteString
val client = OkHttpClient()
val request = Request.Builder()
.url("wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications")
.build()
val listener = object : WebSocketListener() {
override fun onOpen(ws: WebSocket, response: Response) {
println("Connected to Maranduva")
}
override fun onMessage(ws: WebSocket, text: String) {
println("Received: $text")
}
override fun onClosing(ws: WebSocket, code: Int, reason: String) {
ws.close(1000, null)
println("Disconnected")
}
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
t.printStackTrace()
}
}
client.newWebSocket(request, listener)
import Foundation
let url = URL(string: "wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications")!
let session = URLSession(configuration: .default)
let task = session.webSocketTask(with: url)
task.resume()
print("Connected to Maranduva")
func receiveMessage() {
task.receive { result in
switch result {
case .success(let message):
switch message {
case .string(let text):
print("Received:", text)
default:
break
}
receiveMessage() // keep listening
case .failure(let error):
print("Disconnected:", error)
}
}
}
receiveMessage()
#import <Foundation/Foundation.h>
NSURL *url = [NSURL URLWithString:
@"wss://channels.maranduva.com/?app=my-app&channel=orders&channel=notifications"];
NSURLSession *session = [NSURLSession sessionWithConfiguration:
[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionWebSocketTask *task =
[session webSocketTaskWithURL:url];
[task resume];
NSLog(@"Connected to Maranduva");
// Recursive receive block
__block void (^receive)(void);
receive = ^{
[task receiveMessageWithCompletionHandler:^(
NSURLSessionWebSocketMessage *msg, NSError *error) {
if (error) {
NSLog(@"Disconnected: %@", error);
return;
}
if (msg.type == NSURLSessionWebSocketMessageTypeString) {
NSLog(@"Received: %@", msg.string);
}
receive();
}];
};
receive();
Dynamic subscribe & unsubscribe
After connecting, clients can change which channels they're listening to without reconnecting. Send a JSON action message over the open WebSocket to add or remove channel subscriptions on the fly.
Subscribe
Send an action: "subscribe" message with one or more channels:
{
"action": "subscribe",
"channels": "orders:123"
}
{
"action": "subscribe",
"channels": ["orders:123", "payments:456"]
}
Limits: channel names must be 1–100 characters. A single request can subscribe to at most 50 channels, and a connection can hold at most 200 subscribed channels in total.
Unsubscribe
Send an action: "unsubscribe" message to stop receiving messages from one or more channels:
{
"action": "unsubscribe",
"channels": "orders:123"
}
{
"action": "unsubscribe",
"channels": ["orders:123", "payments:456"]
}
Unsubscribing from a channel that the connection is not currently subscribed to is a no-op and does not affect the connection.
Need help? Reach out at info@maranduva.com or visit the contact page.