Bhairava · Custom App integration

Push data into Bhairava

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.

1

Declare your Custom App

Every integration starts with registering your app under Custom App settings — not with connecting to anything. You'll provide:

FieldMeaning
app_nameRequired. What your app is called.
descriptionWhat it does, for whoever approves it.
callback_urlOptional, 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.

2

Connect to the queue

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.

Hostmessaging.closebee.com
Port5677 (AMQP)
Vhost/
Usernameguest
Passwordguest
Publish toclosebee.transmission.push (default exchange, routing key = queue name)
Two headers are required on every message you publish:
license_keyThe connect key from step 1. Missing or invalid, and the message is silently dropped — see the callback note further down.
content-typeapplication/json or application/xml. This also decides which format your body must be in, and which format any callback comes back in.
Host confirmed against Bhairava's own connection config as of this writing. If it doesn't resolve for you, check with your Bhairava contact before assuming your integration is broken.
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()
3

Valid input

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.

ElementMeaning
entitiesThe thing whose state changed — a product, a resource, a fuel. Carries a direction (grew or decayed), a volume, and a value.
impactEntitiesWhat else felt that change, automatically — a warehouse, a tank, a second site. As many as the change actually causes.
managerWho ran the event. A sales ledger, a site contractor, a shift supervisor — singular, and doesn't have to carry a value.
actorsWhoever'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>
✓ accepted — callback (if registered) fires with status: success
4

Invalid input

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>
✕ rejected — reason_code: NO_ENTITIES

Success callback

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"/>
FieldMeaning
statusAlways "success" on this callback.
conEchoes your transmission's own con (epoch milliseconds) — use it to match this callback back to the submission you sent.
typeEchoes your transmission's own type.
received_atEpoch milliseconds when Bhairava finished processing, not when you sent it.

Failure callback

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"/>
FieldMeaning
statusAlways "failure" on this callback.
reason_codeA stable, machine-readable code — see the table below.
messageHuman-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_atEpoch milliseconds when Bhairava finished processing.

Reason codes

Every value reason_code can carry, in the order Bhairava checks them.

StageCodeMeaning
AuthorizationAPP_NOT_APPROVEDYour app's connect key no longer resolves to an approved, active Custom App.
ParsingTRANSMISSION_MALFORMEDThe body isn't valid XML/JSON, or is missing a field the schema requires outright (e.g. con, currency, manager, an entity's changeType).
ValidationNO_ENTITIESZero entities in an otherwise well-formed transmission.
ValidationENTITY_NAME_REQUIREDAn entity is present but has no name.
ValidationMANAGER_REQUIREDNo manager block at all.
ValidationMANAGER_NAME_REQUIREDA manager is present but has no name.
ValidationCON_INVALIDcon isn't a positive timestamp.
ValidationTYPE_REQUIREDtype is missing or empty.
ValidationCURRENCY_REQUIREDcurrency is missing or empty.
ValidationLAT_OUT_OF_RANGElat is outside −90…90.
ValidationLNG_OUT_OF_RANGElng is outside −180…180.
Two failure cases never reach your callback: a missing or invalid connect key. Bhairava can't know which app to notify until the key itself has been verified — those failures are logged on Bhairava's side only. Everything from 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.