Skip to content
cincio Make your card

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_code or org_listed_and_confirmed), the month it was verified as year and month (verified_at, UTC) and the category (contact or membership). Never a day or a time.

Start in six steps

  1. Read https://cincio-api.cincio.workers.dev/.well-known/openid-configuration. It lists the endpoints, scopes and claims.
  2. Make a random state, and a PKCE verifier and its S256 challenge.
  3. 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.
  4. When they return, check the state. If they chose Not now you get access_denied and nothing was shared.
  5. Post the code and your verifier to the token endpoint. There is no client secret.
  6. Check the ID token: ES256 signature with the keys at jwks_uri, then iss, aud and exp.

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.

Departures from OpenID Connect Basic
Discovery fieldCincio publishesStandard OpenID Connect Basic
response_types_supportedcodecode
grant_types_supportedauthorization_codeauthorization_code, refresh_token optional
id_token_signing_alg_values_supportedES256An RSA signature is required; Cincio does not offer one and signs only with ES256
code_challenge_methods_supportedS256not required
token_endpoint_auth_methods_supportednoneclient_secret_basic is the default
subject_types_supportedpairwisepublic 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.

Cincio states who vouched for a detail and when. It is not a check for compliance or eligibility decisions.