Developers

Partner SSO integration

Send your hierarchy managers straight into the CRM, already signed in and scoped to their own team. Adding your platform is configuration on our side, not a release.

How it works

Your platform authenticates the user. We mint a single-use code that locks the lane (MANAGER or POSP). The browser carries only that code back to us.

  1. A user signs in on your platform and clicks through to the CRM.
  2. Your backend asks us for a handoff with nameId (EMP ID) + subjectType (MANAGER or POSP).
  3. We return a redirectUri containing a single-use code that expires in about a minute.
  4. You redirect the browser there. Our callback exchanges the code and the user lands in the CRM on the correct lane.

Both modes converge on the same single-use code, so steps 3 and 4 are identical either way. Your connection is configured for one of them; GET /metadata tells you which.

Who this is for

Two lanes: MANAGER for hierarchy users, POSP for field agents. Choose subjectType on handoff — never in the browser URL.

Two subject types
MANAGER resolves against your SalesTeam hierarchy. POSP resolves against Posp.code (or Cognitensor UserCode for Roinet). Pass subjectType on the authenticated handoff body — never as a query parameter on the callback URL. Legacy isPosp on get-redirect-uri is supported and stored the same way.

1a. Handoff (platform-signed)

Your backend calls us with a per-connection API key, nameId (EMP ID), and subjectType. Use this if you cannot mint signed JWTs.

POST/api/v1/sso/{slug}/handoffX-Sso-Api-Key
Request (MANAGER)
POST /api/v1/sso/{slug}/handoff HTTP/1.1
Host: localhost:3000
Content-Type: application/json
X-Sso-Api-Key: sso_9f3c2b7a4d1e...

{
  "nameId": "EMP-10428",
  "subjectType": "MANAGER",
  "profile": {
    "fullName": "Priya Raghunathan",
    "email": "priya@acme.com",
    "mobile": "9876543210",
    "roleHint": "DM",
    "update": true,
    "metadata": {
      "branchCode": "PUN-01",
      "channel": "partner-app",
      "tags": ["priority"]
    }
  }
}
Request (POSP)
POST /api/v1/sso/{slug}/handoff HTTP/1.1
Host: localhost:3000
Content-Type: application/json
X-Sso-Api-Key: sso_9f3c2b7a4d1e...

{
  "nameId": "CSP023057",
  "subjectType": "POSP",
  "profile": {
    "fullName": "John Doe",
    "email": "john@partner.com",
    "mobile": "9876543210",
    "update": true
  }
}

Already registered in Xpresso (KYC is fetched once). Omit isAlreadyPosp, or send false, for a new POSP.

{
  "nameId": "CSP242922",
  "subjectType": "POSP",
  "isAlreadyPosp": true,
  "profile": {
    "fullName": "AJAY SRIVASTAV",
    "email": "a@roinet.in",
    "mobile": "8808462727"
  }
}
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "redirectUri": "http://localhost:3000/sso/acme/callback?code=F3xq2m9K1s...",
  "expires_in": 60
}
curl (MANAGER)
curl -X POST http://localhost:3000/api/v1/sso/acme/handoff \
  -H "Content-Type: application/json" \
  -H "X-Sso-Api-Key: $SSO_API_KEY" \
  -d '{"nameId":"EMP-10428","subjectType":"MANAGER"}'
curl (POSP)
curl -X POST http://localhost:3000/api/v1/sso/acme/handoff \
  -H "Content-Type: application/json" \
  -H "X-Sso-Api-Key: $SSO_API_KEY" \
  -d '{"nameId":"CSP023057","subjectType":"POSP"}'
About the API key
It is issued once when your connection is created and only its hash is stored, so it cannot be read back to you later. Send it as the X-Sso-Api-Key header, never in a query string. If it leaks, ask for a rotation: the old key stops working immediately.

1b. Assert (partner-signed)

You sign a short-lived assertion with your own key. Optional claim subjectType (or isPosp) selects the lane.

