Mobile App Verification
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Mobile App Verification
Verify Discord users inside a native mobile app (React Native, Flutter, native iOS/Android). The website widget is a browser popup and does not work in a native app, so native apps use an in-app browser plus a result endpoint instead. There is no separate "mobile widget" to install — native verification reuses the same OAuth flow, the same signed token, and the same result delivery as the website widget.
Before you start
Native apps have no browser origin, so the allowed-origins list that protects the website widget can't apply to them. To keep this safe, native verification is an explicit per-server opt-in.
Go to Dashboard → Widget → Native (mobile) verification and turn it on. The widget itself must also be enabled. You can run the website widget and native verification at the same time — the toggle is additive and does not affect your browser widget.
Because native callers skip the origin check, only enable this if you trust the apps that use your server slug.
How it works
Your app creates a random pollId for each verification attempt — letters, numbers, dashes and underscores, 8 to 128 characters (for example, 32 random bytes encoded as base64url).
Your app opens this URL in an in-app browser (Chrome Custom Tabs on Android, SFSafariViewController on iOS, or the Expo WebBrowser):
restorehub.net/verify/YOUR-SLUG?mode=native&pollId=YOUR-POLL-ID
Use mode=native, include the pollId, and do not pass an origin parameter. YOUR-SLUG is your server's slug (or its numeric Discord guild ID).
The user signs in with Discord in that browser. The flow ends on a "Verification complete — you can return to the app" page.
Your app polls the result endpoint until it gets a response:
GET api.restorehub.net/api/widget/result/YOUR-POLL-ID
A 204 No Content means the result isn't ready yet — keep polling (for example every 1.5 seconds, giving up after 2 to 3 minutes). A 200 response means the result is ready. The result is single-use: the first read consumes it, and it expires after 120 seconds either way. Once you have the result, close the in-app browser yourself.
The result
On success you get the same payload the website widget delivers to its onSuccess handler, including the same signed token:
{
"type": "RESTOREHUB_SUCCESS",
"token": "the signed widget JWT",
"user": {
"id": "discord user id",
"username": "name",
"email": "email or null",
"globalName": "display name or null",
"avatar": "avatar hash or null",
"accountCreatedAt": "ISO timestamp"
}
}If native verification is turned off for the server, or the widget is disabled, you get an error instead:
{ "type": "RESTOREHUB_ERROR", "code": "NATIVE_NOT_ENABLED", "message": "..." }Verifying the token
The token is identical to the one the website widget issues, so the same verification works:
POST the token to api.restorehub.net/api/v1/widget/verify-token, or
verify the RS256 signature yourself against the JWKS at api.restorehub.net/api/.well-known/jwks.json.
The access-token and refresh-token endpoints behave the same as for the website widget.
Don't use ASWebAuthenticationSession or a custom app-scheme redirect. That API only auto-dismisses when the browser redirects to a registered callback scheme, but this flow ends on a Restore Hub page rather than redirecting back to your app. Use a plain in-app browser you control, poll the result endpoint, and dismiss the browser yourself when the result arrives — no Discord redirect-URI changes needed.