OROVA.VN — BIZ AI AGENT

/ Developers guide

Receiving Orova Social posts by webhook.

How to build an endpoint that receives the posts Orova Social sends, and verify the signature before you act on them.

Orova Social publishes straight to the platforms it connects to. When a post has to reach somewhere else — an internal channel, your own app, a platform Orova does not support yet — you use the API channel: Orova sends the post to an HTTP address of yours, signed with a secret key, and you take it from there. This guide is for a developer and describes the full request contract.

/ Overview

How it works

You register a receiving address (https://) and a secret key on the API channel of your project. Orova sends a POST request there with a JSON body and an X-Orova-Signature header — an HMAC-SHA256 of the exact bytes of that body, keyed with your secret. Your endpoint verifies the signature, takes the post, and returns a 2xx status.

  • The receiving address must be https. Orova refuses to save the channel if the address is not https:// or has no host — it reports the code api_url.
  • One channel per address. Orova identifies the channel by the receiving address itself. Saving the same address again overwrites the existing channel instead of creating a duplicate.
  • There is a Send test button. Once connected, the channel has a Send test button: Orova fires an orova.test payload at your endpoint and shows you the HTTP status it got back along with the round trip in milliseconds.

/ Setup

Setting it up in Orova

Open SocialProjects → your project → the Channels tab, and find the API box. It has two fields and one button.

  • 1

    Receiving URL (webhook). The address of your endpoint, starting with https://. This is where every request goes.

  • 2

    Secret key. The string used to sign. Leave it blank and Orova generates a random one — but the screen never shows it back, so generate a long string yourself, paste it in, and keep your own copy.

  • 3

    Click Save channel. The channel appears in the list with its receiving address. The secret is never displayed again.

  • 4

    Click Send test. Orova sends a sample orova.test payload. If your endpoint answers 2xx, Orova reports “Webhook returned 200 in … ms”. Any other status is reported with that code; an unreachable endpoint is reported as a network error.

/ Contract

The request contract

Every send arrives as one request shaped like this:

POST <your receiving URL>

Headers:
  Content-Type: application/json
  X-Orova-Signature: sha256=<hex HMAC-SHA256 of the raw body>
  X-Orova-Event: orova.test
  User-Agent: Orova-Social/1.0

Body (JSON):
  {
    "event": "orova.test",
    "workspace_id": 12,
    "project_id": 34,
    "post": {
      "title": "...",
      "content": "...",
      "media": [],
      "channels": ["api"]
    },
    "sent_at": "2026-08-23T01:25:00Z"
  }

The body is compact UTF-8 JSON on a single line. Read event to tell the kinds apart — the Send test payload carries orova.test, and the X-Orova-Event header repeats the same value. Your endpoint should ignore unknown fields rather than reject them, so later additions do not break you.

FieldTypeDescription
eventstringThe kind of event. The Send test payload is "orova.test".
workspace_idintegerID of the workspace the payload came from.
project_idinteger | nullID of the project holding the channel, or null if the channel has none.
postobjectThe post itself.
post.titlestringThe post headline.
post.contentstringThe body of the post, as text.
post.mediaarrayAttachments belonging to the post; an empty array for text-only posts.
post.channelsarray of stringThe channel kinds this post targets, for example ["api"].
sent_atstringWhen Orova sent it, ISO 8601 in UTC, ending in "Z".

/ Verification

Verifying the signature

The X-Orova-Signature header reads sha256=<hex>, where <hex> is the HMAC-SHA256 of the raw bytes of the request body, keyed with the channel secret. Recompute it over exactly the bytes you received — parsing the JSON and re-serializing it will not match, because a single different space changes the signature.

Node.js

// Node.js / Express — verify X-Orova-Signature, then answer fast
import express from "express";
import crypto from "node:crypto";

const app = express();
const OROVA_SECRET = process.env.OROVA_SECRET;   // the key you saved in Orova

// Keep the RAW bytes: the signature covers them, not re-serialized JSON.
app.post(
  "/orova/social",
  express.raw({ type: "application/json", limit: "5mb" }),
  (req, res) => {
    const sent = req.get("x-orova-signature") || "";
    const mine =
      "sha256=" +
      crypto.createHmac("sha256", OROVA_SECRET).update(req.body).digest("hex");

    // Constant-time compare — never use ===.
    const a = Buffer.from(sent);
    const b = Buffer.from(mine);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).json({ error: "bad signature" });
    }

    const payload = JSON.parse(req.body.toString("utf8"));
    if (payload.event === "orova.test") {
      return res.status(200).json({ ok: true });   // the Send test button
    }

    // Real post: hand the work to a queue and reply straight away.
    queuePost(payload).catch(console.error);
    return res.status(200).json({ ok: true });
  },
);

app.listen(3000);

Python

# Python / Flask — same check, same rules
import hashlib
import hmac
import json
import os

from flask import Flask, request

app = Flask(__name__)
OROVA_SECRET = os.environ["OROVA_SECRET"].encode("utf-8")

@app.post("/orova/social")
def orova_social():
    raw = request.get_data()                       # bytes, exactly as sent
    mine = "sha256=" + hmac.new(OROVA_SECRET, raw, hashlib.sha256).hexdigest()
    sent = request.headers.get("X-Orova-Signature", "")
    if not hmac.compare_digest(mine, sent):        # constant-time compare
        return {"error": "bad signature"}, 401

    payload = json.loads(raw)
    if payload.get("event") == "orova.test":
        return {"ok": True}, 200

    queue_post(payload)                            # do the slow work later
    return {"ok": True}, 200

Compare with a constant-time function (crypto.timingSafeEqual, hmac.compare_digest), never with a plain equality check. If the signature does not match, return 401 and stop — do not process the payload.

/ Replying

How to reply

Orova only looks at the HTTP status. Anything in the 2xx range counts as success; the response body can be whatever you like.

  • Reply fast. The Send test button waits 10 seconds at most. Push slow work — downloading media, calling a third-party API — onto a queue and return 2xx straight away.
  • Orova never retries by itself. That is deliberate: automatic retries against a publishing API are how posts end up published twice. A failed send marks the post failed with the reason, and the user retries it from inside Orova.
  • Be ready for a repeat. When a user retries, the same payload arrives again. De-duplicate on your side — for example by remembering the workspace_id and sent_at pairs you have handled.

/ Security

Security

  • Always verify the signature. Your endpoint is a public URL — anyone can call it. The signature is the only thing that proves a payload really came from Orova.
  • Keep the secret out of your code. Store it in an environment variable or a secrets manager, never hard-coded in a file you commit.
  • Rotate the secret by saving again. Enter the same receiving address with the new secret and click Save channel — Orova overwrites the existing channel. Update your endpoint at the same time.
  • Disconnecting deletes the secret. Disconnect the channel and Orova removes the stored secret and stops sending to that address.

/ Wrapping up

That is the whole contract

One https address, one secret, one signature check, one 2xx status — that is all it takes for posts from Orova Social to reach your systems. What happens next is yours to decide: where they get published, where they get stored, how they get logged.

← Back to the guide library

/ Need a hand?

Stuck wiring up your endpoint? Open the Support section inside your workspace, or send us a message.