POST/api/v1/sso/{slug}/assertSigned assertion
Assertion claims
{
  "iss": "https://idp.acme.com",
  "aud": "http://localhost:3000",
  "sub": "EMP-10428",
  "jti": "0f9c1c4e-6f0e-4d5e-9a3f-2c1b8e6a77d1",
  "iat": 1767225600,
  "exp": 1767225720,
  "subjectType": "MANAGER",

  "name": "Priya Raghunathan",
  "email": "priya@acme.com"
}
Request
POST /api/v1/sso/{slug}/assert HTTP/1.1
Host: localhost:3000
Content-Type: application/json

{
  "assertion": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImsxIn0..."
}
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "redirectUri": "http://localhost:3000/sso/acme/callback?code=F3xq2m9K1s...",
  "expires_in": 60
}
Requirements
RS256 or ES256 only — none and HS* are rejected. iss must match the issuer registered on your connection, aud must be our API origin, and jti must be unique per sign-in: a repeated jti is refused as a replay. Keep exp short; the assertion is exchanged immediately. Optional claim subjectType (or isPosp) selects MANAGER vs POSP.

2. Redirect the browser

Send the user's browser to the redirectUri exactly as returned. Do not rewrite it or add isPosp to the URL.

Browser redirect
HTTP/1.1 302 Found
Location: http://localhost:3000/sso/acme/callback?code=F3xq2m9K1s...

3. Exchange the code

Our callback page does this for you. The lane comes from the code, not from the request body.

POST/api/v1/sso/{slug}/exchangeThe code is the credential
Request
POST /api/v1/sso/{slug}/exchange HTTP/1.1
Host: localhost:3000
Content-Type: application/json

{
  "code": "F3xq2m9K1s..."
}
Response
HTTP/1.1 200 OK
Set-Cookie: access_token=...; HttpOnly; Secure; SameSite=Lax; Path=/
Content-Type: application/json

{
  "id": "cku9x...",
  "email": "priya@acme.com",
  "role": "DM",
  "status": "ACTIVE",
  "partnerId": "cpartner...",
  "isPlatformAdmin": false
}

Metadata

Everything you need to configure your side, readable without credentials.

GET/api/v1/sso/{slug}/metadataPublic
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "slug": "acme",
  "label": "Acme Insurance — production",
  "mode": "PLATFORM_SIGNED_HANDOFF",
  "audience": "http://localhost:3000",
  "callbackUrl": "http://localhost:3000/sso/acme/callback",
  "handoffUrl": "http://localhost:3000/api/v1/sso/acme/handoff",
  "assertUrl": "http://localhost:3000/api/v1/sso/acme/assert",
  "exchangeUrl": "http://localhost:3000/api/v1/sso/acme/exchange",
  "acceptedAlgorithms": ["RS256", "ES256"],
  "subjectClaim": "sub",
  "codeTtlSecs": 60,
  "supportedSubjectTypes": ["MANAGER", "POSP"],
  "journeyEnabled": true,
  "journeyProductKeys": ["marine-git"],
  "journeyReturnUrlAllowlist": ["https://trucksuvidha.com"],
  "audience_note": "Pass subjectType MANAGER for hierarchy users or POSP for field agents. The lane is locked into the handoff code; the browser cannot change it. Buy journeys send a journey block; subject is forced to the house POSP."
}

Errors

Every refusal carries a reason code.

