Skip to content

Public API ​

Run funnel sessions, capture user information, and connect a purchase to your app. For funnel UI, use funnelSdkService from @funnelsgrove/runtime; the shared @funnelsgrove/payments package handles checkout surfaces and confirmation.

Authentication ​

Use the project SDK publishable key from Project Settings. This key may be included in browser or mobile code. It identifies the project; your app must still verify the user's identity before looking up access by email.

http
X-SDK-Publishable-Key: <project-sdk-publishable-key>

All public HTTP paths below use this base URL:

text
https://sdk-api.funnelsgrove.com

Backend operations using a secret key or an API key belong to the API reference.

Choose your task ​

I want to…Start here
Connect a purchase to my appVerify access and track login
Build checkout or an upsellPayments
Save user identity and progressRuntime methods and email capture
Upload a photoDirect photo upload
Upload a voice note, screenshot, or other temporary fileTemporary file upload
Handle a failed requestErrors & retries

Runtime Methods ​

funnelSdkService exposes these public methods:

MethodPurpose
bootstrapUserStart or restore a funnel user session.
captureEmailSave the user's email during the funnel.
getCurrentUserLoad the current funnel user.
getUserSubscriptionStatusRead subscription status for already-published bundles; new code uses verify subscription status.
claimUserSubscriptionClaim a paid user for already-published bundles; new code uses track App Login.
updateFunnelUserSave user profile, progress, attribution, or subscription data.
preparePhotoUploadCreate a short-lived direct-upload URL. Prefer uploadPhotoFile unless implementing a custom uploader.
uploadPhotoFileUpload a user photo directly to R2 through a short-lived signed URL.
uploadPhotoDeprecated. Base64 upload for already-published bundles; new code uses uploadPhotoFile.
prepareTempUploadCreate a short-lived upload URL for a temporary file. Prefer uploadTempFile unless implementing a custom uploader.
uploadTempFileUpload an image or audio file directly to temporary storage; the file is deleted after 7 days.
createHostedCheckoutSessionCreate a hosted checkout session.
createSubscriptionCheckoutStart embedded subscription checkout.
createOneTimeCheckoutStart embedded one-time checkout.
updateSubscriptionCheckoutPlanChange the plan in an active subscription checkout.
verifySubscriptionCheckoutVerify subscription checkout completion.
chargeOneClickPaymentCharge a saved customer payment method.
updateSubscriptionPerform a supported subscription action.
listSubscriptionsList subscriptions for a user or Stripe customer.

getUserSubscriptionStatus, claimUserSubscription and uploadPhoto keep working for already-published bundles; use the replacements above in new code.

Current identity ​

User and subscription reads keep the requested user ID, including an ID with historical merge metadata. They do not collect other profiles’ subscriptions. Email capture returns the selected identity separately; persist that ID for subsequent requests. This switch leaves previous history and billing ownership with their recorded owners. Subscription management requires the selected owner.

Email Capture ​

captureEmail submits the version loaded by the visitor through POST /sdk/public/users/email-capture. Direct callers supply that version's funnelVersionId; the typed runtime supplies it from its configuration. A newer publication does not replace the version on a new capture.

A well-formed version ID that is absent or belongs to another funnel returns HTTP 404 with Funnel version not found, before answers, identity, consent or events change. Use a version belonging to the requested funnel; scoped preview versions are supported. Existing capture retries retain the original event.

For the Meta email conversion, analyticsMetadata.metaLeadEventName accepts Lead or CompleteRegistration; missing or unsupported values resolve to Lead. The runtime supplies the published project setting and overrides that field in caller metadata. Direct HTTP callers supply the matching name used by their browser Pixel; the API does not load the current project setting for this field. The first accepted capture stores the name for later delivery and retries.

When a lifecycle email returns a visitor to the funnel, the runtime passes the opaque signed email_return value to bootstrapUser. Invalid, expired, or repeated values do not interrupt bootstrap.

Payments V2 ​

Follow the Payments guide for published offer selection, subscription and one-time checkout, verification, upsells, and subscription management. Current payment requests use V2.

