OROVA.VN — BIZ AI AGENT

/ 開発者向けガイド

Orova Social の投稿を Webhook で受け取る。

Orova Social が送る投稿を受け取るエンドポイントの作り方と、処理する前の署名検証について。

Orova Social は、接続済みのプラットフォームへ直接投稿します。投稿をそれ以外の場所へ届けたいとき — 社内チャンネル、自社アプリ、Orova がまだ対応していないプラットフォーム — には API チャンネルを使います。Orova があなたの HTTP アドレスへ投稿を送り、シークレットキーで署名します。その先の処理はあなた次第です。このページは開発者向けで、リクエストの取り決めをすべて説明します。

/ 概要

仕組み

プロジェクトの API チャンネルに、受信アドレス(https://)とシークレットキーを登録します。Orova はそこへ JSON 本文の POST リクエストを送り、X-Orova-Signature ヘッダーを付けます。これは本文のバイト列そのものを、あなたのキーで HMAC-SHA256 したものです。エンドポイント側は署名を検証し、投稿を受け取り、2xx を返します。

  • 受信アドレスは https 必須. アドレスが https:// でない、またはホストがない場合、Orova はチャンネルを保存せず、コード api_url を返します。
  • アドレス 1 つにつきチャンネル 1 つ. Orova は受信アドレスそのものでチャンネルを識別します。同じアドレスで保存し直すと既存チャンネルを上書きし、重複は作りません。
  • テスト送信ボタンがある. 接続後、チャンネルには Send test ボタンが表示されます。押すと orova.test のペイロードが送られ、返ってきた HTTP ステータスと往復時間(ミリ秒)が表示されます。

/ 設定

Orova 側の設定

Socialプロジェクト → 対象のプロジェクト → チャンネル タブを開き、API の枠を探します。入力欄が 2 つとボタンが 1 つあります。

  • 1

    受信 URL(Webhook). あなたのエンドポイントのアドレス。https:// で始めます。すべてのリクエストはここに届きます。

  • 2

    シークレットキー. 署名に使う文字列です。空欄にすると Orova がランダムに生成しますが、画面には二度と表示されません。長い文字列を自分で生成して貼り付け、控えを手元に残すことをおすすめします。

  • 3

    Save channel を押す. チャンネルが受信アドレスとともに一覧に現れます。シークレットキーは再表示されません。

  • 4

    Send test を押す. Orova がサンプルの orova.test を送ります。2xx が返れば「Webhook が … ms で 200 を返しました」と表示されます。それ以外のステータスはコードとともに、到達できない場合はネットワークエラーとして表示されます。

/ 取り決め

リクエストの取り決め

送信は毎回、次の形のリクエストで届きます。

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"
  }

本文は 1 行のコンパクトな UTF-8 JSON です。種類の判別には event を読んでください。テスト送信のペイロードは orova.test で、X-Orova-Event ヘッダーも同じ値です。将来の項目追加で壊れないよう、エンドポイントは未知の項目を無視する作りにしてください。

項目説明
eventstringイベントの種類。テスト送信は "orova.test"。
workspace_idinteger送信元のワークスペース ID。
project_idinteger | nullチャンネルが属するプロジェクトの ID。未割り当てなら null。
postobject投稿そのもの。
post.titlestring投稿のタイトル。
post.contentstring投稿本文(テキスト)。
post.mediaarray投稿に付く添付。テキストのみなら空配列。
post.channelsarray of stringこの投稿が対象とするチャンネル種別。例: ["api"]。
sent_atstringOrova が送信した時刻。ISO 8601 の UTC で、末尾は "Z"。

/ 検証

署名の検証

X-Orova-Signature ヘッダーは sha256=<hex> の形です。<hex> は本文の生バイト列をチャンネルのシークレットキーで HMAC-SHA256 した値です。受け取ったバイト列そのままで再計算してください。JSON を解析してから再度直列化した文字列では一致しません。空白 1 つ違うだけで署名は変わります。

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

比較には定数時間の関数(crypto.timingSafeEqualhmac.compare_digest)を使い、通常の等価比較は使わないでください。署名が一致しなければ 401 を返して処理を止めます。

/ 応答

応答の仕方

Orova が見るのは HTTP ステータスだけです。2xx なら成功で、本文の内容は自由です。

  • 早く返す. テスト送信の待ち時間は最大 10 秒 です。メディアのダウンロードや外部 API 呼び出しなど重い処理はキューに回し、すぐ 2xx を返してください。
  • Orova は自動で再送しない. 意図的な設計です。投稿系 API への自動リトライは二重投稿の原因になります。送信に失敗した投稿は理由付きで failed になり、利用者が Orova 上から投稿し直します。
  • 同じペイロードが再び来ても平気に. 投稿し直すと同じペイロードがもう一度届きます。workspace_idsent_at の組み合わせを記録するなど、受け側で重複を除いてください。

/ セキュリティ

セキュリティ

  • 必ず署名を検証する. エンドポイントは公開 URL であり、誰でも呼び出せます。そのペイロードが本当に Orova から来た証拠は署名だけです。
  • シークレットキーをコードに書かない. 環境変数またはシークレット管理に保存し、コミットするファイルに直接書かないでください。
  • キーの変更は保存し直すだけ. 同じ受信アドレスに新しいキーを入れて Save channel を押すと、Orova が既存チャンネルを上書きします。エンドポイント側も同時に更新してください。
  • 切断するとキーは削除される. チャンネルを切断すると、保存済みのシークレットキーは削除され、そのアドレスへの送信も止まります。

/ まとめ

取り決めはこれだけ

https のアドレス 1 つ、シークレットキー 1 つ、署名検証 1 回、2xx 1 つ。これだけで Orova Social の投稿があなたのシステムに届きます。その先 — どこへ公開するか、どこに保存するか、どう記録するか — はあなたが決めます。

← ガイド一覧へ戻る

/ お困りですか

エンドポイントの接続で詰まりましたか。ワークスペース内のサポートを開くか、 メッセージをお送りください。