Error response
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "statusCode": 401,
  "message": "Invalid or expired handoff code"
}
Reason codeWhat it means
WRONG_SUBJECT_TYPEThe account does not match the subjectType lane (e.g. a manager on POSP, or a POSP on MANAGER).
ONBOARDING_REQUIREDPOSP first login needs profile.mobile (and usually name/email) so we can start onboarding, or the account cannot complete login yet. Mid-onboarding PENDING users with a User are signed in and sent to /onboarding. Fully ACTIVE POSPs go to /dashboard.
UNKNOWN_SUBJECTNo CRM profile matches that nameId and no usable partner profile was sent on handoff. Managers: send profile or map externalSubject. POSPs: send profile for first-time create/prefill.
AUTO_PROVISION_DISABLEDThe manager has no CRM account and this connection does not create accounts automatically. A partner admin must add them first.
ROLE_NOT_GRANTABLEThe resolved role is outside the roles this connection may create automatically.
RANK_CAPThe resolved role sits above the automatic provisioning ceiling. Senior roles are created by a partner admin.
PARTNER_SUSPENDEDThe tenant is suspended. No door signs anyone in while that holds.
USER_INACTIVEThe CRM account exists but is not active.
INVALID_CODEThe handoff code is unknown, expired, or already used. Codes are single-use.
BAD_API_KEYThe X-Sso-Api-Key header is missing or wrong.
BAD_SIGNATUREThe assertion failed verification: signature, algorithm, issuer, audience, or timing.
REPLAYThis assertion's jti has already been used. Mint a fresh one per sign-in.
CONNECTION_DISABLEDThis SSO connection is switched off. Contact your integration owner.
RATE_LIMITEDToo many attempts in a short window. Back off and retry.
JOURNEY_DISABLEDThis connection does not accept a journey block on handoff.
JOURNEY_RETURN_URL_DENIEDjourney.returnUrl is not on the connection allowlist.
JOURNEY_PRODUCT_MISMATCHjourney.productKey is unknown or does not match the connection.
JOURNEY_HOUSE_POSP_MISSINGjourneyHousePospCode is not configured on the connection.

Buy journey handoff

Optional journey block on platform-signed handoff for partner-embedded insurance purchase (e.g. marine GIT). Reuses Marketplace sell flows after a scoped house-POSP session.

Send a typed journey block on handoff (not profile.metadata). We force the house POSP subject, mint a scoped session, and land the browser on /journey/{partner}/marine-git. After policy issue we POST a signed callback to your journeyCallbackUrl (X-Roinet-Signature: sha256=… over timestamp.body). Pull fallback: GET /api/v1/partner/journeys/:id with the same API key.

Request (buy journey)
POST /api/v1/sso/{slug}/handoff HTTP/1.1
Host: localhost:3000
Content-Type: application/json
X-Sso-Api-Key: sso_9f3c2b7a4d1e...

{
  "nameId": "HOUSE-POSP-CODE",
  "subjectType": "POSP",
  "journey": {
    "productKey": "marine-git",
    "returnUrl": "https://trucksuvidha.com/booking/TS-2608-77412",
    "partnerRefs": { "bookingId": "TS-2608-77412" },
    "ewb": {
      "ewbNo": "692163844058",
      "totInvValue": 45340,
      "fromGstin": "24AAGFD4779D1ZH",
      "toGstin": "37AACCD9205D1ZX",
      "hsnCode": "39269059",
      "productDesc": "Articles of plastics",
      "vehicleNo": "GJ07YZ8615"
    }
  }
}
Marketplace reuse
The buy UI reuses the existing Marketplace ICICI Marine sell flow components — partners do not get a forked wizard.

Security notes

What the design guarantees, and what it expects from you.

  • The browser never carries an assertion, a role, a tenant id, or a subjectType — only an opaque single-use code that is worthless once exchanged.
  • subjectType is locked at handoff time. Changing anything on the callback URL cannot escalate a POSP into a manager session (or the reverse).
  • For MANAGER, roles and territory come from the CRM's own records — we do not honour a role claim from you.
  • We redirect only to the exact origin registered on your connection. There is no caller-supplied redirect parameter.
  • Send nameId as a stable EMP ID / Name ID. Email is a poor choice; an HR / IdP / POSP code is a good one.
  • Optional profile.metadata (JSON object, max ~8KB) is stored on User.metadata for partner-specific extras — not used for RBAC.
  • Endpoints are rate limited per connection and per nameId. Handle 401 and 429 responses rather than retrying in a tight loop.