Partner API v1 · Event catalog v5
Partner Event API
目标平台将一次业务事件送到 MeetROAS。请求必须使用账号凭证签章,并在固定字段 mr_click_id 中携带从目标 URL 取得的 Click ID 值;URL 参数名称预设为 mr_clickid,客户可自订。
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_clickid;目标平台必须读取并保存其值,再将同一个值放入 Partner Event JSON 的固定字段 mr_click_id。
https://casino.example/register?mr_clickid=mrc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- 不要解析、修改或自行产生这个值。
- 不要把 Key ID 或 Secret 当作 mr_click_id。
- 不要假定每个客户的 URL key 都是 mr_clickid;请使用该客户提供的实际投放 URL。
- 正式 mrc_ 有效期为 30 天;未知、过期或不属于该账号的 ID 会回 404。
不投放广告的完整测试
完整对接包提供固定系统测试 App 的 mrt_。Casino 保存该值,放入 body 的 mr_click_id,并加上 test: true。MeetROAS 验证归因、事件格式与冪等,资料只进入 Test Runs,不做来源规划、不送 postback,也不计入正式成效。
https://casino.example/register?mr_clickid=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-95269c1a-de92-48ac-8c62-b3dae89cf133",
"mr_click_id": "mrt_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"event": "deposit",
"occurred_at": "2026-09-24T20:13:20.256Z",
"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_clickid)。 |
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,其他下游不受影响。