BitelioBitelio

Connect a custom CMS

Use the existing API to synchronize contacts and build purchase follow-ups from your own backend

For Shopify, use the native integration and the guided Second purchase program. For another CMS or a custom store, your backend can synchronize contacts, send events and trigger custom workflows through Bitelio's existing API.

This guide is a custom integration, not a WooCommerce or PrestaShop plugin. It does not import an order ledger or catalog, enable Shopify's Thank You block, or populate the guided Second purchase program. Your CMS remains responsible for deciding which orders are paid, delivered, cancelled, refunded or repeat purchases.

1. Prepare your integration

Use a separate Bitelio project and synthetic contacts while building. Start with no active workflows or outgoing webhooks. The requests below write real contact and event records; event tracking has no dry-run flag, and a test-mode secret key does not put /v1/track into test mode.

Keep a durable queue or outbox in your backend. Verify incoming CMS webhooks, deduplicate order changes, process each customer's changes in order and record Bitelio's response IDs. Decide what counts as an eligible first purchase and a second purchase in your own store, including guest identity, subscriptions, test orders, cancellations and refunds.

OperationAuthenticationPurpose
POST /v1/contacts, PATCH /v1/contacts/:idSecret key (sk_*) with contacts:editMaintain contact data and consent
POST /v1/trackPublic key (pk_*) from the same projectRecord a custom event and notify workflows

Run these calls from your server. Never put a secret key in browser code, a theme or a public repository. /v1/track accepts only the public key: using it on a server does not make it secret or prove that a purchase happened. Anyone holding that public key can submit events, so these events must not authorize refunds, rewards, access or other financial actions.

This example registers a new customer without marketing consent:

curl -X POST https://api.bitelio.com/v1/contacts \
  -H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "buyer@example.com",
    "subscribed": false,
    "data": {
      "cms_order_count": 1,
      "cms_repurchase_eligible": false
    }
  }'

The response is the contact itself, with its id at the top level: 201 for a new contact, 200 for an existing one. Save that ID against the customer in your CMS.

This endpoint upserts by email. Explicit subscribed: false also unsubscribes an existing contact, so use this exact example only for an initial customer without consent. For subsequent order updates, omit subscribed to preserve the customer's current choice. Send subscribed: true only after a genuine, recorded opt-in; a purchase is not permission to reverse an unsubscribe. See consent imports and double opt-in.

3. Update eligibility, then record delivery

When your CMS has confirmed that the first qualifying order is delivered and there is no disqualifying incident, update the contact using the ID returned above:

curl -X PATCH https://api.bitelio.com/v1/contacts/CONTACT_ID \
  -H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data":{"cms_repurchase_eligible":true}}'

Then record one event for that delivery:

curl -X POST https://api.bitelio.com/v1/track \
  -H "Authorization: Bearer pk_YOUR_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cms:first-delivered:order-1001:v1" \
  -d '{
    "event": "cms.first_order.delivered",
    "email": "buyer@example.com",
    "data": {"order_id":{"value":"order-1001","persistent":false}}
  }'

Send this only after the contact has been created successfully. Tracking can create a missing contact, and new contacts default to subscribed if you omit subscribed. Here omission preserves the existing contact's choice. The persistent: false wrapper keeps the order ID in this event without adding it to the contact's fields.

Use your own namespace, such as cms.*. Do not imitate system events under shopify.*, pixel.* or email.*; the public tracking API rejects them.

When a second order, cancellation, refund or other exclusion occurs, set cms_repurchase_eligible to false before recording its event. Keep that state current while a workflow is waiting. Bitelio does not derive it from custom order payloads.

4. Build the custom workflow

In Workflows, create a disabled workflow with this sequence:

  1. Trigger: cms.first_order.delivered.
  2. Delay: your chosen interval after delivery.
  3. Condition: current contact data data.cms_repurchase_eligible equals true.
  4. Yes: send a reviewed Marketing template with a fixed link to your store. No: exit.

Keep re-entry disabled if the customer should enter this workflow only once. Check the current data.* field, not the original event.* snapshot. A WAIT_FOR_EVENT step only listens while it is waiting; it does not search past purchases. Marketing sends also check subscription and topic preferences.

Workflow editing requires workflows:edit; manually starting and cancelling executions require workflows:publish. Use the account's existing permissions. The custom workflow does not use the Retention program's execution switch, so that switch is not a sandbox for this integration.

To stop work already in progress, cancel its pending executions. Disabling the workflow only prevents new executions. A condition check and a later send are not an atomic order check, so an urgent cancellation should also cancel the pending execution from your backend or the dashboard.

5. Measure the second purchase you report

When the CMS confirms that this customer has made their second qualifying purchase, set eligibility to false and send the following event once. Do not send it for every later purchase:

curl -X POST https://api.bitelio.com/v1/track \
  -H "Authorization: Bearer pk_YOUR_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cms:second-paid:order-1002:v1" \
  -d '{
    "event": "cms.second_order.paid",
    "email": "buyer@example.com",
    "data": {"order_id":{"value":"order-1002","persistent":false}}
  }'

In Settings → Data → Conversion goals, create a goal named exactly cms.second_order.paid, then attach it to the workflow. Leave the amount field empty to count conversions without money. Creating goals requires settings:manage, attaching them to workflows requires workflows:edit, and reading results requires analytics:view.

These are conversions reported by your integration, dated when Bitelio receives the event. Historical timestamps in the payload do not backdate them. Attribution credits a recent click or open; it does not establish that the message caused the purchase. If you add amount and currency fields, the reported value is gross revenue, without automatic refund reversal, costs or margin. With non-persistent wrappers, the goal paths must include .value, for example amount.value and currency.value.

A conversion-goal holdout can compare sending against not sending without Shopify. It withholds eligible marketing across the project, not just this workflow, and needs an explicit measurement plan. Do not enable one merely to test this guide.

6. Validate and handle retries

  • Test only in the isolated project, with synthetic contacts and no active outgoing webhooks. Keep workflows disabled to inspect stored events first. To test execution, use a temporary workflow with conditions and an exit step only, without email, SMS or webhook actions.
  • Confirm a duplicate event request returns 409 with IDEMPOTENCY_KEY_REUSED, and store the IDs from the first successful response. Keys are scoped to the project and retained for 24 hours by default; they are not permanent order deduplication and do not replay the first response.
  • Keep your own durable deduplication. A timeout or 5xx may have happened after a write; do not generate a new key and blindly repeat it. Reconcile the contact/event history first. Respect Retry-After on 429, use bounded retries and avoid unbounded contact imports.
  • A 200 tracking response confirms the recorded event, not successful email delivery. Check workflow executions and email logs separately.
  • Before enabling the real workflow, verify the sending domain, consent rules, billing limits and current eligibility updates. Review the message and activate only after the integration checks pass.

The separate dashboard endpoint /events/track takes name and contact IDs rather than this payload. It does not provide the tracking endpoint's idempotency or rate-limit contract; it is not a drop-in replacement.

See event declarations, idempotency, workflows and conversion goals for the individual contracts.