Python SDK

Track revenue, signups, product usage, and user identity from your Python backend using the official Ripples SDK.

Server-side tracking. Use the Python SDK to send events from your backend — ideal for tracking payments processed server-side, webhooks, or any event that shouldn't go through the browser.

Install

pip install ripples

Add your secret key to your environment:

RIPPLES_SECRET_KEY=priv_your_secret_key

Your secret key can be found in your Ripples dashboard under Settings → API Keys.

Quick start

from ripples import Ripples

ripples = Ripples()

# Track a payment
ripples.revenue(49.99, "user_123")

# Track a signup
ripples.signup("user_123", email="[email protected]")

# Track product usage
ripples.track("created a budget", "user_123", area="budgets")

# Identify / update a user
ripples.identify("user_123", email="[email protected]", signed_up_at=user.created_at)

Events are batched in memory and sent automatically when the process exits.

Track revenue

Call revenue() whenever a payment is processed on your server:

ripples.revenue(49.99, "user_123")

Pass extra context as keyword arguments. Any key that isn’t a known field becomes a custom property automatically:

ripples.revenue(49.99, "user_123",
    email="[email protected]",
    currency="EUR",
    transaction_id="txn_abc123",
    name="Pro Plan",
    plan="annual",       # custom property
    coupon="WELCOME20",  # custom property
)

Refunds are negative revenue. Give each refund its own transaction_id (your refund’s id, or the payment’s id plus a suffix) and point at the payment with original_transaction_id. One call per refund, partial refunds included:

ripples.revenue(-29.99, "user_123",
    transaction_id="txn_abc123:refund:1",
    original_transaction_id="txn_abc123",
)

A refund sent under the payment’s own transaction_id is still recorded, keyed by the payment id and the refund’s time, so a retry of the same call is not counted twice. Its own id is still the safer contract: two refunds sent in the same second under the payment’s id would collapse into one.

Pass timestamp= to record a payment or refund when the money actually moved, e.g. when importing history.

Revenue for a user Ripples has not seen yet is kept on that user: Ripples creates the person under your user_id, and their later identify() or signup() (from your server or the browser) lands on the same person and still records their signup. So identify() and revenue() for a new user can go in the same flush, in either order. Pass signed_up_at to that identify() so the signup is dated correctly.

Parameters

Parameter Type Description
amount required float Revenue amount in your default currency. Use negative values for refunds.
user_id required str Your internal user ID.
**attributes optional Any Additional properties: email, currency, transaction_id, name, plus any custom keys.

Track subscriptions (MRR)

Call subscription() when a subscription is created, upgraded, downgraded, or canceled. This powers the MRR metric on your dashboard.

Stripe / Paddle users: MRR is tracked automatically via the integration. Only use this method if you use a payment provider without a native Ripples integration.

# User subscribes to Pro Monthly ($29/mo)
ripples.subscription("sub_123", "user_456", "active", 29.00, "month",
    name="Pro", currency="EUR")

# User upgrades to Business Annual ($499/yr)
ripples.subscription("sub_123", "user_456", "active", 499.00, "year",
    name="Business")

# User cancels
ripples.subscription("sub_123", "user_456", "canceled", 0)

When importing history, pass the real dates. Without canceled_at a cancel is dated the day Ripples receives it, which puts every historical churn on the import day:

ripples.subscription("sub_123", "user_456", "canceled", 29.00, "month",
    started_at=datetime(2021, 1, 5), canceled_at=datetime(2023, 5, 10))

Trials (trialing) are not counted in MRR: they show beside it until the subscription turns active. past_due still counts, since the payment is owed.

Parameters

Parameter Type Description
subscription_id required str A stable identifier for the subscription. Must be the same across updates and cancellations.
user_id required str Your internal user ID.
status required str One of: active, canceled, past_due, trialing, paused.
amount required float Amount per billing cycle (e.g. 29.00). Pass 0 when canceling.
interval optional str Billing interval: "month" (default), "year", "week", or "day".
currency optional str 3-letter currency code (e.g. "EUR"). Defaults to project currency.
name / plan optional str Plan name shown in the dashboard.
interval_count optional int Billing frequency multiplier (e.g. 3 for quarterly). Default: 1.
started_at, canceled_at optional datetime | str When the subscription started and was canceled. Pass them when importing history.

