เชื่อมระบบของคุณกับ Outbound Lead: สร้างธุรกิจให้ลูกค้า ตั้งค่าการหา lead จากกลุ่ม Facebook รับ lead ผ่าน webhook และดึงยอดใช้ไปคิดเงิน · Provision businesses, configure the Facebook lead scanner, receive leads by webhook and import usage for billing.
ทุก request ส่ง Authorization: Bearer olp_… (key ยาว 52 ตัว ออกให้โดยทีม Outbound Lead แสดงครั้งเดียว) · Send your key on every request. Keys can expire (30/90/180/365 days or never); two keys may be active at once so you can rotate without downtime.
https://outbound.mcloud.co.th/api/partner/v1429 rate_limited + Retry-AfterContent-Type: application/json); unknown fields are refused (400) so typos surface early.X-Request-Id — แนบมาเมื่อติดต่อทีม · quote it when you contact us.{ "error": { "code": "validation_failed", "message": "The request is invalid. See `issues`.", "issues": [{ "path": "minScore", "message": "Too big: expected number to be <=5" }] } }| code | HTTP | ความหมาย | Meaning |
|---|---|---|---|
validation_failed | 400 | ข้อมูลไม่ถูกต้อง ดู issues | Invalid input; see issues |
missing_token | 401 | ไม่ได้ส่ง Authorization | No Bearer token |
invalid_token | 401 | key ไม่ถูกต้องหรือถูกเพิกถอน | Unknown or revoked key |
token_disabled | 401 | key ถูกปิดชั่วคราว | Key disabled |
token_expired | 401 | key หมดอายุ | Key expired |
partner_suspended | 403 | บัญชีพาร์ทเนอร์ถูกระงับ | Partner suspended |
business_quota_exceeded | 403 | ธุรกิจครบโควตา | Business quota reached |
feature_disabled | 403 | ธุรกิจยังไม่เปิด Outbound Lead | Feature off for the business |
not_found | 404 | ไม่พบ หรือไม่ใช่ของพาร์ทเนอร์นี้ | Missing or not yours |
conflict | 409 | ขัดกับสถานะปัจจุบัน (เช่น กลุ่มซ้ำ, หยุดสแกนอยู่) | Conflicts with current state |
idempotency_key_reused | 422 | ใช้ Idempotency-Key ซ้ำกับคำขอที่ต่างกัน | Key reused with a different request |
rate_limited | 429 | เรียกถี่เกินไป รอตาม Retry-After | Slow down; honour Retry-After |
internal_error | 500 | ผิดพลาดฝั่งระบบ แจ้ง X-Request-Id | Server error; quote X-Request-Id |
ใส่ Idempotency-Key (ข้อความใดก็ได้ ≤ 255 ตัว) กับทุก POST: ส่งซ้ำด้วย key และ body เดิมภายใน 24 ชม. จะได้ผลลัพธ์แรกกลับมาพร้อม Idempotent-Replayed: true โดยไม่ทำซ้ำ · key เดิมกับ body ต่างกัน → 422. Only successful (2xx) responses are replayed; an error releases the key.
POST /businesses ยัง idempotent ด้วย externalRef: ส่ง ref เดิมได้ธุรกิจเดิม (200) แทนที่จะสร้างใหม่ (201).
?limit=&offset= → {items,total,limit,offset}?cursor= → {items,nextCursor,hasMore}. Leads and usage records are oldest-first and return the cursor even on an empty page — store it and poll with it to receive only new rows.# 0) who am I?
curl -s https://outbound.mcloud.co.th/api/partner/v1/whoami -H "Authorization: Bearer $OL_KEY"
# 1) create (or find) the business of your customer — idempotent on externalRef
curl -s -X POST https://outbound.mcloud.co.th/api/partner/v1/businesses \
-H "Authorization: Bearer $OL_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: create-tenant-42" \
-d '{"name":"ร้านตัวอย่าง","externalRef":"tenant-42"}'
# 2) configure what to look for
curl -s -X PUT https://outbound.mcloud.co.th/api/partner/v1/businesses/$BID/settings \
-H "Authorization: Bearer $OL_KEY" -H "Content-Type: application/json" \
-d '{"keywords":["ขายฝาก","จำนอง"],"leadDefinition":"คนที่ต้องการเงินด่วนโดยมีที่ดินค้ำ","minScore":3,"dailyBudgetThb":200}'
# 3) the Facebook groups to read
curl -s -X POST https://outbound.mcloud.co.th/api/partner/v1/businesses/$BID/groups \
-H "Authorization: Bearer $OL_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://www.facebook.com/groups/123456789","name":"กลุ่มขายฝาก"}'
# 4) switch the feature on (businesses start OFF)
curl -s -X PUT https://outbound.mcloud.co.th/api/partner/v1/businesses/$BID/features \
-H "Authorization: Bearer $OL_KEY" -H "Content-Type: application/json" \
-d '{"outboundLead":{"enabled":true}}'
# 5) read leads (keep nextCursor and poll with it), update their status
curl -s "https://outbound.mcloud.co.th/api/partner/v1/businesses/$BID/leads?limit=50&cursor=$CURSOR" -H "Authorization: Bearer $OL_KEY"
curl -s -X PATCH https://outbound.mcloud.co.th/api/partner/v1/businesses/$BID/leads/$LEAD_ID \
-H "Authorization: Bearer $OL_KEY" -H "Content-Type: application/json" \
-d '{"status":"contacting","note":"โทรแล้ว","partnerRef":"crm-77"}'
# 6) bill your customer: import usage rows (de-duplicate on id)
curl -s "https://outbound.mcloud.co.th/api/partner/v1/usage/records?cursor=$USAGE_CURSOR" -H "Authorization: Bearer $OL_KEY"
# 7) out of credit? pause scanning (no AI is spent while paused)
curl -s -X PUT https://outbound.mcloud.co.th/api/partner/v1/businesses/$BID/scanning \
-H "Authorization: Bearer $OL_KEY" -H "Content-Type: application/json" \
-d '{"paused":true,"reason":"credit exhausted"}'เพิ่มปลายทางด้วย POST /webhooks/endpoints (https เท่านั้น, ห้ามที่อยู่ภายใน) จะได้ secret ครั้งเดียว · Events: lead.created, lead.updated, scan.completed, bot.degraded, business.feature_changed. Answer 2xx within 10 s; failures are retried after 5 s, 30 s, 2 min, 10 min and 1 h, then marked dead (resend with POST /webhooks/deliveries/{id}/resend). Redirects are not followed.
POST https://your.app/webhooks/outbound-lead
Content-Type: application/json
X-OL-Event: lead.created
X-OL-Delivery: 0193f1c2-… (one per attempt series)
X-OL-Signature: t=1761900000,v1=5f2c… (HMAC-SHA256(secret, t + "." + rawBody), hex)
{
"id": "0193f1c0-…", // event id — the same on every retry
"type": "lead.created",
"createdAt": "2026-10-31T09:12:00.000Z",
"businessId": "0193e…",
"data": { "lead": { "id": "…", "score": 4, "phone": "081…", "summary": "…", "postUrl": "…", … } }
}ตรวจลายเซ็นทุกครั้ง · Always verify the signature
t + "." + body ดิบ ด้วย secret แล้วเทียบกับ v1 แบบ constant-time และปฏิเสธ t ที่เก่ากว่า 5 นาที. During secret rotation the header may carry several v1 values — accept if any matches.import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.OL_WEBHOOK_SECRET; // whsec_…
// IMPORTANT: verify the RAW body, before any JSON parsing
app.post("/webhooks/outbound-lead", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-OL-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400); // replay window 5 min
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
const ok = header.split(",").some((p) => {
const [k, v] = p.trim().split("=");
return k === "v1" && v.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v, "hex"), Buffer.from(expected, "hex"));
});
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
// de-duplicate on event.id (the same on every retry), then handle event.type
res.sendStatus(200); // answer within 10 s; do slow work in the background
});import hashlib, hmac, json, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["OL_WEBHOOK_SECRET"].encode() # whsec_…
@app.post("/webhooks/outbound-lead")
def outbound_lead():
raw = request.get_data() # the RAW body
header = request.headers.get("X-OL-Signature", "")
parts = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
t = next((int(v) for k, v in parts if k == "t" and v.isdigit()), None)
if t is None or abs(time.time() - t) > 300:
abort(400)
expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
if not any(k == "v1" and hmac.compare_digest(v, expected) for k, v in parts):
abort(401)
event = json.loads(raw)
# de-duplicate on event["id"], then handle event["type"]
return "", 200