Investment sessions on mobile
Send an investor from your iOS or Android app through Wefunder's hosted checkout and get them back into your app afterwards. Investment sessions are in beta: any app installed on the company or syndicate that runs the offering can create them with write:investment_sessions, a scope you tick when you create the app.
The shape is the same as on the web: your backend mints a session, the investor completes checkout on a Wefunder page, and Wefunder sends them to your success_url (or cancel_url). On a phone, two things differ: the page must open in a system browser sheet, and the return URLs are usually links that reopen your app.
Supported surfaces
The hosted page asks for a Wefunder password, a passkey, Google or Apple sign-in, and bank details. Open it only where your app cannot read or change what the investor types.
| Platform | Surface | Status | Notes |
|---|---|---|---|
| iOS | ASWebAuthenticationSession | Recommended | Shares Safari's cookies (keep prefersEphemeralWebBrowserSession = false), so an investor already signed in to Wefunder in Safari skips login. Closes itself when the page navigates to your callback. |
| iOS | SFSafariViewController | Supported | Has its own cookie store, so investors sign in again. You must catch the return through a universal link or custom scheme handler and dismiss the controller yourself. |
| Android | Custom Tabs (androidx.browser) | Recommended | Shares the default browser's cookies. Return through an app link or custom-scheme intent filter. |
| React Native / Expo | expo-web-browser openAuthSessionAsync | Supported | Uses ASWebAuthenticationSession on iOS and Custom Tabs on Android. |
| Android | WebView | Not yet | Wefunder's own sign-in loads. One Google sign-in completed inside it in our test on Android 15, but Google's OAuth policy disallows embedded web views, so that path can be blocked at any time; checkout inside it is unfinished on our side. See below. |
| iOS | WKWebView, UIWebView | Not yet | Untested. The same unfinished pieces as Android apply. |
| Any | In-app browsers of social or messaging apps | Not supported | You control nothing about them: no return handling, no cookie policy, no way to finish the flow. |
Embedded web views are not finished yet. The system sheets above are the path we have tested end to end; a web view is missing three things, and until they are in place treat it as unsupported and tell us what breaks:
- The return.
ASWebAuthenticationSession(andexpo-web-browser's auth session) closes itself when the page navigates to yoursuccess_urlorcancel_url.SFSafariViewControllerand Custom Tabs hand the navigation to your universal-link or intent filter and you dismiss them, as the table says. A web view gives you neither: your app has to catch that navigation itself (shouldOverrideUrlLoadingon Android,decidePolicyForon iOS) and dismiss the view, or a finished checkout stays covered by the web view. - Google sign-in. Google's OAuth policy does not allow its sign-in inside embedded web views. It worked in one test of ours; that is not a permitted or dependable path, and a compliant flow hands sign-in to the system browser or a native sheet.
- Sign-in starts from scratch. A web view shares no cookies with the system browser, so an investor who is already signed in to Wefunder in their browser signs in again, often without the passkey or password manager they normally use.
- Bank linking. Bank connections can redirect to the bank's own site or app, which a web view does not hand off reliably.
One thing to weigh before you build on a web view at all: the app that hosts it can read and inject into the page, so the investor's Wefunder password and bank login pass through a surface you control. The sheets keep that out of your app.
Return URLs
success_url and cancel_url must belong to your app. On mint, each must be one of:
- an
httpsURL on the origin (scheme, host and port) of one of your app's registered OAuth redirect URIs. This is how you use an iOS universal link or an Android app link; - a URL in a custom scheme that one of your redirect URIs uses, such as
com.partner.app://checkout/done. Use the hierarchical form (scheme://...orscheme:/...): Wefunder appends query parameters tocancel_url, whichscheme:opaqueURLs cannot carry; httpon a loopback host (localhost,127.0.0.1,[::1]) that one of your redirect URIs uses, on any port, for local development.
Anything else, including an origin or scheme that only another app registered, returns 422 validation_error and no session is created. Redirect URIs are the ones on your app's keys in the developer portal; separate several with spaces or new lines. The sandbox and live keys of one app share the list, so a development URI added to your sandbox key is also accepted on live mints; remove it when you no longer need it.
Registering a redirect URI also lets OAuth send authorization codes there, so register only origins and schemes you control.
After the investor confirms, the checkout's confirmation step sends them to success_url as given. On cancel or an ended session, Wefunder appends session_id (the is_ id) and reason (user_canceled, user_left, canceled or expired) to cancel_url.
ASWebAuthenticationSession watches a single scheme (or, on iOS 17.4+, a single https host and path). Point both URLs at the same place and tell them apart with a query parameter:
success_url = com.partner.app://wefunder-return?result=success,
cancel_url = com.partner.app://wefunder-return?result=cancel.
Flow
-
Your backend mints the session. Never mint from the app: the token that can mint sessions must stay on your server.
curl -X POST https://api.wefunder.com/partner/investment_sessions \
-H "Authorization: Bearer $WEFUNDER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"investment_session": {
"spv_id": "ofr_abc123",
"email": "[email protected]",
"success_url": "com.partner.app://wefunder-return?result=success",
"cancel_url": "com.partner.app://wefunder-return?result=cancel"
}
}'Return
data.id(theis_id) anddata.attributes.url(the hosted page) to the app. -
The app opens
urlin a system browser sheet (samples below). -
The app handles the return. Treat the return URL as a hint, not proof: anything on the device can open your custom scheme.
-
The app asks your backend for the outcome. Your backend knows from the
investment_session.completed,.canceledand.expiredwebhooks, or readsGET /partner/investment_sessions/{id}and checksstatus. There is no session status endpoint the app can call directly; polling your own backend keeps the token server-side.
iOS (Swift)
import AuthenticationServices
import UIKit
final class WefunderCheckout: NSObject, ASWebAuthenticationPresentationContextProviding {
private var authSession: ASWebAuthenticationSession?
/// `sessionURL` is `data.attributes.url` from your backend.
func start(sessionURL: URL, completion: @escaping (_ returnURL: URL?) -> Void) {
let handler: ASWebAuthenticationSession.CompletionHandler = { url, _ in
// nil when the investor closed the sheet. Either way, ask your backend for the outcome.
completion(url)
}
let session: ASWebAuthenticationSession
if #available(iOS 17.4, *) {
session = ASWebAuthenticationSession(url: sessionURL, callback: .customScheme("com.partner.app"), completionHandler: handler)
} else {
session = ASWebAuthenticationSession(url: sessionURL, callbackURLScheme: "com.partner.app", completionHandler: handler)
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = false // share Safari's Wefunder login
session.start()
authSession = session
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.first ?? ASPresentationAnchor()
}
}
With a universal link instead of a custom scheme, use .https(host: "partner.example", path: "/app/wefunder-return") (iOS 17.4+, requires the associated domain).
Android (Kotlin)
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
fun openWefunderCheckout(context: Context, sessionUrl: String) {
CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
.launchUrl(context, Uri.parse(sessionUrl))
}
Register the return target on the activity that should resume:
<activity
android:name=".WefunderReturnActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="com.partner.app" android:host="wefunder-return" />
</intent-filter>
</activity>
For an app link, use android:scheme="https" with your host and path and android:autoVerify="true". In the activity, read intent.data, then ask your backend for the session's status.
React Native (Expo)
import * as WebBrowser from "expo-web-browser"
export async function openWefunderCheckout(sessionUrl: string) {
const result = await WebBrowser.openAuthSessionAsync(sessionUrl, "com.partner.app://wefunder-return")
// result.type is "success" (with result.url) when the page reached the return URL,
// or "cancel" / "dismiss" when the investor closed the sheet.
return result
}
Bare React Native apps can use a library that wraps the same system APIs; avoid react-native-webview for checkout.
Testing
Mint sessions with your sandbox key, which lands the investor on your sandbox realm (see Sandbox). Test both returns, completion and cancel, on a real device: simulators and emulators do not always hand a custom scheme or app link back to your app the way a phone does.