Skip to main content
POST
Sign in with Apple (native)

Body

application/json
identityToken
string
required

The Apple identity token as delivered by ASAuthorizationAppleIDCredential.identityToken (a UTF-8 string, not the raw bytes) — a short-lived RS256 JWT signed by Apple. Verified against Apple's public JWKS (signature, issuer, audience allowlist, expiry); the raw Apple private key is never involved. Single-use in spirit (10-minute validity) — do not cache it client-side.

Example:

"eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwic3ViIjoiMDAxMjM0LmFiY2RlZi4xMjM0IiwiYXVkIjoiY29tLnByYXhpcy5wcmlhIiwiZXhwIjoxNzU3NTAwMDAwLCJpYXQiOjE3NTc0OTk0MDAsIm5vbmNlIjoiYWJjMTIzIiwiZW1haWwiOiJqYW5lLmRvZUBwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSJ9.signature"

nonce
string
required

Required (not optional — a replay control that can be skipped is not one). The RAW nonce the app generated for this sign-in attempt; the app sends Apple a SHA-256 digest of this same value. The server accepts the raw nonce or its SHA-256 as hex, base64 or base64url, matched against the nonce claim inside identityToken.

Example:

"f3a1c9e2b6d4"

fullName
object | null

Apple sends this ONLY on the very first authorization for a given Apple ID + app pair — send it whenever the native SDK callback provides it, or the account is created nameless (Apple will not resend it later without the user removing and re-granting the app). Ignored entirely when an existing user is matched by appleSub or by a verified email.

brandingId
string

Same optional branding hint accepted by /api/auth/autosignup and /api/auth/signin — switches the active institution to the branded twin when the user is already a member.

Example:

"pub_6430736fd62d650040420674"

Response

Signed in (or newly created and signed in). Same union as /api/auth/signin: either { token, profile }, or { mfaRequired: true, challengeId, factor, availableFactors, maskedEmail } when the resolved user has email MFA enabled — discriminate on mfaRequired === true and complete via POST /api/auth/mfa-verify.

Successful signin response shape. Two variants are returned by the same endpoint depending on whether email MFA is required: • JWT issued{ token, profile }. The user is signed in. • MFA challenge{ mfaRequired: true, challengeId, maskedEmail, mandatorySuper? }. The client must POST the 6-digit code to /api/auth/mfa/verify with the challengeId; the verify endpoint then issues the JWT. Discriminate via mfaRequired === true (per Phase 1 design §6.1).

token
string

Signed JWT token. Present when MFA is not required or has just been verified. Include this in subsequent API requests via the x-access-token header or Authorization Bearer header. Expires after 6 hours (configurable via JWT_VALIDITY_SEC). Automatically refreshed on profile load (sliding session).

Example:

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NDMwNzM2ZmQ2MmQ2NTAwNDA0MjA2NzQiLCJlbWFpbCI6ImpvaG4uZG9lQG15ZG9tYWluLmNvbSIsImN1c3RvbWVySWQiOiJjdXNfTnh4eHh4eCIsImFjY291bnRUeXBlIjoidXNlciIsInNlc3Npb25JZCI6InMlM0FhYmMxMjMiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDA4NjQwMH0.signature"

profile
object
brandingSwitch
object

Outcome of a branded-link switch attempt. Present only when the request carried a brandingId (an institution publicId from a /login?brandingId= link). The hint never grants access: switched requires an active membership on the twin (or platform-operator status) and an active parent account. Clients clear the stored hint on switched/already and show the no-access notice on refused.

mfaRequired
boolean

When true, the response is an MFA challenge — no JWT issued. Client should redirect to the MFA verify screen with the challengeId.

Example:

true

challengeId
string

MongoDB ObjectId of the issued mfaChallenge. Only present when mfaRequired: true. POST this to /api/auth/mfa/verify alongside the 6-digit code.

Example:

"6856fa89cbafcff8d98680f5"

maskedEmail
string

Partially-masked email address the verification code was sent to (for the verify-screen "code sent to …" prompt). Only present when mfaRequired: true.

Example:

"j*****e@example.com"

mandatorySuper
boolean

Phase 2 — when true, this MFA challenge was issued under super-mandatory enforcement (MFA_SUPER_MANDATORY=true and the user is past the rollout date). The verify screen should render an explanatory banner and suppress the Cancel affordance, since the user can't dismiss the flow without enrolling. On successful verify, the server persists user.mfaEnabled = true so the next signin follows the normal phase-1 trusted-device path.

Only present when mfaRequired: true AND the gate fired. Omitted (not false) otherwise — clients should default to false when absent.

Example:

true