Track product usage

Call track() only for significant product usage — actions that prove a user got real value (created a budget, sent a message, invited a teammate). This is not a generic event log like PostHog or Mixpanel: do not send pageviews, banner impressions, button clicks, or “viewed X” events. Every track() call feeds the Activation dashboard, so noise here pollutes your funnel.

# Significant product usage — user created something
ripples.track("created a budget", "user_123", area="budgets")

# User added their 10th transaction — we consider this their activation moment
ripples.track("added transaction", "user_123",
    area="transactions",
    activated=True,  # marks THIS occurrence as the activation moment
)

How it works. Ripples auto-detects activation moments (first occurrence per user per action), computes adoption rates by product area, and correlates usage patterns with retention and payment.

Parameters

Parameter Type Description
action_name required str What the user did. Be specific: 'created a budget', not 'budgets'.
user_id required str Your internal user ID.
area optional str Product area this action belongs to (e.g. 'budgets', 'reports'). Groups actions in the dashboard.
activated optional bool Set to True on the specific occurrence when activation happens for this user. This does not mark the event type as an activation event — it marks this particular moment as when the user activated. For example, 'sent message' is a regular event, but when a user sends their 10th message you may consider that their activation moment and send that occurrence with activated=True.

Track milestones

A milestone is a moment in a person’s lifecycle that you count once per person, date by when they first reached it, and attribute to how they were acquired: mql, sql, demo_requested, demo_held, email_verified, onboarding_completed. It is not product usage, so it never feeds activation or retention. See Track Milestones for the rule of thumb and the recommended names.

# A known user, dated by when it happened (the CRM's stage-change time)
ripples.milestone("mql", str(user.id), timestamp=changed_at, source="hubspot")

# A lead with no account yet: the bound browser visitor or a verified email
ripples.milestone("demo_requested", email=form.email, company_size=form.size)

Parameters

Parameter Type Description
name required str snake_case (^[a-z][a-z0-9_]{0,63}$). Built-in moments (signed_up, activated, first_paid…) and billing events (payment_failed, refunds, renewals) are refused.
user_id optional str | None Your user. Strongest identity.
email= optional str | None For a lead with no account. Merged into the account when you later identify() the user with the same email. Send it only once verified.
visitor_id= optional str | None A browser visitor id. Beats the one bound with set_visitor_id().
timestamp= optional datetime | str | None When it happened. Omit for "now".
**properties optional Any Any other keyword is a property. Every property is stored.

At least one of user_id, a visitor (set_visitor_id() or visitor_id=) or email= is required; a milestone naming nobody is not sent and is passed to on_error.

Track signups

Call signup() when a new user registers:

ripples.signup("user_123",
    email="[email protected]",
    name="Jane Smith",
    referral="twitter",  # custom property
    plan="free",          # custom property
)

Django example

from ripples import Ripples, set_visitor_id, visitor_id_from_cookies

def register(request):
    # Bind the browser's visitor first so the signup keeps its traffic source
    set_visitor_id(visitor_id_from_cookies(request.COOKIES))

    user = User.objects.create_user(...)

    ripples = Ripples()
    ripples.signup(str(user.id),
        email=user.email,
        name=user.get_full_name(),
    )
    ripples.flush()

    return JsonResponse({"id": user.id})

Flask example

from ripples import Ripples, set_visitor_id, visitor_id_from_cookies

@app.route("/register", methods=["POST"])
def register():
    set_visitor_id(visitor_id_from_cookies(request.cookies))

    user = create_user(...)

    ripples = Ripples()
    ripples.signup(str(user.id),
        email=user.email,
        name=user.name,
    )
    ripples.flush()

    return jsonify({"id": user.id})

Keep the visitor ID with the signup. The Python SDK does not read cookies on its own: call set_visitor_id(visitor_id_from_cookies(...)) in the request that handled the signup form, or pass visitor_id= on the call. If the account is created later (email confirmation link, Celery task), store the ID at form time and pass that one; the cookie in the confirming browser is a different visitor. Without it the signup is attributed to Direct. See Preserving attribution.

Identify users

Call identify() to update user traits at any time — on login, plan change, profile update, etc.:

