Pixels
A merchant pixels record stores the per-merchant configuration for ad-network tracking pixels and server-side conversion APIs (CAPI). One row per merchant. The shape mirrors what Storlaunch's storefront snippet needs to fire client-side tracking, plus the secrets needed for Ripllo to make server-side conversion calls on the merchant's behalf.
This resource has two access modes you need to keep straight:
GET /andPATCH /— auth-required. Returns the full record including CAPI access tokens. Used by the dashboard.GET /public/:accountId— no auth. Returns IDs only, never secrets. Used by Storlaunch's storefront pages to fire client-side pixel snippets.
The explicit select list on the public route is the primary guard against shipping metaCapiAccessToken to the storefront. Any future edit to this resource must be paired-reviewed with the same rigor as a payout webhook secret.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/pixels |
Get pixel config |
PATCH |
/api/v1/pixels |
Update pixel config |
GET |
/api/v1/pixels/public/:accountId |
Public pixel config (no auth, no secrets) |
Get pixel config
GET /api/v1/pixels
Returns the merchant's full pixel record, including secrets. If the merchant hasn't configured anything yet, returns a default stub with enabled: true and every provider field null, so the dashboard can render the empty state without special-casing.
Response
{
"data": {
"id": "mxp_01HX...",
"accountId": "acc_01HX...",
"metaPixelId": "1234567890",
"metaCapiAccessToken": "EAAGm…",
"metaTestEventCode": "TEST12345",
"googleAnalyticsId": "G-AB12CD34EF",
"googleAdsConversionId": "AW-1234567890",
"googleAdsPurchaseLabel": "abcDEF1234",
"tiktokPixelId": "C4ABC123…",
"enabled": true,
"createdAt": "2026-05-01T10:42:00.000Z",
"updatedAt": "2026-05-13T10:11:00.000Z"
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
| Status | error.code |
When |
|---|---|---|
403 |
NO_ACCOUNT |
Caller's token has no accountId. |
Update pixel config
PATCH /api/v1/pixels
Upserts the merchant's record. Only fields you supply are updated; omitted fields stay as-is. To clear a field, send null.
Request body
| Field | Type | Notes |
|---|---|---|
metaPixelId |
string (≤64) | null | The Meta Pixel ID, e.g. 1234567890. Safe to embed client-side. |
metaCapiAccessToken |
string (≤500) | null | The CAPI access token, used server-side by Ripllo to send conversion events to Meta's Conversions API. Never returned by the public endpoint. |
metaTestEventCode |
string (≤64) | null | Meta's test-event code for CAPI dry runs. Set during integration testing; clear once verified. |
googleAnalyticsId |
string (≤64) | null | Measurement ID, format G-XXXXXXXX. |
googleAdsConversionId |
string (≤64) | null | Google Ads conversion ID, format AW-XXXXXXXXXX. |
googleAdsPurchaseLabel |
string (≤64) | null | Google Ads conversion label for purchases. |
tiktokPixelId |
string (≤64) | null | TikTok Pixel ID. |
enabled |
boolean | Master switch — when false, the public endpoint returns null and Ripllo's server-side CAPI dispatcher skips this merchant. |
Response
The full updated MerchantPixels object (including secrets, since this is the merchant-facing endpoint).
Examples
// Node
await ripllo.pixels.update({
metaPixelId: '1234567890',
metaCapiAccessToken: process.env.META_CAPI_TOKEN,
googleAnalyticsId: 'G-AB12CD34EF',
enabled: true,
});
# Python
ripllo.pixels.update(
meta_pixel_id='1234567890',
meta_capi_access_token=os.environ['META_CAPI_TOKEN'],
google_analytics_id='G-AB12CD34EF',
enabled=True,
)
// Go
_, err := client.Pixels.Update(ctx, &ripllo.PixelsUpdateParams{
MetaPixelID: ripllo.String("1234567890"),
MetaCapiAccessToken: ripllo.String(os.Getenv("META_CAPI_TOKEN")),
GoogleAnalyticsID: ripllo.String("G-AB12CD34EF"),
Enabled: ripllo.Bool(true),
})
# curl
ripllo_curl PATCH '/api/v1/pixels' \
'{"metaPixelId":"1234567890","enabled":true}'
Public pixel config
GET /api/v1/pixels/public/:accountId
No auth. Returns a sanitized view of the merchant's pixel IDs, suitable for shipping to a storefront page. Returns null (not 404) when:
- The merchant has no config row.
- The merchant has set
enabled: false. - The merchant has a row but all four of
metaPixelId,googleAnalyticsId,googleAdsConversionIdandtiktokPixelIdare empty. Only those four count towards "has something to fire" —googleAdsPurchaseLabelis excluded from the test, so a merchant whose only configured field is the purchase label also getsnull.
This null-pass-through is what lets storefronts unconditionally request the snippet and skip rendering when it comes back falsy.
Response — success
{
"data": {
"metaPixelId": "1234567890",
"googleAnalyticsId": "G-AB12CD34EF",
"googleAdsConversionId": "AW-1234567890",
"googleAdsPurchaseLabel": "abcDEF1234",
"tiktokPixelId": "C4ABC123…",
"enabled": true
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Response — nothing to fire
{ "data": null, "error": null, "meta": { ... } }
Note what's missing from the success response: metaCapiAccessToken and metaTestEventCode. These are server-side secrets and are never returned from /public/:accountId regardless of enabled state. If you see them leaking in any future release, file a security ticket immediately.
The merchant pixels object
| Field | Type | Nullable | Notes |
|---|---|---|---|
id |
string | no | Opaque cuid — no type prefix. |
accountId |
string | no | Unique — one row per merchant. |
metaPixelId |
string | yes | Public, embed-safe. |
metaCapiAccessToken |
string | yes | Secret. Server-side only. |
metaTestEventCode |
string | yes | Optional; only set during CAPI integration testing. |
googleAnalyticsId |
string | yes | Public. |
googleAdsConversionId |
string | yes | Public. |
googleAdsPurchaseLabel |
string | yes | Public. |
tiktokPixelId |
string | yes | Public. |
enabled |
boolean | no | Master switch. |
createdAt, updatedAt |
ISO 8601 | no |
CAPI helper: hashEmail
Meta's CAPI expects em as the SHA-256 hex digest of email.trim().toLowerCase(). Ripllo has a hashEmail helper for this, but it lives server-side in the Ripllo backend only (backend/src/routes/pixels.ts) — the Node, Python and Go SDKs do not export it, so import { hashEmail } from '@forjio/ripllo-node' fails. It's a one-liner in every language:
// Node
import { createHash } from 'node:crypto';
const hashEmail = (email) => createHash('sha256').update(email.trim().toLowerCase()).digest('hex');
const em = hashEmail('Alice@Example.com'); // → "5a13d2..." (hex)
# Python
import hashlib
def hash_email(email: str) -> str:
return hashlib.sha256(email.strip().lower().encode()).hexdigest()
Getting the normalisation wrong (forgetting the lowercase or the trim) is the most common reason CAPI events don't match — hash exactly those two transforms and nothing else.
Security review checklist
When changing this resource (route, schema, or the SDK surface), confirm:
- The
/public/:accountIdselectlist explicitly lists only the public IDs — never spreads...row. metaCapiAccessTokenandmetaTestEventCodeare not on the public select list.- Any new secret field added here is added to both the merchant-facing
select(so it surfaces) and the public-facing select-exclusion list (so it doesn't leak). - There is no regression test for this. No
pixels.test.tsexists; the hand-maintainedselectinbackend/src/routes/pixels.tsis the only thing keepingmetaCapiAccessTokenoff the public response. Review the select list by eye on every change to this route — and writing that test is a standing to-do.
This resource is high-blast-radius: a leak of metaCapiAccessToken lets anyone with the storefront URL impersonate the merchant when posting conversions to Meta, which corrupts attribution and can get the merchant's Meta account flagged.
Events
The pixels resource doesn't emit outbox events — treating credential changes as broadcastable events would be its own leak.
Nor are they audited: PATCH /pixels performs the upsert and nothing else. The audit log currently covers API-key and webhook-endpoint mutations only, so a changed metaCapiAccessToken leaves no trail. Log the change on your side if you need one.