M25 · Destinations

Destinations

Push audiences to Meta Ads, Google Ads, and custom webhooks in one click. Credentials are stored in KMS envelopes.

3 channels

Meta CAPI · Google Ads · Webhook

Three destination types. All share the same audience binding + dispatch log + dedup infrastructure.

Meta CAPI

Meta CAPI

Push hashed email + phone to a Custom Audience. SHA-256 lowercase trim + E.164 — exactly Meta's spec.

Google Ads Customer Match

Google Ads

OAuth 2.0 flow + platform-wide developer_token. Customer Match API for hashed user list management.

Webhook (custom)

Custom webhook

Push to your own system. HMAC-SHA256 signature header + JSON body + retry/dedup support.

Meta CAPI setup

Step-by-step setup

Pixel ID + Access Token + Test Event Code from Facebook Business Manager — 4 steps to live.

  1. 01

    Prep Pixel + Business Manager

    Facebook Business Manager → Events Manager → copy the Pixel ID. Make sure you have Custom Audience create permissions.

  2. 02

    Generate a System User Access Token

    Business Settings → Users → System Users → 'Generate Token' → ads_management + business_management scopes. Copy the token once.

  3. 03

    Add the credential in Gurulu

    /app/destinations → 'New Meta CAPI' → Pixel ID + Access Token + (optional) Test Event Code. Gurulu stores credentials in a KMS envelope; UI shows last-4 mask only.

  4. 04

    Bind audience + dispatch

    Pair destination + audience. On first dispatch the Custom Audience is created if missing; then hourly delta push kicks in.

// Hash format: lowercase, trim, SHA-256
// E.164 phone: '+901234567890' → sha256 hex
import { createHash } from 'node:crypto';

function hashPii(value: string): string {
  return createHash('sha256').update(value.trim().toLowerCase()).digest('hex');
}

// Audience üye listesi push
await fetch('https://graph.facebook.com/v18.0/{pixel_id}/users', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + ACCESS_TOKEN },
  body: JSON.stringify({
    payload: {
      schema: ['EMAIL_SHA256', 'PHONE_SHA256'],
      data: members.map((m) => [hashPii(m.email), hashPii(m.phone)]),
    },
  }),
});

Google Ads OAuth

OAuth flow + developer token

Create the OAuth Client in Google Cloud Console and apply for a developer_token — Customer Match API access opens up.

  1. 01

    Google Cloud Console — OAuth Client

    APIs & Services → Credentials → 'Create OAuth Client ID' → Application type: Web. Register the redirect URI exactly as Gurulu's callback.

  2. 02

    Developer Token application

    Google Ads → Tools → API Center → apply for a Developer Token. Standard Access can take 2-3 days.

  3. 03

    Start OAuth from Gurulu

    /app/destinations → 'New Google Ads' → 'Connect with Google' → consent screen → on callback refresh_token is stored in a KMS envelope.

# OAuth callback URI (Google Cloud Console'a kayıtla):
https://api.gurulu.io/v1/destinations/oauth/google/callback

# Developer token başvurusu — Google Ads API erişimi için
# https://developers.google.com/google-ads/api/docs/first-call/dev-token

Webhook integration

HMAC-SHA256 verify + retry

Verify on your server, dedupe, ack. Gurulu manages retries.

Node.js / Bun / TypeScript

// Node.js — HMAC-SHA256 verify
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyGuruluSignature(
  rawBody: string,
  header: string | undefined,
  secret: string,
): boolean {
  if (!header?.startsWith('sha256=')) return false;
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = header.slice(7);
  return (
    expected.length === received.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(received))
  );
}

Python 3.10+

# Python — HMAC-SHA256 verify
import hmac, hashlib

def verify_gurulu_signature(raw_body: bytes, header: str | None, secret: str) -> bool:
    if not header or not header.startswith("sha256="):
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    received = header[7:]
    return hmac.compare_digest(expected, received)

Payload sample

{
  "audience_id": "aud_01H8XYZ",
  "tenant_id": "tnt_01H8ABC",
  "members": [
    { "person_id": "per_01H8DEF", "email_sha256": "…", "joined_at": "2026-05-29T06:00:00Z" }
  ],
  "delta": "joined",
  "dispatched_at": "2026-05-29T07:00:00Z"
}

KMS encryption

Credentials transparency

Meta access token, Google refresh token, webhook secret — all stored in KMS envelopes.

How they're stored

Each credential is encrypted with an AES-GCM data encryption key (DEK); the DEK is wrapped by the root KMS key. Only the worker decrypts — UI never touches plain text.

  • docsModules.destinations.kms.how.bullets.envelope
  • Worker server-side decrypt only — UI and API never see plain text
  • Credential list in UI shows last-4 mask (e.g. '••••••a8f3')
{
  "kid": "kms_2026_q2_primary",
  "encrypted_dek": "AQEDAH4...",
  "iv": "5b9e8c7a4f3d2e1b6c0a9d8e",
  "tag": "a1b2c3d4...",
  "ciphertext": "f7e8d9..."
}

Dispatch + dedup

24h dedup + exp backoff retry

The same (audience, member, payload) won't be dispatched twice in 24 hours. On failure, exponential backoff retry.

Payload-hash dedup

A payload_hash is computed per dispatch + a 24h dedup window is held. Same hash → skip (idempotent).

Exp backoff retry

30s → 2m → 10m → 1h → 6h. After 5 attempts: DLQ. The dispatches table shows every attempt + error code.

Related docs

Read next

Create an audience first, then wire it to a destination. Go deeper via the API reference.

Destinations — Gurulu Docs