サーバーサイドトラッキング
POST /api/v1/events エンドポイントを使うと、バックエンドから直接 Zenovay に分析イベントを送信できます。決済 Webhook で取得した購入、認証サービスから発生する identify イベント、その他クライアントブラウザを真実のソースにできない場面でご利用ください。
イベントはブラウザトラッカーのイベントと同じ Postgres テーブルに到着しますが、source: 'server' でタグ付けされるため、ブラウザストリームと並んでダッシュボードに表示されます。app.zenovay.com の整合性レポートはこのフラグを利用して 2 つのストリームを区別します。
ブラウザトラッカーとサーバーサイドエンドポイントは互いに補完するものであり、置き換え可能ではありません。両方を併用してください。ブラウザトラッカーはページビューとエンゲージメントを取得し、サーバーエンドポイントはブラウザに送信を任せられない正本のイベント(購入、識別、サイト外の活動など)を取得します。
エンドポイント
/api/v1/events最大 1,000 件のサーバーサイドイベントをバッチで取り込みます
URL: https://api.zenovay.com/api/v1/events
ヘッダー:
Authorization: Bearer zv_...— 必須Content-Type: application/json— 必須Sec-GPC: 1— 任意。指定された場合、バッチ全体がgpc_opted_outの理由で拒否されます
認証
app.zenovay.com → 設定 → API キー で発行した zv_* API キーで認証してください。このキーは呼び出し元を 1 つのチームに紐付け、さらに単一のウェブサイト(スコープ site_access)、またはそのチーム配下のすべてのウェブサイト(スコープ full_access)のいずれかを認可します。
キーはエッジで SHA-256 によりハッシュ化され、website_api_keys に対して検証されます。プランに応じた月次および分単位の上限が適用されます(詳しくは レート制限 を参照してください)。
リクエストボディ
{
"trackingCode": "ZV_AbC123...",
"events": [
{
"type": "pageview",
"ts": 1735689600000,
"visitorId": "11111111-1111-4111-9111-111111111111",
"sessionId": "22222222-2222-4222-9222-222222222222",
"props": { "url": "https://example.com/pricing", "referrer": "" },
"idempotencyKey": "pv-2026-05-02-abc"
}
],
"serverContext": {
"clientIp": "203.0.113.42",
"userAgent": "Mozilla/5.0 ...",
"acceptLanguage": "en-US"
}
}
| フィールド | 型 | 必須 | 備考 |
|---|---|---|---|
trackingCode | string | はい | ウェブサイトのトラッキングコード。app.zenovay.com → ウェブサイト設定で確認できます。 |
events | ServerEvent[] | はい | 1〜1,000 件のイベント。 |
events[i].type | pageview | event | identify | goal | purchase | はい | イベント分類 — 後述を参照。 |
events[i].ts | number | はい | Unix ミリ秒。サーバー時刻から ±24 時間以内である必要があります。範囲外の場合は invalid_ts で拒否されます。 |
events[i].sessionId | string | 条件付き | pageview と goal で必須。pageview では UUID が必要です(列が UUID 型のため)。 |
events[i].visitorId | string | 条件付き | pageview、identify、goal で必須。pageview では UUID が必要です。 |
events[i].props | object | はい | 種類ごとのペイロード — 後述を参照。 |
events[i].idempotencyKey | string | 推奨 | trackingCode 単位で一意であること。24 時間にわたり重複書き込みを防ぎます。最大 128 文字。 |
events[i].consent | opt-in | opt-out | unknown | 任意 | opt-out の場合、当該イベントのみが consent_opted_out の理由で拒否されます。 |
serverContext.clientIp | string | 任意 | 永続化前に日次ソルト付き SHA-256 でハッシュ化されます。平文は一切保存しません。 |
イベントタイプ
type | 書き込み先 | 必須の props |
|---|---|---|
pageview | visitors、page_views、user_events、Analytics Engine | url |
event | user_events(event_type='custom') | name(カスタムイベント名) |
identify | identified_users((website_id, visitor_id) で UPSERT) | email、または以下のいずれか: name、customer_id、phone、company。任意のカスタム属性も併せて指定可能。 |
goal | goal_completions(名前で目標を検索した上で書き込み) | name(app.zenovay.com → 目標 で有効な目標と一致している必要があります)、任意で value |
purchase | payment_events | amount(数値)、任意で currency(既定値 USD)、payment_provider(stripe/lemonsqueezy/polar/server、既定値 server)、status(既定値 succeeded) |
レスポンス
認証とバリデーションが成功した場合、エンドポイントは常に HTTP 200 を返します。イベント単位の結果はボディに記述されるため、成功と拒否のイベントを 1 つのバッチに混在させることができます。
{
"acceptedCount": 4,
"dedupedCount": 1,
"rejectedCount": 0,
"errors": []
}
イベントが拒否された場合、errors には元のインデックスとともに各失敗が一覧表示されます。
{
"acceptedCount": 0,
"dedupedCount": 0,
"rejectedCount": 1,
"errors": [
{ "index": 0, "reason": "invalid_ts", "message": "ts is missing or beyond ±24h skew tolerance" }
]
}
冪等性
各イベントには idempotencyKey を付与してください。以後 24 時間以内に同じ (trackingCode, idempotencyKey) を持つイベントは dedupedCount として返却され、二重には書き込まれません。128 文字を超えるキーや空のキーは invalid_idempotency_key の理由で拒否されます。
冪等性はエッジ(Cloudflare KV キャッシュ)とデータベース(visitors、page_views、user_events の (website_id, idempotency_key) に対する部分的な一意インデックス)の両方で適用されます。KV 層は高速で、データベース層は同じキーを持つ 2 つのイベントが数ミリ秒のうちに別の Cloudflare コロに到達するというまれなエッジケースを捕捉します。
レート制限
分単位および月単位の上限は、ご利用中のサブスクリプションプランから継承されます。
| プラン | 1 分あたり | 1 か月あたり |
|---|---|---|
| Free | 10 | 1,000 |
| Pro | 30 | 10,000 |
| Scale | 60 | 100,000 |
| Enterprise | 120 | 1,000,000 |
上限に達すると、エンドポイントは標準の X-RateLimit-* および X-Usage-* レスポンスヘッダーとともに HTTP 429 を返します。指数バックオフを推奨します。
プライバシー
- IP は決して平文で保存されません。
serverContext.clientIpが指定されている場合、永続化前に日次ソルト付き SHA-256 でハッシュ化されます。指定がない場合は、センチネル値unknownが保存されます。 - GPC を尊重します。
Sec-GPC: 1を伴うリクエストは、バッチ内のすべてのイベントをgpc_opted_outの理由で拒否します。行は一切書き込まれません。 - イベント単位の同意。
consent: 'opt-out'のイベントはconsent_opted_outの理由で個別に拒否されますが、同じバッチ内の他のイベントは引き続き受け入れられる場合があります。 - サーバーサイドの同意取得は顧客側の責任です。 ブラウザトラッカーは Cookie レスであり、ePrivacy/GDPR の下で同意取得前から合法に動作しますが、サーバーサイドでの取り込みは定義上、お客様自身のアプリケーションにおける同意ポリシーの下で行われます。
サンプル
curl -X POST https://api.zenovay.com/api/v1/events \
-H "Authorization: Bearer $ZENOVAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"trackingCode": "ZV_YourTrackingCode",
"events": [{
"type": "pageview",
"ts": '"$(date +%s%3N)"',
"visitorId": "11111111-1111-4111-9111-111111111111",
"sessionId": "22222222-2222-4222-9222-222222222222",
"props": { "url": "https://yourdomain.com/pricing" },
"idempotencyKey": "pv-2026-05-02-pricing-1"
}]
}'async function reportPurchaseToZenovay(stripeEvent, visitorId) {
await fetch('https://api.zenovay.com/api/v1/events', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ZENOVAY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
trackingCode: process.env.ZENOVAY_TRACKING_CODE,
events: [{
type: 'purchase',
ts: Date.now(),
visitorId,
props: {
amount: stripeEvent.data.object.amount_total / 100,
currency: stripeEvent.data.object.currency.toUpperCase(),
payment_provider: 'stripe',
provider_event_id: stripeEvent.id,
customer_email: stripeEvent.data.object.customer_email,
plan_name: stripeEvent.data.object.metadata?.plan,
},
idempotencyKey: stripeEvent.id,
}],
}),
});
}import requests, time, os
def identify_user(visitor_id: str, email: str, attributes: dict):
requests.post(
"https://api.zenovay.com/api/v1/events",
headers={
"Authorization": f"Bearer {os.environ['ZENOVAY_API_KEY']}",
"Content-Type": "application/json",
},
json={
"trackingCode": os.environ["ZENOVAY_TRACKING_CODE"],
"events": [{
"type": "identify",
"ts": int(time.time() * 1000),
"visitorId": visitor_id,
"props": {"email": email, **attributes},
"idempotencyKey": f"id-{visitor_id}-{int(time.time())}",
}],
},
timeout=5,
).raise_for_status()require 'net/http'
require 'json'
require 'securerandom'
ZENOVAY_API_KEY = ENV.fetch('ZENOVAY_API_KEY')
ZENOVAY_TRACKING_CODE = ENV.fetch('ZENOVAY_TRACKING_CODE')
def track_event_zenovay(event_name, visitor_id, properties = {})
uri = URI('https://api.zenovay.com/api/v1/events')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = 5
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ZENOVAY_API_KEY}"
request['Content-Type'] = 'application/json'
request.body = {
trackingCode: ZENOVAY_TRACKING_CODE,
events: [{
type: 'event',
ts: (Time.now.to_f * 1000).to_i,
visitorId: visitor_id,
props: { name: event_name, **properties },
idempotencyKey: "#{event_name}-#{visitor_id}-#{SecureRandom.hex(8)}"
}]
}.to_json
response = http.request(request)
raise "Zenovay tracking failed: #{response.code}" unless response.code.to_i == 200
JSON.parse(response.body)
end
# 例: Sidekiq ジョブまたは Rails コントローラから
track_event_zenovay('newsletter_subscribed', visitor_id, source: 'footer_form')拒否理由
reason | 発生する条件 |
|---|---|
gpc_opted_out | リクエストに Sec-GPC: 1 が含まれていた場合。バッチ全体が拒否されます。 |
consent_opted_out | 当該イベントが consent: 'opt-out' を持っていた場合。 |
invalid_ts | ts が欠落、数値以外、またはサーバー時刻から ±24 時間を超えている場合。 |
invalid_event_type | type がサポートされている 5 種の値のいずれでもない場合。 |
invalid_idempotency_key | idempotencyKey が空、または 128 文字を超えている場合。 |
invalid_visitor_id | 必須の visitorId が欠落、または pageview で UUID 形式でない場合。 |
invalid_session_id | 必須の sessionId が欠落、または pageview で UUID 形式でない場合。 |
unknown_goal | props.name が当該ウェブサイトの有効なカスタム目標のいずれにも一致しない場合。 |
invalid_payment_provider | props.payment_provider が stripe/lemonsqueezy/polar/server のいずれでもない場合。 |
internal_error | 想定外のデータベースまたはランタイム障害。同じ idempotencyKey でリトライしても安全です。 |
これらのイベントはダッシュボードのどこに表示されるか
各イベントタイプは Zenovay の特定の画面に表示されます:
type | 表示先 |
|---|---|
pageview | ページタブ、リアルタイムアクティビティ、訪問者セッションタイムライン |
event | カスタムイベント一覧、訪問者タイムライン(特定のイベントは event_name でフィルタ可能) |
identify | 識別済みユーザーページ、訪問者プロファイル詳細 |
goal | 目標タブ、ファネル完了統計 |
purchase | 収益ダッシュボード、収益アトリビューション、識別済みユーザープロファイル |
各 API キーには app.zenovay.com → 設定 → API キー → [キーをクリック] に専用の サーバーサイドアクティビティ パネルがあります。キーのウェブサイトスコープにおける直近 24 時間および 7 日間の受け入れ済みイベント数、上位イベントタイプ、最新 20 件のイベントを表示します。
ここに表示されるのは受け入れ済みイベントのみです。拒否理由は POST /api/v1/events のレスポンス本体で同期的に返されます — 送信側のデバッグ時にはそちらを確認してください。
このエンドポイントが行わないこと
- カスタム目標をその場で作成しません。事前に app.zenovay.com → 目標 で定義してください。
- オフラインの活動をオンラインの訪問者にマッチングしません。この機能は別エンドポイントとしてロードマップ上にあります。
- ブラウザトラッカーを置き換えません。バックエンド由来のページビューには、
propsで指定しない限り、デバイス、OS、ブラウザ、ジオデータは含まれません。 - 訪問者 ID を生成しません。お客様側で既に保有している UUID を使用してください。
pageviewイベントでは、その UUID は実際の RFC-4122 UUID である必要があります。