Direct Photo Upload ​

Use uploadPhotoFile to prepare a short-lived upload URL and send the Blob directly to object storage. For a custom uploader, call preparePhotoUpload first (POST /sdk/public/uploads/photos/prepare) with camelCase metadata.

Preparation has a 25-second client budget; the direct upload has a separate 60-second budget. Both accept an AbortSignal and execute once. A timeout does not prove that the object was never written. See request budgets.

Temporary File Upload ​

Use temporary storage for visitor files the funnel needs for a short time: voice recordings, photos, chat screenshots, cover images. The browser sends the bytes directly to object storage; your funnel keeps only the returned URL. Files are temporary: the storage lifecycle policy deletes them 7 days after upload.

In funnel code, call apiService.uploadTempFile(file, { purpose }) (@funnelsgrove/runtime 0.30.0 or later); it returns { key, url, contentType, sizeBytes, expiresAt }. funnelSdkService.uploadTempFile is the lower-level equivalent that takes { file, purpose, userId } and returns publicUrl. Store url and expiresAt in answers, never the file bytes or a data URL.

Allowed files ​

KindContent typesMaximum size
Imageimage/jpeg, image/png, image/webp, image/heic, image/heif, image/gif10 MB
Audioaudio/webm, audio/ogg, audio/mp4, audio/mpeg, audio/wav, audio/x-m4a, audio/aac25 MB

Content-type parameters such as audio/webm;codecs=opus are accepted; the upload is signed for the base type. A file without a type is rejected.

Prepare request ​

http
POST /sdk/public/uploads/temp/prepare
Content-Type: application/json
X-SDK-Publishable-Key: <project-sdk-publishable-key>
json
{
  "funnelId": "4bd7f645-e104-4d77-81c1-401dc8ac7d49",
  "userId": "funnel-user-id",
  "contentType": "audio/webm;codecs=opus",
  "sizeBytes": 482113,
  "purpose": "voice"
}
FieldRequiredRules
funnelIdYesUUID of a funnel in the key's project.
userIdNoFunnel user ID, 1–255 characters. Omitted uploads are stored as anonymous.
contentTypeYesOne of the allowed types above; a missing or empty value returns 415.
sizeBytesYesExact byte size of the file, a positive integer.
purposeNoDiagnostic slug matching ^[a-z0-9-]{1,32}$, such as voice or cover.
publishableKeyNoBody fallback for clients that cannot send the header.

Unknown fields are rejected.

Prepare response ​

201 Created:

json
{
  "key": "tmp/sdk-temp-uploads/<workspace>/<funnel>/<user>/20261006_<random>.webm",
  "uploadUrl": "https://<account>.r2.cloudflarestorage.com/...&X-Amz-Signature=...",
  "uploadHeaders": {
    "Content-Type": "audio/webm",
    "Cache-Control": "private, max-age=86400"
  },
  "publicUrl": "https://<public-storage-host>/tmp/sdk-temp-uploads/.../20261006_<random>.webm",
  "contentType": "audio/webm",
  "sizeBytes": 482113,
  "expiresAt": "2026-10-13T10:00:00.000Z"
}

uploadUrl is valid for 5 minutes. expiresAt is 7 days after preparation; treat the file as unavailable after it. Storage removes expired files asynchronously, so a URL may keep working briefly past expiresAt. publicUrl is publicly readable by anyone who has it; its random component is a UUIDv4 (122 random bits).

Upload the bytes ​

Send one PUT with exactly the returned headers and the original bytes. The signature covers Content-Type, Cache-Control, and the declared size, so any other header value or byte count is rejected by storage.

ts
const prepared = await funnelSdkService.prepareTempUpload({
  contentType: file.type,
  sizeBytes: file.size,
  purpose: 'voice',
});
const response = await fetch(prepared.uploadUrl, {
  method: 'PUT',
  headers: prepared.uploadHeaders,
  body: file,
});
if (!response.ok) throw new Error(`Upload failed (${response.status})`);
// Persist prepared.publicUrl and prepared.expiresAt.

