Every OAuth-based vendor integration in AppEngine follows the same shape: the org admin clicks "Connect Stripe" (or Facebook, Google, etc.), the platform redirects through the vendor's consent screen, the vendor returns a code to AppEngine's callback, and AppEngine exchanges it for an access token that gets encrypted and stored against the org. Once stored, Upstream calls the vendor on the org's behalf.
This page walks through the universal flow. Vendor-specific quirks live in the per-vendor guides.
The four steps
- 1
Get the auth URL
The admin UI calls an integration-type-specific endpoint that builds the consent URL with the right scopes and a state token tied to the org and the return URL.
const { authUrl, state } = await fetch( '/api/upstream/call/facebook-provider/get-auth-url', { method: 'POST', headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` }, body: JSON.stringify({ scopes: ['pages_manage_posts', 'pages_read_engagement'], returnUrl: 'https://admin.example/integrations', }), }, ).then(r => r.json());The
stateis signed with the org ID and a nonce. The platform validates it on callback to prevent CSRF. - 2
Redirect to the vendor
The browser navigates to
authUrl. The vendor shows their consent screen — "AppMint wants access to your Facebook pages, allow?".window.location.href = authUrl; - 3
Vendor calls back to AppEngine
The vendor redirects the user back to:
GET/connect/oauth2callback/:vendorNo authwith
codeandstatequery params. The Connect module:- Validates the
stateagainst the signed token, extracts the org ID. - Calls the vendor's token-exchange endpoint with the
codeto get anaccess_tokenandrefresh_token. - Encrypts the tokens and saves them to the integration record for the org.
- Redirects the browser to the original
returnUrlwith?integration=connected.
This route is
@PublicRoute()because the vendor cannot send a JWT. - Validates the
- 4
Use the integration
Subsequent Upstream calls find the saved tokens by
orgid, refresh them automatically when they expire, and pass the active token in the vendor SDK call.// Now this works without any further auth await fetch('/api/upstream/call/facebook-provider/post-to-page', { method: 'POST', headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` }, body: JSON.stringify({ pageId: '...', message: 'Hello' }), });
Refresh tokens
When a token expires, the vendor connector implementation calls the refresh endpoint and rotates the stored token in-place. Application code never sees this — the call that triggered the refresh waits for the new token and retries once.
If a refresh fails (revoked grant, expired refresh token), the integration moves to status: "expired". The next call returns a 412-style error and the admin must reconnect via step 1.
State and CSRF protection
The state parameter is a signed JWT containing:
orgId— which org is connectinguserId— which user clicked connectreturnUrl— where to send the browser backnonce— single-use, validated and burned on callback
If a state token is replayed, malformed, or older than 10 minutes, the callback rejects with 400.
Multi-account vendors
For vendors where one org connects multiple accounts (e.g. Facebook with multiple ad accounts), the integration record stores an array of accounts. The Upstream call accepts an accountId parameter to pick which one to use.
await fetch('/api/upstream/call/facebook-provider/post-to-page', {
method: 'POST',
headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
body: JSON.stringify({
accountId: 'act_123',
pageId: '...',
message: 'Hello',
}),
});
Disconnecting
/upstream/shutdown/:idJWTshutdown revokes the token where the vendor supports it (Stripe, Facebook do; some don't), zeroes the credential blob, and marks the integration inactive. Any in-flight calls fail; subsequent calls return "vendor not configured".
Error handling at consent
If the user denies consent, the vendor redirects back with error=access_denied. The Connect module logs the failure, marks the in-progress integration failed, and redirects the browser back to returnUrl with ?integration=denied. The admin UI surfaces the error.
Vendors without OAuth
A few vendors (Mailgun, SendGrid, AWS SES, Twilio API-key mode) authenticate via long-lived API keys rather than OAuth. The flow shortens to a single form: paste the key, click test, save.
await fetch('/api/upstream/save-integration', {
method: 'POST',
headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
body: JSON.stringify({
type: 'sendgrid-provider',
name: 'Production SendGrid',
credentials: {
apiKey: 'SG.xxx',
},
}),
});
// then test
await fetch('/api/upstream/test/sendgrid-provider/verify', {
method: 'POST',
headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
body: JSON.stringify({}),
});
The same encryption-at-rest applies. The Upstream call shape doesn't change between OAuth and API-key vendors — application code is identical either way.
The unified automation webhook at /connect/automation/:id/:stepId/:entity/:activity/ is what tracking pixels and click-redirects target. It's separate from vendor OAuth callbacks but uses the same Connect module.