Partner API v1 · Event catalog v5
Partner Event API
目標平台將一次業務事件送到 MeetROAS。請求必須使用帳號憑證簽章,並在固定欄位 mr_click_id 中攜帶從目標 URL 取得的 Click ID 值;URL 參數名稱預設為 mr_click_id,客戶可自訂。
1. 取得完整對接包
目標平台工程師不需要登入 MeetROAS 後台。請客戶帳號 Admin 交付一份帳號層級完整對接包,並確認下列項目全部存在:
- Key ID + Secret
- Production, validation, catalog, OpenAPI, Developer Hub, and Playground URLs
- 實際 App/Campaign 名稱、host、流量來源與目標 URL query key
- 固定系統測試 App +
mrt_+ 到期時間 - 簽章、請求大小、速率限制、重試與錯誤契約
帳號測試不選擇客戶 App/Campaign,也不包含正式投放 query key。正式 Campaign 的 query key 與 mrc_ 由投手在投放流程另行提供。
2. 取得並回傳 Click ID
MeetROAS 會把 Click ID 加到導向目標平台的 URL。query key 由客戶 App 設定,預設為 mr_click_id;目標平台必須讀取並儲存其值,再將同一個值放入 Partner Event JSON 的固定欄位 mr_click_id。
https://casino.example/register?mr_click_id=mrc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- 不要解析、修改或自行產生這個值。
- 不要把 Key ID 或 Secret 當作 mr_click_id。
- 不要假定每個客戶的 URL key 都是 mr_click_id;請使用該客戶提供的實際投放 URL。
- 正式 mrc_ 有效期為 30 天;未知、過期或不屬於該帳號的 ID 會回 404。
不投放廣告的完整測試
完整對接包提供固定系統測試 App 的 mrt_。Casino 儲存該值,放入 body 的 mr_click_id,並加上 test: true。MeetROAS 驗證歸因、事件格式與冪等,資料只進入 Test Runs,不做來源規劃、不送 postback,也不計入正式成效。
https://casino.example/register?mr_click_id=mrt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtest: true 必須搭配 mrt_;正式請求不得帶 test,且必須使用真實投放產生的 mrc_。測試完成後移除 test,不可把 mrt_ 改寫成 mrc_。
3. 建立請求標頭與 HMAC 簽章
| Header | 值 | 由誰產生 |
|---|---|---|
Content-Type | application/json | 目標平台固定填寫 |
X-MeetROAS-Key | mrk_… | MeetROAS 帳號 Admin 在後端建立 |
X-MeetROAS-Timestamp | 傳送當下的 10 位 Unix seconds | 目標平台每次請求產生 |
X-MeetROAS-Signature | v1=<64 lowercase hex> | 目標平台以 Secret 計算;不是另外申請的憑證 |
- 先把 JSON 序列化成最終 raw body。
- 產生 timestamp = floor(current_time_ms / 1000)。
- 簽章輸入必須是 timestamp + '.' + raw_body。
- 以 Secret 做 HMAC-SHA256,輸出 lowercase hex,再加上 v1=。
1786344000.{"event_id":"payment-123",...}簽章後必須原樣傳送同一個 body。任何重新縮排、欄位排序或換行都會讓簽章失效。伺服器時間必須與 UTC 相差不超過 5 分鐘。
4. 驗證整合
POST https://api.meetroas.com/v1/events/validate
驗證端點使用與正式請求相同的憑證、標頭、簽章與 schema,但不會建立正式事件或觸發流量平台轉換。
選擇你的伺服器語言
四個範例都會序列化一次 body、簽署相同 bytes,再傳送到不會建立正式事件的驗證端點。
import { createHmac, randomUUID } from "node:crypto";
const keyId = process.env.MEETROAS_KEY_ID;
const secret = process.env.MEETROAS_SECRET;
if (!keyId || !secret) throw new Error("Missing MeetROAS credentials");
const body = JSON.stringify({
event_id: `test-${randomUUID()}`,
mr_click_id: "mrt_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
event: "deposit",
occurred_at: new Date().toISOString(),
value: 49.95,
currency: "USD",
test: true,
});
const timestamp = String(Math.floor(Date.now() / 1000));
const digest = createHmac("sha256", secret)
.update(`${timestamp}.${body}`, "utf8")
.digest("hex");
const response = await fetch("https://api.meetroas.com/v1/events/validate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-MeetROAS-Key": keyId,
"X-MeetROAS-Timestamp": timestamp,
"X-MeetROAS-Signature": `v1=${digest}`,
},
body,
});
console.log(response.status, await response.text());import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class MeetROASExample {
public static void main(String[] args) throws Exception {
String keyId = requireEnv("MEETROAS_KEY_ID");
String secret = requireEnv("MEETROAS_SECRET");
String body = String.format(
"{\"event_id\":\"test-%s\",\"mr_click_id\":\"mrt_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"," +
"\"event\":\"deposit\",\"occurred_at\":\"%s\",\"value\":49.95,\"currency\":\"USD\",\"test\":true}",
UUID.randomUUID(), Instant.now());
String timestamp = Long.toString(Instant.now().getEpochSecond());
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal((timestamp + "." + body).getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte value : digest) hex.append(String.format("%02x", value & 0xff));
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.meetroas.com/v1/events/validate"))
.header("Content-Type", "application/json")
.header("X-MeetROAS-Key", keyId)
.header("X-MeetROAS-Timestamp", timestamp)
.header("X-MeetROAS-Signature", "v1=" + hex)
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
private static String requireEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) throw new IllegalStateException("Missing " + name);
return value;
}
}package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type event struct {
EventID string `json:"event_id"`
ClickID string `json:"mr_click_id"`
Event string `json:"event"`
OccurredAt string `json:"occurred_at"`
Value float64 `json:"value"`
Currency string `json:"currency"`
Test bool `json:"test"`
}
func main() {
keyID, secret := os.Getenv("MEETROAS_KEY_ID"), os.Getenv("MEETROAS_SECRET")
if keyID == "" || secret == "" { panic("missing MeetROAS credentials") }
body, err := json.Marshal(event{
"test-" + strconv.FormatInt(time.Now().UnixNano(), 10), "mrt_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"deposit", time.Now().UTC().Format(time.RFC3339), 49.95, "USD", true,
})
if err != nil { panic(err) }
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(append([]byte(timestamp+"."), body...))
req, err := http.NewRequest("POST", "https://api.meetroas.com/v1/events/validate", bytes.NewReader(body))
if err != nil { panic(err) }
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-MeetROAS-Key", keyID)
req.Header.Set("X-MeetROAS-Timestamp", timestamp)
req.Header.Set("X-MeetROAS-Signature", "v1="+hex.EncodeToString(mac.Sum(nil)))
response, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
fmt.Printf("%d %s\n", response.StatusCode, responseBody)
}using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var keyId = RequireEnv("MEETROAS_KEY_ID");
var secret = RequireEnv("MEETROAS_SECRET");
var body = JsonSerializer.Serialize(new {
event_id = $"test-{Guid.NewGuid()}",
mr_click_id = "mrt_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
@event = "deposit",
occurred_at = DateTimeOffset.UtcNow.ToString("O"),
value = 49.95,
currency = "USD",
test = true,
});
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}"));
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.meetroas.com/v1/events/validate");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
request.Headers.Add("X-MeetROAS-Key", keyId);
request.Headers.Add("X-MeetROAS-Timestamp", timestamp);
request.Headers.Add("X-MeetROAS-Signature", $"v1={Convert.ToHexString(digest).ToLowerInvariant()}");
using var response = await new HttpClient().SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
static string RequireEnv(string name) => Environment.GetEnvironmentVariable(name)
?? throw new InvalidOperationException($"Missing {name}");{
"valid": true,
"authentication": "valid",
"schema": "valid",
"event": "deposit",
"catalog_version": 2,
"test": true
}回應中的 catalog_version 是該事件正規化 identity 的版本;為確保既有 event_id 重試仍保持相同 identity,既有事件 shape 會保留原始 v1/v2。目前接受的清單版本請讀取機器事件清單。
對接方可直接在 Playground 輸入 Key ID 與 Secret,自行完成同一套簽章、schema 與安全拒絕驗證;Secret 只留在該瀏覽器分頁記憶體。
5. 先傳送測試事件,再切換正式
POST https://api.meetroas.com/v1/events
{
"event_id": "test-d58a7aa7-043e-43d1-9c3f-9254da8370c5",
"mr_click_id": "mrt_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"event": "deposit",
"occurred_at": "2026-11-09T16:16:33.303Z",
"value": 49.95,
"price": 0.01,
"currency": "USD",
"user_data": {
"email_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"phone_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"external_id_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
},
"client": {
"ip": "192.0.2.10",
"user_agent": "Mozilla/5.0 (Example Browser)",
"page_url": "https://destination.example/complete",
"referrer_url": "https://destination.example/checkout",
"meta_fbp": "fb.1.1788480000000.1234567890",
"tiktok_ttp": "synthetic-test-ttp"
},
"test": true
}| 欄位 | JSON type | 必要 | 格式與語義 |
|---|---|---|---|
event_id | string | 是 | 目標平台產生的冪等鍵,1–128 字元,不含個人資料;同一業務事實重試必須沿用。 |
mr_click_id | string | 是 | 固定 API 欄位;值來自目標 URL 中客戶設定的 Click ID query key(預設 mr_click_id)。 |
event | string | 是 | 必須來自下方接受事件清單。 |
occurred_at | string | 是 | ISO-8601 UTC;一般來源須在 31 天內,Meta/TikTok 須在七天內且不可超過未來五分鐘。 |
value | number | 金融事件選填 | 目標平台回報的轉換價值/收益。0–1,000,000,000,最多六位小數;不是 minor units,MeetROAS 不換算。 |
price | number | 金融事件選填 | 目標平台回報的轉換成本;與 value 獨立,可單獨或同時提供。 |
currency | string | 選填 | ISO 4217 大寫三碼;提供 currency 時必須同時提供 value 或 price,MeetROAS 不猜幣別。 |
user_data | object | 選填 | 合法取得並同意用於廣告衡量的 SHA-256 小寫雜湊:email_sha256、phone_sha256、external_id_sha256。不要傳原始個人資料。 |
client | object | 選填 | 事件當下的瀏覽器 ip、user_agent、無 query/fragment 的 HTTPS page_url/referrer_url,以及 meta_fbp/tiktok_ttp;不可填下游伺服器資料。TikTok 建議 ip 與 user_agent 成對提供。 |
test | boolean | 僅測試時 | 測試請求固定為 true 並搭配 mrt_。正式請求移除此欄位並使用 mrc_。 |
user_data / client 子欄位
兩個物件及其子欄位均選填;缺值請省略,不要傳 null、陣列或未知欄位。金額使用 JSON number(49.95,不是 "49.95"),test 使用 boolean。以下範例的雜湊、IP、cookie 全是假資料,只展示格式,不可當作真實訪客資料送出;正式使用實際瀏覽器資料,且須有廣告衡量用途的合法依據與同意。
| 欄位 | JSON type | 必要 | 格式與限制 |
|---|---|---|---|
| user_data.email_sha256 | string | 選填 | SHA-256 小寫十六進位,恰好 64 字元 |
| user_data.phone_sha256 | string | 選填 | SHA-256 小寫十六進位,恰好 64 字元 |
| user_data.external_id_sha256 | string | 選填 | SHA-256 小寫十六進位,恰好 64 字元 |
| client.ip | string | 選填 | 事件當下訪客的 IP 字串;2–45 字元,僅 0–9、A–F、a–f、冒號與句點 |
| client.user_agent | string | 選填 | 事件當下瀏覽器 User-Agent;1–1024 字元,不含控制字元 |
| client.page_url | string | 選填 | HTTPS URL,最多 2048 字元;不可含帳號密碼、query 或 fragment |
| client.referrer_url | string | 選填 | HTTPS URL,最多 2048 字元;不可含帳號密碼、query 或 fragment |
| client.meta_fbp | string | 選填 | 實際 Meta _fbp cookie;1–256 字元,僅英數字、句點、底線、連字號 |
| client.tiktok_ttp | string | 選填 | 實際 TikTok _ttp cookie;1–256 字元,僅英數字、句點、底線、連字號 |
MeetROAS 分開儲存 value 與 price,並只投影到語意相符且該上游支援的欄位。TrafficStars 可同時接收 value(收益)與 price(CPA 成本);PropellerAds payout、ExoClick value、MGID r 與 BidVertiser revenue 只接收 value;HilltopAds price 與 Adsterra atpay 只接收 price;RichAds 不傳送金額。選填 match data 只加密暫存於待送 payload,七天後清除;同一 event_id 改變金額、事件或 match data 會回 409。Meta/TikTok 事件須在發生後七天內送達。
6. 接受的事件
event 必須使用下列 key。請以機器可讀 catalog 作為自動驗證的權威來源。
賭場或目標平台只傳送其實際支援並已啟用的事件,不需要實作或傳送完整清單。它是業務事實的 producer;尤其 first_day_deposit 由賭場判定,MeetROAS 不自行推導或計算首日。
| 事件 key | 何時傳送 | 重複規則 | event_id 規則 | 金額 |
|---|---|---|---|---|
account_registration完成註冊 | 使用者已完成目標平台定義的帳號註冊流程;僅瀏覽頁面或送出未完成的表單不算。 | once_per_click | 每次註冊使用一個穩定 ID;重試時沿用同一個 ID。 | 禁止 |
first_deposit完成首充 | 帳號的第一筆已驗證充值已成功生效;pending 或失敗交易不算。 | once_per_click | 每個帳號只傳送一次;重試時沿用同一個 ID。 | 選填 |
first_day_deposit完成首日首充 | 賭場判定玩家在首日完成首次已生效的入金、充值或購買後,由目標平台主動傳送;MeetROAS 只驗證、儲存與路由,不根據註冊日、時區或交易歷史自行計算首日。 | once_per_account | 每個玩家或帳號只傳送一次;同一首日首次入金事實與重試沿用一個穩定 ID。 | 選填 |
deposit完成充值 | 一筆已驗證且成功生效的充值;這是可重複事件。 | repeatable | 每筆交易使用不同 ID;同一筆交易重試時沿用原 ID。 | 選填 |
login完成登入 | 目標平台已驗證憑證並成功建立新的已認證登入工作階段;失敗嘗試、僅重新整理既有工作階段或只瀏覽登入頁不算。 | repeatable | 每次成功建立登入工作階段使用不同 ID;重試同一次登入時沿用原 ID。 | 禁止 |
7. 支援的流量平台
下游只需實作一次 Partner Event API;MeetROAS 負責依客戶設定連線對應流量平台。
| 狀態 | 流量平台 |
|---|---|
| 支援 | Meta (Facebook CAPI) · TikTok Events API · TrafficStars · PropellerAds · RichAds · ExoClick · HilltopAds · EvaDav · BidVertiser · AdMaven · AdsGram · Kadam · Adsterra · GeeMee · MGSkyAds |
| 即將支援 | MGID |
8. 回應、重試與上線
每個 Key ID、每個端點 scope 的限制為 300 次/60 秒;raw body 上限 16384 bytes;timestamp 容許偏差 300 秒;客戶端請求 timeout 建議 10 秒。429 會回 Retry-After: 60。建議 2、4、8、16、32 秒指數退避,加隨機抖動,最多重試 5 次;仍失敗則進入人工調查,不得更換 event_id 或重建 body。
| HTTP | 意義 | 動作 |
|---|---|---|
200 | validate 成功,或測試/正式事件為已處理的重複 event_id | 成功;不要產生新 event_id 重送 |
202 | 測試/正式事件首次接受 | 成功;不要重送 |
400/415 | JSON、欄位或 Content-Type 無效 | 修正後再送 |
401 | Key、timestamp 或 signature 無效 | 檢查帳號憑證、伺服器時間與 raw body |
404 | endpoint 找不到測試/正式 mr_click_id | 檢查是否儲存對應導向參數及是否已過期 |
409 | event_id 已對應不同業務事實 | 停止自動重試並調查冪等邏輯 |
429/5xx | 暫時性限制或服務錯誤 | 指數退避加抖動,保留原 event_id 與 body |
固定帳號測試的 200/202 回應必須是 integration_status=passed 與 delivery_readiness=not_applicable;這表示下游對接完成,且測試不依賴任何投手、Goal Binding 或 Connector。
- 取得憑證並通過驗證端點。
- 從完整對接包取得固定測試 mrt_,以 test: true 將支援的事件序列送到正式 endpoint。
- 在 Test Runs 確認歸因、格式、重複與衝突語義;測試過程不規劃或傳送 postback。
- 正式投放後儲存真實 mrc_,移除 test 欄位,才開始傳送正式業務事件。
9. 工程師上線前自檢
這份清單只確認你的後端是否已經接好。完成後,工程師不需再因每個 Campaign 改程式碼;投手只需在投放流程提供實際 query key 並完成 Campaign/轉換事件設定。
10. 安全要求
- Secret 只能儲存在伺服器端 secrets manager。
- 不得將 Secret 放入 URL、瀏覽器程式碼、工單或應用日誌。
- 每次請求使用新的 timestamp,並簽署最終傳送的原始 body。
- 憑證遺失或疑似外洩時,只替換這家下游的 Key;確認新包已儲存後撤銷舊 Key,其他下游不受影響。