Developers
Sign in with Cincio tells an app who a person is, and who vouched for each detail and in which month. Closed to outside apps for now.
The demo is Cincio's own small app, with made-up data. It is the only app that can use Sign in with Cincio today. No outside app can be registered yet, and this page will say so when that changes.
The provider is closed to outside apps; the key list is empty until it opens.
What an app receives
Only what the person ticks on the consent screen, and nothing else from their card. There is no name or picture claim.
sub- A different identifier for each app, so two apps cannot match the same person by it.
email,email_verified- The sign-in email, once it has been confirmed with a code.
phone_number,phone_number_verified- The phone number, once it has been confirmed with a code.
cincio_verified- For each detail shared: the method (
email_code,phone_codeororg_listed_and_confirmed), the month it was verified as year and month (verified_at, UTC) and the category (contactormembership). Never a day or a time.
Start in six steps
- Read
https://cincio-api.cincio.workers.dev/.well-known/openid-configuration. It lists the endpoints, scopes and claims. - Make a random
state, and a PKCE verifier and its S256 challenge. - Send the person to the authorize endpoint with
response_type=code, your client id, your exact redirect address, the scopes, the state and the challenge. - When they return, check the state. If they chose Not now you get
access_deniedand nothing was shared. - Post the code and your verifier to the token endpoint. There is no client secret.
- Check the ID token: ES256 signature with the keys at
jwks_uri, theniss,audandexp.
The redirect address must match the one registered for the app exactly. A wrong address, an unknown app or an app with no name is refused and nothing is shared.
How Cincio differs from a standard OpenID Connect provider
Cincio follows the authorization code flow with PKCE, with fewer options than the full standard. These are the fields in the discovery document where it differs.
| Discovery field | Cincio publishes | Standard OpenID Connect Basic |
|---|---|---|
response_types_supported | code | code |
grant_types_supported | authorization_code | authorization_code, refresh_token optional |
id_token_signing_alg_values_supported | ES256 | An RSA signature is required; Cincio does not offer one and signs only with ES256 |
code_challenge_methods_supported | S256 | not required |
token_endpoint_auth_methods_supported | none | client_secret_basic is the default |
subject_types_supported | pairwise | public or pairwise |
Not offered: a UserInfo endpoint (read the claims from the ID token the token endpoint returns), refresh tokens, client secrets and dynamic registration.
When a person changes what they share or removes an app, the app learns at the next sign-in, not before. ID tokens last 5 minutes.
Test status: automated tests cover the flows above. The OpenID conformance suite has not been run yet.
Sample for an Expo app
Type-checked with tsc --strict on 2026-10-28 against expo-auth-session 58.0.14, expo-web-browser 58.0.6 and expo-crypto 58.0.7. It has not been run on a device or against the live issuer yet. Expo Go cannot run it; use a development build. It reads the claims from the ID token and checks iss, aud, exp and nonce. Checking the signature with the keys at jwks_uri is your server’s job.
import * as React from 'react';
import { Button } from 'react-native';
import * as WebBrowser from 'expo-web-browser';
import * as Crypto from 'expo-crypto';
import {
makeRedirectUri, useAuthRequest, useAutoDiscovery, exchangeCodeAsync,
} from 'expo-auth-session';
WebBrowser.maybeCompleteAuthSession();
const ISSUER = 'https://cincio-api.cincio.workers.dev';
const CLIENT_ID = 'CLIENT_ID'; // the demo client today; it will change
function claimsOf(idToken: string): Record<string, unknown> {
const body = idToken.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(atob(body));
}
export default function SignInWithCincio(props: { onSignedIn: (claims: Record<string, unknown>) => void }) {
const discovery = useAutoDiscovery(ISSUER);
const redirectUri = makeRedirectUri({ scheme: 'myapp' });
const nonce = React.useMemo(() => Crypto.randomUUID(), []);
const [request, response, promptAsync] = useAuthRequest(
{ clientId: CLIENT_ID, scopes: ['openid', 'email'], redirectUri, extraParams: { nonce } },
discovery
);
React.useEffect(() => {
if (response?.type !== 'success' || !request || !discovery) return;
exchangeCodeAsync(
{ clientId: CLIENT_ID, code: response.params.code, redirectUri,
extraParams: { code_verifier: request.codeVerifier! } },
discovery
).then((token) => {
// The claims are in the ID token.
const claims = claimsOf(token.idToken!);
// Checking the signature with the keys at jwks_uri is your server's job.
const ok = claims.iss === ISSUER && claims.aud === CLIENT_ID
&& Number(claims.exp) * 1000 > Date.now() && claims.nonce === nonce;
if (ok) props.onSignedIn(claims);
});
}, [response]);
return <Button disabled={!request} title="Sign in with Cincio" onPress={() => promptAsync()} />;
}
Android and iOS
From Chrome’s documentation, not compiled by us: on Android use Auth Tab (androidx.browser 1.9.0 or later, Chrome 137 or later). Where the browser lacks it, the same call falls back to a Custom Tab. Use an HTTPS redirect verified by Digital Asset Links, because another app can register a custom scheme and intercept the callback.
From Apple’s documentation, not compiled by us: on iOS use ASWebAuthenticationSession with the callback: initializer (iOS 17.4 or later). The callbackURLScheme: form is deprecated.
A prompt for your coding agent
Paste this into the tool you build with.
Add "Sign in with Cincio" to my app with OpenID Connect: authorization code flow with PKCE (S256), as a public client with no client secret. Read every endpoint from the discovery document and do not hard-code them: https://cincio-api.cincio.workers.dev/.well-known/openid-configuration Ask only for the scopes the app needs, from: openid, email, phone, cincio:membership. The person ticks what to share, so handle any claim that is missing. Send a random state and the PKCE challenge, check the state when the person comes back, then exchange the code at the token endpoint with the code_verifier. Verify the ID token before trusting it: ES256 signature with the keys at jwks_uri, then iss, aud and exp (and nonce if you sent one). The sub is different for every app, so do not match people across apps by it. The cincio_verified claim says how and in which month a detail was verified. Show it as who vouched and when. Never use it for compliance or eligibility decisions. In a mobile app, open the sign-in in the system browser tab (Chrome Custom Tabs on Android, ASWebAuthenticationSession on iOS), never in an embedded WebView. Cincio is closed to outside apps for now, so build against the demo client and expect the client id and redirect address to change.
Native apps
Open the sign-in in the system browser tab: Chrome Custom Tabs on Android, ASWebAuthenticationSession on iOS. An embedded WebView is not a safe place to sign in and is not supported.
If you send a sign-in link in a WhatsApp Business template button, people open it in WhatsApp’s own browser, where password managers and SMS autofill may be missing: send the short email code, or tell them to tap the three dots, then Open in browser.