Currency

Marketing

Plugipay family portal: marketing

The Marketing section covers three related surfaces: pixels (so ad platforms and analytics know your storefront exists), abandoned-cart recovery (so a missed checkout gets a follow-up), and broadcasts (so you can fan out a one-shot send to a contact list).

This page is a kickoff — each of the three sub-surfaces has its own dashboard page; we cover the highlights here and link out to source-of-truth API references.

Looking for “campaigns”? The top-level umbrella that groups discounts + creator briefs + affiliators + cart recovery + broadcasts is the Campaigns hub. The old “Marketing campaigns” (one-shot blast sends) was renamed Broadcasts in May 2026 to free the name up for the hub. The on-disk table is still called MarketingCampaign; the Prisma symbol and dashboard label are both Broadcast.

Pixels

Merchant pixels are the tracking IDs your storefront fires into ad platforms and analytics. Ripllo holds one row per workspace covering the common ones:

Pixel Where it lives Required?
Meta (Facebook) pixel ID metaPixelId Only if you run Meta ads
Meta CAPI access token metaCapiAccessToken Only if you want server-side conversion API
Meta test event code metaTestEventCode Only while you're debugging CAPI events
GA4 measurement ID googleAnalyticsId Recommended for everyone
Google Ads conversion ID googleAdsConversionId Only if you run Google Ads
Google Ads purchase label googleAdsPurchaseLabel Only if you run Google Ads
TikTok pixel ID tiktokPixelId Only if you run TikTok ads

There's also an enabled boolean (default true). Turn it off and the public read below returns null regardless of what's configured.

Configuring pixels

Navigate to Dashboard → Pixels. The form takes each ID as a plain string. Save calls PATCH /api/v1/pixels.

The CAPI access token is the only secret in the bunch. It is stored as a plain column (no encryption at rest today — the AES-256-GCM helper only covers channel credentials), but it is never returned through the public storefront read endpoint: that handler selects an explicit non-secret field list. The token comes back only through the merchant-authenticated read.

Storefront access

Your storefront fetches the public pixel set via:

GET /api/v1/pixels/public/:accountId

This returns only public IDs (no CAPI token), is unauthenticated, and is the endpoint your Storlaunch (or other) storefront calls to inject pixel tags into the page.

Abandoned-cart recovery

When a customer adds something to their cart but doesn't check out, Ripllo can send them a reminder email (with an optional discount code attached) and recover some of those sales.

Configuring the recovery flow

Navigate to Dashboard → Abandoned cart. The settings page takes:

  • enabled — master switch (off by default).
  • delayHours — how long the cart has to be idle before the first reminder fires. Integer, 1–168, default 4. Anything outside that range comes back as 400 VALIDATION.
  • emailSubject — the reminder's subject line (default "You left something in your cart").
  • emailPreview — the preview/preheader text (default "Come back to finish your order").
  • discountCodeId — optional. Pick an existing code from the dropdown; it's attached to every reminder. A code from another workspace is rejected with 400 VALIDATION.

That's the whole config — there is no sender (from-name/from-email) configuration and no body template, so no {{customer.name}}-style interpolation. Ripllo does not send the email itself yet; the partner storefront does, and Ripllo records it.

Save calls PATCH /api/v1/abandoned-cart/config.

Recording reminders

When your storefront detects an abandoned cart and decides to send (typically after the delayHours window), it calls:

POST /api/v1/abandoned-cart/reminders
Authorization: Ripllo-HMAC-SHA256 keyId=<id>, scope=*, signature=<hex>
{
  "customerId": "cust_...",
  "cartId": "cart_...",          // becomes externalRef
  "email": "alice@example.com",
  "cartSnapshot": [...],
  "valueAtSend": 250000,
  "currencyAtSend": "IDR",
  "discountCodeId": "disc_..."
}

This endpoint is partner-authenticated: the workspace it writes to comes from the signed request, not from the body. Send an accountId that isn't the account you're signed in as (or acting for via X-Ripllo-On-Behalf-Of) and you get 403 ACCOUNT_MISMATCH; omit it and Ripllo fills it in from the principal.

Ripllo first checks the customer's BuyerEmailPreference opt-out list — suppressed addresses are silently skipped (you get created: false, reason: 'opted_out' back). Otherwise it stamps a reminder row (bare cuid id) and returns { id, created: true }. Passing externalSource + externalRef makes the call idempotent: a replay of the same pair returns the existing row with created: false.

