Skip to content
Local environment Preproduction — not production data

Connecting a third-party provider (OAuth)

An App that connects an external system — a payroll vendor, a SaaS tool, DATEV — runs an OAuth authorization code flow against that provider. Factorial Code runs both redirect legs for you.

For the form side of it — the Connect button and how the result reaches the form — see Connecting an external account. This page is about the authorization itself.

Almost every provider requires redirect URIs to be registered in advance and then matches them exactly. But a webhook URL carries the workspace slug, and each customer’s installation of your App is its own deploy-{deployId} workspace:

https://code.factorialhr.com/platform/api/deploy-9f3c1ab8d2e4/webhooks/my-callback

That URL is different for every customer, so it can never be the registered one. Registering one customer’s URL and routing everybody through it is not a workaround — it makes one installation a dependency of all the others.

So the platform owns a single fixed redirect URI, and dispatches each callback to the installation it belongs to.

https://code.factorialhr.com/platform/api/oauth/callback

One string, for every App and every customer. Nothing per-workspace to keep in step, and no query parameters — providers compare the registered URI exactly.

Call fcode.oauth.start() from the process that renders your Connect button — typically a pre-render process — and give the form’s OAuth widget the authorizationUrl it returns.

const flow = await fcode.oauth.start({
authorizeUrl: "https://login.example.com/authorize",
clientId: fcode.env.PROVIDER_CLIENT_ID,
scope: ["openid", "offline_access", "payroll:read"],
onComplete: "oauth-callback",
data: { companyId, legalEntityId },
});
return { variables: { authorizeUrl: flow.authorizationUrl } };
Option
authorizeUrlThe provider’s authorization endpoint. Required.
clientIdThe client id registered with the provider. Required.
onCompleteThe process to run when the provider redirects back. Required, and it must already exist — starting a flow that names a missing process fails here, at render time, rather than after a customer has signed in.
scopeA list of scopes, or one already-joined string.
dataAnything this flow should carry to onComplete. Opaque to the platform and handed back untouched.
extraParamsProvider-specific authorization parameters (a nonce, an audience). They cannot override the ones the protocol depends on.
versionTagPins onComplete to a version, so a flow started before a release still lands on the code it was started with.
pkceOn by default. Turn it off only for a provider that cannot cope with it.

A flow is valid for 15 minutes, which is enough to survive a provider sign-in with 2FA. The authorization URL can be opened more than once — a form re-render or a double click reaches the provider again with the same challenge — but it completes only once.

The platform runs your onComplete process itself. It does not need a webhook, public or otherwise: nobody is calling it over HTTP, so there is nothing to expose and nothing to authenticate.

Everything the token exchange needs is already in the parameters:

const { code, codeVerifier, redirectUri, data, state, error } = fcode.context.parameters;
if (error) {
return redirectToForm({ status: "error", message: "Authorization was refused." });
}
const tokens = await exchange({
code,
code_verifier: codeVerifier,
redirect_uri: redirectUri, // the provider compares this byte for byte
client_id: fcode.env.PROVIDER_CLIENT_ID,
client_secret: fcode.env.PROVIDER_CLIENT_SECRET,
});
await fcode.datastore.set(`tokens.${data.companyId}`, JSON.stringify(tokens));
return {
status: 302,
headers: {
Location:
"https://code.factorialhr.com/sdk/oauth-callback.html" +
"?status=success&value=" + encodeURIComponent(data.companyId) +
"&state=" + encodeURIComponent(state), // the form only accepts a completion that echoes it
},
};
Parameter
codeThe authorization code. Absent when the provider refused.
codeVerifierThe PKCE verifier for this flow. Absent when pkce was off.
redirectUriWhat the platform sent as the redirect URI. Replay it in the token exchange — providers compare the two exactly.
dataWhatever you passed to start().
stateThe flow’s state. Echo it in your redirect to the forms SDK page: the authorization URL always carries one, so the form rejects a completion that does not echo it.
error, errorDescriptionPresent when the provider refused.

Whatever the process returns becomes the HTTP response the browser follows, so ending with a 302 to the forms SDK callback page is what closes the popup and tells the form how it went.

  • fcode.oauth needs the platform. Like fcode.schedule and fcode.storage, it is backed by the workspace’s meta token, so it is unavailable under a local fcode run. Exercise an OAuth flow on the platform, from an installed App.
  • response_mode=fragment and the implicit flow cannot work: a URL fragment never reaches a server, so there is nothing to dispatch on. Use the authorization code flow.
  • A provider that omits state when refusing cannot be routed either; the platform sends the popup to the forms SDK page with an error instead.