FloorSync

Free dry run, no card: see what Meta and GA4 would get for a sale. Syncs Stripe sales to Meta Ads and GA4 for towers.app floors.

Hosted MCP Server

npx add-mcp 'https://floorsync.towers.app/api/mcp'

Installs into Claude Code, Codex, Cursor and more

Documentation

FloorSync for founders and their agents

For any business that takes payments on Stripe and runs Meta or GA4 ads, on any server: Node, Python, serverless, or a towers.app floor. A “floor” here means one business: one Stripe account, one Meta pixel, one GA4 property, one key.

Setting up as an agent? Read /agent-install.md: the whole setup in plain HTTP calls, dry run first, no installs.

Try it now, no key

An agent can POST any Stripe event to /api/v1/demo/events/stripe with no key (a few calls an hour). It gets back what Meta and GA4 would get, the ledger line, and a short note for its owner with what’s needed before live sends and the sign-up link. Nothing is sent or kept. On MCP, it’s the try_stripe_event tool.

Machine-readable: /llms.txt, /openapi.json, /api/capabilities and the MCP endpoint POST /api/mcp.

How-tos: Stripe sales missing in Meta or GA4? A 5-step check, Stripe to Meta Conversions API, Stripe to GA4. Compared: Tracklution, Admaxxer, Stape.

1. The owner sets it up

  1. The founder enters their email (no password, no card) and opens the emailed link: the key (fs_…) and a prompt for their agent are on the page it opens.
  2. They save the Meta pixel ID with a Conversions API access token, and the GA4 Measurement ID with a Measurement Protocol API secret.
  3. They start the plan: $29 a month per business, 7 days free. Agents can’t buy it; until it starts, live calls answer 402 and dry runs still work.

2. Your server forwards each Stripe event

In your Stripe webhook handler, after it has checked Stripe’s signature and handled the event, pass the event on. Every POST sends your key and the body’s SHA-256 (the CDN asks for it). Keep the key on your server: Stripe can’t point its webhook straight at FloorSync, because it can’t add these headers.

No webhook yet? In Stripe’s dashboard, Developers → Webhooks, add an endpoint on your own server for checkout.session.completed, invoice.paid, invoice.payment_failed, customer.subscription.deleted and charge.refunded. On a towers.app floor, the /api/stripe-events route already gets them.

Node (any framework, serverless too):

import { createHash } from 'node:crypto'

// event: what stripe.webhooks.constructEvent(...) returned
const text = JSON.stringify(event)
await fetch('https://floorsync.towers.app/api/v1/events/stripe', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Floor-Key': FLOORSYNC_KEY,
    'x-amz-content-sha256': createHash('sha256').update(text).digest('hex'),
  },
  body: text,
  signal: AbortSignal.timeout(10000),
}).catch(() => {}) // never let FloorSync slow down your own webhook

Python:

import hashlib, os, requests

# payload: the raw body bytes you passed to stripe.Webhook.construct_event(...)
try:
    requests.post('https://floorsync.towers.app/api/v1/events/stripe', data=payload, timeout=10, headers={
        'Content-Type': 'application/json',
        'X-Floor-Key': os.environ['FLOORSYNC_KEY'],
        'x-amz-content-sha256': hashlib.sha256(payload).hexdigest(),
    })
except requests.RequestException:
    pass

Add ?dry_run=1 to get the exact Meta and GA4 payloads back, with GA4’s validation answer, and send nothing.

3. What goes where

Stripe eventMetaGA4
checkout.session.completed (paid)Purchase, event_id = session idpurchase, transaction_id = session id
checkout.session.completed ($0 trial)StartTrialstart_trial
invoice.paid (subscription_cycle)Purchase, event_id = invoice id, system_generatedpurchase with the stored client_id
invoice.payment_failed, refunds, subscription deletedLedger only

Each event id counts once. Email and phone go to Meta SHA-256 hashed only; GA4 gets no personal data. A failed send is kept in the sync log and retried each hour, 3 tries in all. Your first GA4 send is checked on GA4’s validation server first.

4. Attribution at checkout (recommended)

When your server creates a Checkout session, post the buyer’s ids so the sale and its renewals match the right visitor:

POST /api/v1/attribution
{"checkout_session_id": "cs_…", "fbp": "fb.1.…", "fbc": "fb.1.…", "client_id": "1234567890.1234567890",
 "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 …", "page_url": "https://yourbusiness.com/pricing/"}

5. Read the ledger

GET /api/v1/ledger?days=7
{"mode": "live", "days": 7, "currency": "USD", "revenue": 10,
 "counts": {"new_paid": 1, "trials": 0, "renewals": 0, "refunds": 0, "failed_payments": 0, "cancellations": 0},
 "conversions": {"meta": {"delivered": 1, "failed": 0, "skipped": 0}, "ga4": {"delivered": 1, "failed": 0, "skipped": 0}},
 "spend": null, "spend_source": null, "revenue_per_spend": null,
 "plan_active": true, "health": [], "note": "Nothing to fix."}

An example answer. Spend comes from Meta Insights when the owner saves an ads_read token, or from POST /api/v1/spend.

Errors

  • 401 no_key / bad_key: ask the owner for the current key.
  • 402 payment_required: nothing was sent; give the owner the owner_url.
  • 400 bad_request: the message says what to fix.