The reminder row has an optional marketingCampaignId column, but nothing populates it through the API today — reminders are always written with it null, so cart recovery doesn't yet show up under a Campaign's linked children. Attribution flows the other way for now: a reminder that carries a discount code rolls up through that code's campaign tag.

Marking recovered

When the same customer completes checkout shortly after a reminder, your payment-success handler calls:

POST /api/v1/abandoned-cart/recover
Authorization: Ripllo-HMAC-SHA256 keyId=<id>, scope=*, signature=<hex>
{
  "customerId": "cust_...",
  "checkoutSessionId": "...",
  "completedAt": "..."
}

Partner-authenticated in exactly the same way as /reminders. Ripllo finds the most recent unrecovered reminder for that customer sent within the last 72 hours and stamps recoveredAt + recoveredBySessionId on it, returning { recovered: true, reminderId } (or { recovered: false } if there was nothing to match). This drives your recovery-rate stat on the dashboard.

Opt-out

The unsubscribe link in every reminder email points at GET /api/v1/abandoned-cart/unsubscribe?accountId=…&email=…&token=…. All three query params are required — a link missing any of them renders a 400 page, and a token that doesn't verify against (accountId, email) renders a 401 page. Build the link with the helper in the abandoned-cart service so the HMAC matches. Hitting a valid link adds the email to BuyerEmailPreference for the workspace; subsequent reminder attempts to the same address skip silently.

Merchants can see and edit that suppression list directly: GET /api/v1/abandoned-cart/suppressions, POST /api/v1/abandoned-cart/suppressions ({ email }), and DELETE /api/v1/abandoned-cart/suppressions/:email.

Broadcasts (one-shot blast sends)

A broadcast is a one-shot email (or WhatsApp, or push) send to a contact list. Use it for: newsletter blasts, product launches, seasonal promotions.

Creating a broadcast

Navigate to Dashboard → Compose (the working name for the broadcast composer). The form covers:

  • Audience — pick a contact list or audience segment.
  • Channel — pick a connected email/WhatsApp integration.
  • Template — either reuse a saved template or compose inline. Merge tags are single-brace and fixed: {firstName}, {lastName}, {brandName}, {discountCode}, {cartUrl}, {productUrl}. Note that nothing substitutes them per recipient yet — the resolver exists but no send path calls it, so a tag currently goes out as literal text.
  • Schedule — send now or pick a future timestamp.

Save calls POST /api/v1/broadcasts — the broadcast sits in draft state until you trigger send.

Sending

Click Send test first — this triggers POST /api/v1/broadcasts/:id/send-test which delivers a single email to the address you specify, with the compiled template. Verify it looks right.

Click Send to fire POST /api/v1/broadcasts/:id/send. The broadcast flips draft/scheduledsending and the response returns immediately with { id, queued }; the worker then dispatches the queued MarketingMessage rows. Note that the worker only updates those message rows — nothing transitions the broadcast itself to sent today, so a fully-delivered blast stays in sending. /cancel moves it to paused. There is no failed broadcast status; per-message failures live on the message rows.

Templates

Reusable templates live under /api/v1/broadcasts/templates (GET/POST, plus PATCH/DELETE on /templates/:id). Each template has a name, channel-specific content, and an optional structured blocks document. POST /api/v1/broadcasts/templates/compile renders a blocks doc to the exact HTML+text a recipient would get — that's what the builder's preview pane shows.

Tying broadcasts to a campaign

Broadcasts carry a nullable marketingCampaignId, so a blast can be attached to a Campaigns hub row on create or with a later PATCH (set it to null to detach). The campaign detail page has a Broadcasts tab with an "Add broadcast" CTA, and GET /:id/full returns every linked broadcast.

The counters are the part that still lags: counts.broadcastsSent only counts linked broadcasts whose status is sent, and counts.messagesSent only counts messages belonging to those. Since nothing moves a broadcast out of sending today, both read 0 even after a blast has fully delivered. The linked-broadcast list is the reliable view until that transition ships.

Revenue attribution is still one-way: a broadcast that points users at a discount code rolls up its revenue via that code, because Ripllo does not run conversion tracking on the send itself. costIdr.broadcastSends is likewise always 0 — per-send pricing isn't modelled.

Next

  • Campaigns — the central hub that groups discounts, briefs, affiliators, and cart recovery.
  • Discount codes — attach a code to abandoned-cart reminders.
  • Referrals — another distribution lever.
  • API reference — full endpoint set including broadcasts, pixels, abandoned-cart.
CurrencyRupiah is paid by transfer or QRIS; US dollars settle through PayPal.