Storage accepts browser uploads from funnel origins through the bucket's CORS policy (PUT with Content-Type and Cache-Control headers), the same policy used by direct photo upload.

Errors ​

StatusCodeMeaning
400SDK_INVALID_REQUESTMalformed body, unknown field, invalid purpose, or missing key.
401SDK_UNAUTHORIZEDInvalid publishable key.
404SDK_NOT_FOUNDFunnel outside the key's project.
413SDK_PAYLOAD_TOO_LARGEImage above 10 MB or audio above 25 MB.
415SDK_UNSUPPORTED_MEDIA_TYPEMissing, empty, or unlisted content type.
429—More than 30 preparations per minute from one identified client IP; honor Retry-After. Clients whose IP the edge cannot verify are bounded by the general SDK ingress limits instead.
503SDK_UNAVAILABLEStorage unavailable or the 20-second server budget elapsed.

Preparation has a 25-second client budget and runs once; uploadTempFile gives the PUT a separate 120-second budget. A failed preparation writes nothing, so a new attempt is safe. A timed-out PUT may still have written the object; a retry prepares a new key.

Verify Subscription Status ​

Use the project SDK publishable key with either a known funnelsgrove userId or a verified email. funnelId is optional.

Verify with a known user ID:

http
POST /sdk/public/users/verify_subscription
Content-Type: application/json
X-SDK-Publishable-Key: <project-sdk-publishable-key>

{
  "userId": "u_123"
}

Or verify with an email only after the app has confirmed ownership through OTP or equivalent authentication:

http
POST /sdk/public/users/verify_subscription
Content-Type: application/json
X-SDK-Publishable-Key: <project-sdk-publishable-key>

{
  "email": "verified-user@example.com"
}

The endpoint is read-only. It returns the access flag, a test flag, and the stable public user ID; it never returns the user's email, internal database ID, or subscription records. test is true when the selected active subscription belongs to Stripe test mode.

json
{
  "active": true,
  "test": false,
  "user": {
    "user_id": "u_123"
  }
}

If no matching user has active access, active is false. A known user without active access still has a user.user_id; only an unresolved identity returns user: null. Grant production access only when active is true and test is false.

json
{
  "active": false,
  "test": false,
  "user": null
}

Track App Login ​

Track app login only after the user has verified ownership of the funnel email through OTP or your backend has already proven the app-account ↔ funnelsgrove-user association. Call this endpoint after registration/login regardless of paid access, and persist the returned user.user_id on the authenticated account. Verify paid access separately.

Pass the funnelsgrove user ID in the request body:

http
POST /sdk/public/users/track_login
Content-Type: application/json
X-SDK-Publishable-Key: <project-sdk-publishable-key>

{
  "userId": "u_123",
  "runtimeEnvironment": "test"
}

The endpoint records registration; it does not verify access or receive your app account ID.

runtimeEnvironment is optional. An explicit live or test selects only that user identity. When omitted, the endpoint looks for the live user first and falls back to test only if that user is absent. It never looks up subscriptions. If the user ID is ambiguous across funnels, provide funnelId.

Registration does not require a subscription, payment, or a published funnel. HTTP 200 means the registration is durably queued for background analytics and lifecycle delivery. The compact response is:

json
{
  "user": { "user_id": "your-funnel-user-id" },
  "login": { "tracked": true, "newlyTracked": true, "trackedAt": "2026-09-09T12:00:00.000Z" }
}

Repeat and concurrent requests retain the first event and timestamp and return newlyTracked: false. No subscription is claimed or profile repaired. Use verify_subscription separately for paid access; the response contains no active, activeUntil, subscription, payment, or event history fields.

Acceptance does not wait for analytics or email delivery. An uncertain write may already be committed; a later explicit login reuses the original receipt. See Errors & retries before adding automatic retries.

Retryable Responses ​

See Errors & retries for operation-specific retry rules, cancellation, request budgets, and checkout diagnostics.

Build. Launch. Grow.