/ 開発者向けガイド
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 ヘッダーも同じ値です。将来の項目追加で壊れないよう、エンドポイントは未知の項目を無視する作りにしてください。
| 項目 | 型 | 説明 |
|---|---|---|
| event | string | イベントの種類。テスト送信は "orova.test"。 |
| workspace_id | integer | 送信元のワークスペース ID。 |
| project_id | integer | null | チャンネルが属するプロジェクトの ID。未割り当てなら null。 |
| post | object | 投稿そのもの。 |
| post.title | string | 投稿のタイトル。 |
| post.content | string | 投稿本文(テキスト)。 |
| post.media | array | 投稿に付く添付。テキストのみなら空配列。 |
| post.channels | array of string | この投稿が対象とするチャンネル種別。例: ["api"]。 |
| sent_at | string | Orova が送信した時刻。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.timingSafeEqual、hmac.compare_digest)を使い、通常の等価比較は使わないでください。署名が一致しなければ 401 を返して処理を止めます。
/ 応答
応答の仕方
Orova が見るのは HTTP ステータスだけです。2xx なら成功で、本文の内容は自由です。
- 早く返す. テスト送信の待ち時間は最大
10 秒です。メディアのダウンロードや外部 API 呼び出しなど重い処理はキューに回し、すぐ 2xx を返してください。 - Orova は自動で再送しない. 意図的な設計です。投稿系 API への自動リトライは二重投稿の原因になります。送信に失敗した投稿は理由付きで
failedになり、利用者が Orova 上から投稿し直します。 - 同じペイロードが再び来ても平気に. 投稿し直すと同じペイロードがもう一度届きます。
workspace_idとsent_atの組み合わせを記録するなど、受け側で重複を除いてください。
/ セキュリティ
セキュリティ
- 必ず署名を検証する. エンドポイントは公開 URL であり、誰でも呼び出せます。そのペイロードが本当に Orova から来た証拠は署名だけです。
- シークレットキーをコードに書かない. 環境変数またはシークレット管理に保存し、コミットするファイルに直接書かないでください。
- キーの変更は保存し直すだけ. 同じ受信アドレスに新しいキーを入れて Save channel を押すと、Orova が既存チャンネルを上書きします。エンドポイント側も同時に更新してください。
- 切断するとキーは削除される. チャンネルを切断すると、保存済みのシークレットキーは削除され、そのアドレスへの送信も止まります。
/ まとめ
取り決めはこれだけ
https のアドレス 1 つ、シークレットキー 1 つ、署名検証 1 回、2xx 1 つ。これだけで Orova Social の投稿があなたのシステムに届きます。その先 — どこへ公開するか、どこに保存するか、どう記録するか — はあなたが決めます。
/ お困りですか
エンドポイントの接続で詰まりましたか。ワークスペース内のサポートを開くか、 メッセージをお送りください。
