Org Web Adapter

hungryroot/popsicle/appsflyer-onelink-cookbook-shares.org

ID
673bfe3b-7ca6-4a14-9984-7f649bdfe676

AppsFlyer OneLink for Cookbook Shares

Context

Cookbook share links today (~cookbooks/views.py:787-792~, ~cookbooks/sqids.py~, ~cookbooks/urls_v3.py:30~) are plain web URLs of the form ~/api/v3/cookbooks/<slug>/~. If a recipient without the app installs it and signs up later, we lose install attribution back to the original sharer --- there's no bridge from "opened a web link" to "installed the app."

Raised by Wally Chang in [[https://hungryroot.slack.com/archives/C0B4QA7Q05A/p1782918794864129][#acquisition-attribution, 2026-07-01]]. AppsFlyer is already used for mobile attribution tracking generally, but a full-repo search (~appsflyer~, ~onelink~, ~af_channel~, ~af_sub~, ~media_source~, ~branch.io~, etc.) confirms *zero* existing AppsFlyer integration in this backend --- no SDK client, no OneLink calls, nothing. Generating the OneLink itself is the missing piece.

Two options were discussed in the thread:

1. Generate the link client-side --- each app SDK calls AppsFlyer at share time; web would need its own equivalent to preserve attribution through app-store installs.

2. Generate the link server-side --- one source of truth, all clients (web, iOS, Android) call the BE for the link. *Preferred* (Wally + Tommy, confirmed in thread).

This doc covers backend scope only: a generic attribution-link service, with Cookbooks as the first (and currently only) caller.

Related existing spec

~exp-242-shareable-cookbooks.org~ (this directory) already designs referral attribution for shared user cookbooks, for the pure-web case: it reuses ~Customer.referral_code~, the ~/r/<code>~ redirector, and ~PromoMiddleware~ to credit the sharer when a recipient signs up + first-orders via ~CookbookSharedDetailView~'s ~owner_referral_code~ field. That spec's ~visibility~ field is already implemented on ~Cookbook~ (~cookbooks/models.py:82~); its ~owner~/~share_token~ fields are not yet added.

This AppsFlyer work is complementary, not a replacement: it adds *install attribution* (the app-store detour) on top of the same referral-code rails that spec already covers for direct web landings. A OneLink's web fallback should point at the existing/planned cookbook share URL with ~referral_code~ preserved, so a recipient who doesn't install still flows through the exact mechanism ~exp-242-shareable-cookbooks.org~ already designed.

Existing Rails (verified, to be reused)

- ~Customer.referral_code~ (~app/models/customer.py:175~) --- 8-char unique hash, doubles as a ~PromoCode~ (~type=REFERRAL_DISCOUNT~). Generated via ~reset_customer_referral_code()~ in ~app/services/api/pricing.py:48-80~, called from ~create_customer()~ (~app/services/api/customer.py:110~).

- ~/r/<code>~ redirector (~app/views/web.py:388-407~) --- stamps ~request.session["promo"]~ + a ~referral_code~ cookie.

- ~PromoMiddleware~ (~app/middleware.py:178-211~) --- redeems ~session["promo"]~ via ~api.redeem_promo_code()~ pre-checkout. This is what makes web-fallback attribution "just work" per Wally's comment in the thread --- no new redemption logic needed.

- Cookbook favorites sharing already surfaces ~referral_code~ in its response (~cookbooks/services.py:361~, ~cookbooks/datatypes.py:48~, ~cookbooks/serializers.py:258-266~) --- currently only for Favorites/User (Mapping-backed) cookbooks, not marketing cookbooks.

- Segment event pipeline (~app/services/segment_tracking.py~, ~app/services/async_helpers.py~, ~app/celery_tasks/tasks.py:208-241~) --- existing pattern for a service wrapping an outbound third-party call (sync helper -> Celery task -> external API). Not directly reusable here since the OneLink URL must be returned synchronously in the share response, unlike fire-and-forget Segment events.

Design

Generic Attribution Link Service

New module (exact location/name TBD at implementation time, e.g. ~attribution/onelink.py~ or similar --- not nested inside ~cookbooks/~) wrapping the [[https://dev.appsflyer.com/hc/reference/onelink-v2-create-link][AppsFlyer OneLink v2 create-link API]].

Interface should be feature-agnostic:

- Takes a fallback/deep-link URL, campaign metadata, and an optional referral code.

- Returns the generated OneLink URL (or raises/returns an error the caller can handle --- see Open Questions).

- Does not know about ~Cookbook~ or any cookbook-specific concept. Cookbooks is the first caller; the shape should allow other features (product share, invite links) to adopt it later without rework.

Cookbook Integration (first caller)

- Wire into the favorites/user cookbook share flow (~cookbooks/views.py~ ~CustomerFavoritesView.post()~ and/or ~CookbookDetailView~ path) so the share response returns an AppsFlyer link instead of, or alongside, the raw public cookbook URL.

- The OneLink's web fallback must point at the existing public cookbook URL (~/api/v3/cookbooks/<slug>/~, or the future ~/c/<token>~ share URL from ~exp-242-shareable-cookbooks.org~), with ~referral_code~ preserved as a query param/cookie-stamping trigger so the existing referral-capture plumbing keeps working for users who land on web instead of installing the app.

Configuration

- Add AppsFlyer config (dev key, OneLink template ID) to settings/secrets following the existing per-environment secrets pattern (~local/secrets.json~ + environment-specific settings components).

Explicitly Out of Scope

- Client-side (mobile SDK) link generation --- rejected in favor of the centralized backend approach.

- Wiring any feature besides Cookbooks --- the service should be *capable* of reuse, but no other caller is being built now.

- Building out ~exp-242-shareable-cookbooks.org~'s ~owner~/~share_token~ fields --- separate effort; this doc assumes whichever share URL exists at implementation time (current sqid-based slug, or the future token-based one).

Blocking Dependency

AppsFlyer dev key + OneLink template ID are not confirmed to exist yet for backend use. Needs sourcing from marketing/growth (whoever administers the AppsFlyer account) before implementation can start.

Open Questions

- Exact interface/method signature for the attribution-link service.

- Whether the OneLink create-link call is made synchronously in the share request path (likely, since the link must be returned in the response) vs. some caching/pre-generation strategy.

- Param naming for AppsFlyer's ~af_channel~/~media_source~ equivalents in our call.

- Error handling/timeout behavior if the AppsFlyer API is slow or down during a share request (fall back to the plain web URL rather than failing the share entirely?).

Files Likely Affected

- New attribution-link service module (location TBD).

- ~cookbooks/views.py~ --- share response wiring (~CustomerFavoritesView~, ~CookbookDetailView~).

- Settings/secrets components --- AppsFlyer dev key + OneLink template ID.

- Tests for the new service (unit, mocking the AppsFlyer API call) and for the cookbook share response shape.

Verification

- Unit tests for the attribution-link service: correct OneLink request payload built from inputs; API error handling.

- Cookbook share endpoint tests: response includes the AppsFlyer link; referral code is preserved in the fallback URL.

- Manual: generate a share link, confirm it resolves via AppsFlyer's dashboard/test tools, confirm the web fallback lands on the correct cookbook page with the referral code intact.

Future / Out of Scope

- Additional callers of the attribution-link service (product share, invite links, etc.).

- Extending to marketing cookbooks (not just Favorites/User cookbooks).