IntraID

Add the IntraID button

Authorization code flow with PKCE, plus two endpoints you will not have seen before: the relay.

1 · Register the app

Register your company, create an application, and declare each field you want with the purpose and the reason. That reason is shown to the person, in your words. You get a client_id and a secret shown exactly once.

2 · Put the button on your page

<!-- your page -->
<div id="intraid-button"></div>
<script src="https://intraid.aiutil.in/button.js" defer
        data-start="/auth/intraid/start"
        data-label="Continue with IntraID"></script>

3 · Your start endpoint

It mints the PKCE verifier and the state, keeps them in your session, and redirects. Ask for scope=field:purpose pairs — only pairs you declared are offered.

// /auth/intraid/start
session_start();
$verifier = rtrim(strtr(base64_encode(random_bytes(48)), '+/', '-_'), '=');
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$_SESSION['iid_verifier'] = $verifier;
$_SESSION['iid_state']    = $state = bin2hex(random_bytes(8));

header('Location: https://intraid.aiutil.in/authorize?' . http_build_query([
  'client_id'             => 'cli_8e384dc89f95d9ac21eb97ec',
  'redirect_uri'          => 'https://yoursite.com/auth/intraid/callback',
  'scope'                 => 'profile.name:account contact.email:transactional contact.email:otp',
  'state'                 => $state,
  'code_challenge'        => $challenge,
  'code_challenge_method' => 'S256',
]));

4 · Your callback, and the token exchange

// /auth/intraid/callback?code=…&state=…
if (!hash_equals($_SESSION['iid_state'], $_GET['state'] ?? '')) exit('bad state');

$r = http_post('https://intraid.aiutil.in/oauth/token', [
  'grant_type'    => 'authorization_code',
  'code'          => $_GET['code'],
  'client_id'     => 'cli_8e384dc89f95d9ac21eb97ec',
  'client_secret' => INTRAID_SECRET,
  'code_verifier' => $_SESSION['iid_verifier'],
  'redirect_uri'  => 'https://yoursite.com/auth/intraid/callback',
]);

// {
//   "access_token": "iat_…",
//   "sub": "usr_9f21c8…",            ← your only identifier for this person
//   "granted": {"profile.name":["account"],"contact.email":["transactional","otp"]},
//   "relay_handles": {"contact.email":"eml_4a1f…"}
// }
Store sub as your user id. It is stable for your app and meaningless anywhere else. There is no email address in this response and there is no endpoint that will give you one.

5 · Read the fields you were granted

GET https://intraid.aiutil.in/api/userinfo?purpose=account
Authorization: Bearer iat_…

{
  "sub": "usr_9f21c8…",
  "purpose": "account",
  "profile": { "profile.name": "Rahul", "profile.city": "Lucknow" },
  "relay":   { "contact.email": { "handle": "eml_4a1f…", "readable": false } }
}

The purpose is required and is checked against what the person approved. Asking for a purpose you were not granted returns 403 and appears in their ledger as a refused attempt.

6 · Send them an email

POST https://intraid.aiutil.in/api/relay/email
Authorization: Bearer iat_…
Content-Type: application/json

{
  "purpose": "transactional",
  "subject": "Your order #4821 has shipped",
  "body":    "It left our Lucknow warehouse this morning…"
}

→ { "sent": true, "purpose": "transactional", "remaining_today": 9 }

7 · One-time codes

POST https://intraid.aiutil.in/api/relay/otp          { "channel": "email" }
→ { "ref": "otp_8c21…", "expires_in": 300 }

POST https://intraid.aiutil.in/api/relay/otp/verify   { "ref": "otp_8c21…", "code": "418290" }
→ { "verified": true }

You never see the code and you never see where it went. You learn one bit: it matched, or it did not.

Ceilings and refusals

LimitValue
Relayed emails per person per day10
Codes per person per hour5
Field reads per person per hour60
Access token lifetime3600s
Purposes refused outrightmarketing, profiling, resale, advertising

Going far past a ceiling is treated as intent, not accident: the connection is suspended and the person is told what happened.

/.well-known/intraid-configuration