How to push data into Bhairava from your own software: declare a Custom App, connect to the queue, and send transmissions — the one contract this pipeline accepts, whatever your data actually is.
Every integration starts with registering your app under Custom App settings — not with connecting to anything. You'll provide:
| Field | Meaning |
|---|---|
app_name | Required. What your app is called. |
description | What it does, for whoever approves it. |
callback_url | Optional, but this is what every notification on this page depends on — without it, you can still push data, you just won't hear back whether it was accepted. |
Once approved and linked to a tenant, you're issued a connect key — a signed token that identifies your app on every message you send. That's the license_key the next step refers to.
The injection queue takes no real per-connection credential — every client connects the same way. Your actual identity is carried in the license_key header on each individual message, not the connection itself.
| Host | messaging.closebee.com |
| Port | 5677 (AMQP) |
| Vhost | / |
| Username | guest |
| Password | guest |
| Publish to | closebee.transmission.push (default exchange, routing key = queue name) |
license_key | The connect key from step 1. Missing or invalid, and the message is silently dropped — see the callback note further down. |
content-type | application/json or application/xml. This also decides which format your body must be in, and which format any callback comes back in. |
ConnectionFactory factory = new ConnectionFactory(); factory.setHost("messaging.closebee.com"); factory.setPort(5677); factory.setVirtualHost("/"); factory.setUsername("guest"); factory.setPassword("guest"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); String payload = "{\"transmission\":{\"@con\":\"1789826400000\",\"@type\":\"Consumption\",\"@currency\":\"USD\",\"entities\":[...],\"manager\":{...}}}"; Map<String, Object> headers = new HashMap<>(); headers.put("license_key", YOUR_CONNECT_KEY); AMQP.BasicProperties props = new AMQP.BasicProperties.Builder() .contentType("application/json") .headers(headers) .build(); channel.basicPublish("", "closebee.transmission.push", props, payload.getBytes("UTF-8")); channel.close(); connection.close();
$connection = new AMQPStreamConnection(
'messaging.closebee.com', 5677, 'guest', 'guest', '/'
);
$channel = $connection->channel();
$payload = json_encode([
'transmission' => [
'@con' => '1789826400000',
'@type' => 'Consumption',
'@currency' => 'USD',
'entities' => [ ... ],
'manager' => [ ... ]
]
]);
$message = new AMQPMessage($payload, [
'content_type' => 'application/json',
'application_headers' => new AMQPTable(['license_key' => YOUR_CONNECT_KEY]),
]);
$channel->basic_publish($message, '', 'closebee.transmission.push');
$channel->close();
$connection->close();const amqp = require('amqplib'); const payload = { transmission: { '@con': '1789826400000', '@type': 'Consumption', '@currency': 'USD', entities: [ ... ], manager: { ... } } }; const conn = await amqp.connect(`amqp://guest:guest@messaging.closebee.com:5677/`); const channel = await conn.createChannel(); channel.publish( '', 'closebee.transmission.push', Buffer.from(JSON.stringify(payload)), { contentType: 'application/json', headers: { license_key: YOUR_CONNECT_KEY } } ); await channel.close(); await conn.close();
import pika
import json
connection = pika.BlockingConnection(pika.ConnectionParameters(
host="messaging.closebee.com",
port=5677,
virtual_host="/",
credentials=pika.PlainCredentials("guest", "guest"),
))
channel = connection.channel()
payload = {
"transmission": {
"@con": "1789826400000",
"@type": "Consumption",
"@currency": "USD",
"entities": [ ... ],
"manager": { ... }
}
}
channel.basic_publish(
exchange="",
routing_key="closebee.transmission.push",
body=json.dumps(payload),
properties=pika.BasicProperties(
content_type="application/json",
headers={"license_key": YOUR_CONNECT_KEY},
),
)
connection.close()A complete, accepted transmission — power drawn from a diesel generator, the same worked example used throughout Bhairava's docs.
Four content elements, nothing else. Every field below is optional except a node's own name — a transmission uses only the nodes a given event actually has.
| Element | Meaning |
|---|---|
entities | The thing whose state changed — a product, a resource, a fuel. Carries a direction (grew or decayed), a volume, and a value. |
impactEntities | What else felt that change, automatically — a warehouse, a tank, a second site. As many as the change actually causes. |
manager | Who ran the event. A sales ledger, a site contractor, a shift supervisor — singular, and doesn't have to carry a value. |
actors | Whoever's own balance moved because of it — a customer, a donor, a tax authority. Including none, when the event is purely internal. |
{
"transmission": {
"@con": "1789826400000",
"@type": "Consumption",
"@currency": "USD",
"@lat": "-77.8500",
"@lng": "166.6700",
"@name": "Site B — McMurdo Sound Research Station",
"entities": [
{
"name": "DIESEL",
"parent": "FUEL",
"changeType": "DECAY",
"changeVolume": 50,
"changeAmount": 4500,
"impactEntities": [
{ "name": "Site B Fuel Tank", "changeType": "DECAY", "changeVolume": 50, "changeAmount": 4500 }
]
}
],
"manager": {
"@type": "org", "@subtype": "manager",
"name": "Site Ops Contractor", "changeType": "GROWTH", "changeAmount": 4000
},
"actors": [
{ "@type": "org", "@subtype": "consumer", "name": "Remote Observatory", "changeType": "DECAY", "changeAmount": 4500 }
]
}
}<TRANSMISSION con="1789826400000" type="Consumption" currency="USD" lat="-77.8500" lng="166.6700" name="Site B — McMurdo Sound Research Station">
<TRASMISSIONENTITIES>
<ENTITY>
<NAME>DIESEL</NAME>
<PARENT>FUEL</PARENT>
<CHANGETYPE>DECAY</CHANGETYPE>
<CHANGEVOLUME>50</CHANGEVOLUME>
<CHANGEAMOUNT>4500</CHANGEAMOUNT>
<IMPACTENTITIES>
<IMPACTENTITY><NAME>Site B Fuel Tank</NAME><CHANGETYPE>DECAY</CHANGETYPE><CHANGEVOLUME>50</CHANGEVOLUME><CHANGEAMOUNT>4500</CHANGEAMOUNT></IMPACTENTITY>
</IMPACTENTITIES>
</ENTITY>
</TRASMISSIONENTITIES>
<ENTITYMANAGER>
<ACTOR type="org" subtype="manager"><NAME>Site Ops Contractor</NAME><CHANGETYPE>GROWTH</CHANGETYPE><CHANGEAMOUNT>4000</CHANGEAMOUNT></ACTOR>
</ENTITYMANAGER>
<TRASMISSIONACTORS>
<ACTOR type="org" subtype="consumer"><NAME>Remote Observatory</NAME><CHANGETYPE>DECAY</CHANGETYPE><CHANGEAMOUNT>4500</CHANGEAMOUNT></ACTOR>
</TRASMISSIONACTORS>
</TRANSMISSION>Same shape, but no entities — nothing whose state actually changed. Rejected before it ever reaches Bhairava's data.
{
"transmission": {
"@con": "1789826400000",
"@type": "Consumption",
"@currency": "USD",
"entities": [],
"manager": {
"name": "Site Ops Contractor", "changeType": "GROWTH", "changeAmount": 4000
}
}
}<TRANSMISSION con="1789826400000" type="Consumption" currency="USD">
<TRASMISSIONENTITIES></TRASMISSIONENTITIES>
<ENTITYMANAGER>
<ACTOR type="org" subtype="manager"><NAME>Site Ops Contractor</NAME><CHANGETYPE>GROWTH</CHANGETYPE><CHANGEAMOUNT>4000</CHANGEAMOUNT></ACTOR>
</ENTITYMANAGER>
</TRANSMISSION>Sent once your transmission has passed every check.
✓ status: success{
"status": "success",
"con": 1789826400000,
"type": "Consumption",
"received_at": 1758271234567
}<CALLBACK status="success" con="1789826400000" type="Consumption" received_at="1758271234567"/>
| Field | Meaning |
|---|---|
status | Always "success" on this callback. |
con | Echoes your transmission's own con (epoch milliseconds) — use it to match this callback back to the submission you sent. |
type | Echoes your transmission's own type. |
received_at | Epoch milliseconds when Bhairava finished processing, not when you sent it. |
Sent when your transmission is rejected, with a machine-readable reason.
✕ status: failure{
"status": "failure",
"reason_code": "NO_ENTITIES",
"message": "[NO_ENTITIES [entities] at least one ENTITY is required]",
"received_at": 1758271234567
}<CALLBACK status="failure" reason_code="NO_ENTITIES" message="[NO_ENTITIES [entities] at least one ENTITY is required]" received_at="1758271234567"/>
| Field | Meaning |
|---|---|
status | Always "failure" on this callback. |
reason_code | A stable, machine-readable code — see the table below. |
message | Human-readable detail. For a validation failure, this may list more than one problem at once — don't parse it, use reason_code for logic. |
received_at | Epoch milliseconds when Bhairava finished processing. |
Every value reason_code can carry, in the order Bhairava checks them.
| Stage | Code | Meaning |
|---|---|---|
| Authorization | APP_NOT_APPROVED | Your app's connect key no longer resolves to an approved, active Custom App. |
| Parsing | TRANSMISSION_MALFORMED | The body isn't valid XML/JSON, or is missing a field the schema requires outright (e.g. con, currency, manager, an entity's changeType). |
| Validation | NO_ENTITIES | Zero entities in an otherwise well-formed transmission. |
| Validation | ENTITY_NAME_REQUIRED | An entity is present but has no name. |
| Validation | MANAGER_REQUIRED | No manager block at all. |
| Validation | MANAGER_NAME_REQUIRED | A manager is present but has no name. |
| Validation | CON_INVALID | con isn't a positive timestamp. |
| Validation | TYPE_REQUIRED | type is missing or empty. |
| Validation | CURRENCY_REQUIRED | currency is missing or empty. |
| Validation | LAT_OUT_OF_RANGE | lat is outside −90…90. |
| Validation | LNG_OUT_OF_RANGE | lng is outside −180…180. |
APP_NOT_APPROVED onward, Bhairava already knows who you are, so you'll hear about it.Registered your callback URL through Custom App settings and not seeing anything? Confirm the URL accepts POST with a JSON or XML body and returns a 2xx status — Bhairava doesn't retry, so a non-2xx or timed-out response is logged as a delivery failure on Bhairava's side and nothing more is attempted for that submission.