ripples.identify("user_123",
    email="[email protected]",
    name="Jane Smith",
    avatar_url="https://example.com/avatars/jane.jpg",
    signed_up_at=user.created_at,
    company="Acme Inc",  # custom property
    role="admin",          # custom property
)

Always pass signed_up_at from server-side calls. This method can be the FIRST time Ripples ever hears about a user — e.g. a nightly job, a login handler, or any user who never went through a client-side signup. When that happens, Ripples treats the call as a brand-new signup and, with no signed_up_at, dates it "now" instead of the user's real account age. This corrupts your signups chart and retention cohorts. There's no downside to always sending it: it only ever moves the signup date earlier, never later, so it's safe to include on every call.

Properties

Key Type Description
email optional str User's email address. Shown in the visitor profile and used for Stripe revenue attribution.
name optional str Display name shown in the visitor profile.
avatar_url optional str URL to the user's avatar image. Displayed in the visitor profile. Also accepted as avatar.
signed_up_at optional, but always recommended server-side str | datetime ISO 8601 date (or datetime) of when the user actually signed up (e.g. user.created_at). If this is the first identify() call Ripples has ever seen for this user, it's synthesized into a Signup event — omit this and it gets dated "now" instead of the real signup date.
visitor_id optional str The browser visitor UUID from the _rpl_vid cookie. Accepted by signup() and identify(). Links the user to the anonymous session that brought them in, so they inherit its channel, source and campaign. Bind it per request with set_visitor_id(visitor_id_from_cookies(request.COOKIES)), or pass it explicitly from confirmation handlers and background tasks. See Preserving attribution.
[custom] optional str | int | float Any additional traits you want to attach to the user (e.g. plan, company, role).

Track companies

For B2B products: say which company a user works in with group(). Call it at signup, when a user joins or switches company, and when the company’s traits change:

ripples.group(user.id, team.id,
    name=team.name,   # shown instead of the id
    plan="business",
    seats=25,
)

# No company needed on later calls: this counts for team.
ripples.track("created a report", user.id)

Ripples remembers who works where, so nothing has to be passed on each call. Traits merge into what the company already has. Pass None as the user to update a company’s traits only, e.g. from a nightly sync. Use your own id for the company, never its name. See Track Companies for how it works.

Error handling

By default, errors during flush are swallowed so your app is never disrupted by a Ripples outage. Use on_error to log them:

import logging

ripples = Ripples(on_error=lambda e: logging.warning(f"Ripples error: {e}"))

You can also catch errors explicitly:

from ripples import RipplesError

try:
    ripples.revenue(49.99, "user_123")
    ripples.flush()
except RipplesError as e:
    # Log or handle gracefully — never block the user flow
    logging.warning(f"Ripples error: {e}")

Configuration

The SDK reads RIPPLES_SECRET_KEY from your environment automatically. You can also pass it explicitly:

ripples = Ripples("priv_explicit_key",
    base_url="https://your-domain.com/api",  # self-hosted
    timeout=10,  # seconds (default: 3)
)

The self-hosted URL can also be set via env:

RIPPLES_URL=https://your-domain.com/api

Configuration options

Option Default Description
base_url https://api.ripples.sh API endpoint. Override for self-hosted deployments.
timeout 3 HTTP read timeout in seconds.
connect_timeout 2 HTTP connection timeout in seconds.
max_queue_size 100 Auto-flush when the queue reaches this size.

Flush manually

Events are batched and sent automatically when the Python process exits (via atexit). For long-running processes, web servers, or CLI scripts, call flush() explicitly:

ripples.flush()

Web frameworks. In Django or Flask, call flush() at the end of each request (or use middleware) to ensure events are delivered promptly. The auto-flush on exit works best for CLI scripts and short-lived processes.

Queue workers. A Celery, RQ or Dramatiq worker never exits between tasks, so its events wait until 100 have queued up, and are lost if the worker is killed first. Call flush() at the end of every task, or create the client with max_queue_size=1 to send each call as it is made.

Custom HTTP client

Subclass and override _post() to use httpx, aiohttp, or any other HTTP library:

class MyRipples(Ripples):
    def _post(self, path: str, data: dict) -> None:
        # your custom implementation using httpx, etc.
        pass

Requirements

  • Python 3.9+
  • requests