Launch on 01/01/2027Licenses available from 01/01/2027
PreviewThis page documents the release that launches on 01/01/2027. Packages, images and sign-up open then, and details may change before it.
Docs / Reference / API reference

API reference

Ready
TL;DR

The complete relying-party API — every call your backend makes on IdentityService, one collapsible card per RPC with its request and response fields, the typed errors it raises, and copy-paste Go examples today — more languages as each SDK publishes. Transport is gRPC over mutual TLS; there is no HTTP gateway.

Who this is for: the engineer integrating the relying-party surface, as an exhaustive lookup. For the narrative — how the calls fit into a ceremony — start with Your first ceremony and the guides. The device-facing surface is documented separately under Device-facing gRPC.

ⓘ Transport

Every call is gRPC over mutual TLS — your client certificate identifies the relying party; there is no HTTP gateway and no API key. See Certificates & PKI to provision the cert, and Errors for the typed failure model every call shares.

The examples show idiomatic SDK code plus a raw-gRPC path in Go. Every call can also return the shared transport reasons — MTLS_CERT_NOT_PINNED, MTLS_CERT_EXPIRED, PERMISSION_DENIED, SERVER_INTERNAL, SERVER_UNAVAILABLE — so the per-call error tables list only the domain-specific reasons.

One contract, ten languagesEvery client is generated from the same definitions the server implements.One contractthe message and service definitions — the single source of truthIdentityServiceDeviceGatewayDeviceOnboardAdminServiceLicenseServicegenerated, never hand-writtenThe serverGothe implementation itselfRelying-party libraries8 languagesGo · Python · TypeScript · RustJava · PHP · Ruby · .NETDevice libraries2 languagesSwift · KotlinWHAT STOPS THEM DRIFTING APARTStalenessthe build fails if a generated binding no longer matches the contract it came fromCoveragethe build fails if any call exists in the contract but is missing a scenario in any languageTyped errorsa source analysis fails the build on any error raised without a named reasonLockstep versionsall ten packages carry one version; a mismatched set cannot be releasedA change to the contract is a change to eleven things at onceThat is the cost of the arrangement, and the checks above are what make it a mechanical cost rather than a risky one.
One interface contract generating the server, eight relying-party libraries and two device libraries — so a call means the same thing in every language.

Users7 RPCs

Unary IdentityService.CreateUser Create one opaque identity 🔒 mTLS

Creates a single anonymous user and returns its opaque ID. The relying party is identified by its mTLS certificate, so the request carries no body.

⚠ Critical gotcha

Keep one opaque ID per real person for their lifetime, and hold the mapping on your side. Ydentifi cannot detect or merge duplicates — a second identity fragments a person’s devices and history. De-duplicate before you call.

Request — CreateUserRequest

No fields — the relying party is identified by its mTLS certificate.

Response — CreateUserResponse

FieldTypeNotes
opaque_idstringrequiredThe 64-character hex identifier for the new user. Store it against your real record.

Errors

ReasonRaised when
LICENSE_USER_CAP_REACHEDthe deployment holds more seats or users than its license allows, and refuses every call until its operator resolves it
MAX_USERS_REACHEDthe relying party’s own seat cap (max_users) is reached; deleting a user frees a seat at once

Examples

// The RP is identified by its mTLS client cert, so the request is empty.
res, err := client.CreateUser(ctx, &identity.CreateUserRequest{})
if err != nil { /* handle error */ }
opaqueID := res.OpaqueId  // 64-char hex — store it against your user
Server-streaming IdentityService.BulkCreateUsers Create many identities in one stream 🔒 mTLS

Creates many identities in a single server-streamed call — for a migration or a large signup. Chunks arrive as they are created; a final summary reports the totals.

Frames are a oneof: zero or more chunk frames, then exactly one summary frame. Persist each chunk as it arrives.
⚠ Gotcha

Running out of headroom mid-batch is not an error — the stream completes and summary.capped tells you how many were skipped. Check it rather than relying on the call to fail.

Request — BulkCreateUsersRequest

FieldTypeNotes
countuint32requiredHow many identities to create (≥ 1). Capped at the RP’s remaining headroom.
idempotency_keystringrequiredRe-sending the same key resumes an interrupted run instead of creating a second batch.

Response — BulkCreateUsersFrame (stream)

FieldTypeNotes
chunk.opaque_idsstring[]optionalThe IDs created in this chunk.
chunk.created_totaluint32optionalRunning total so far, inclusive.
summary.requesteduint32optionalThe requested count.
summary.createduint32optionalTotal created (including any resumed partial).
summary.cappeduint32optionalHow many were not created when headroom ran out (else 0).
summary.capped_reasonenumoptionalWhy capping occurred (else UNSPECIFIED).

Errors

ReasonRaised when
INVALID_ARGUMENTcount < 1 or idempotency_key missing
LICENSE_USER_CAP_REACHEDthe deployment holds more seats or users than its license allows. Raised when the stream opens or between chunks; the chunks already sent are created, and re-sending the same key resumes once it is resolved

Examples

stream, err := client.BulkCreateUsers(ctx, &identity.BulkCreateUsersRequest{
    Count:          1000,
    IdempotencyKey: "batch-2026-07-24-a", // re-send to resume a partial run
})
if err != nil { /* handle */ }
for {
    frame, err := stream.Recv()
    if err == io.EOF { break }
    if err != nil { /* handle */ }
    switch f := frame.Frame.(type) {
    case *identity.BulkCreateUsersFrame_Chunk:
        store(f.Chunk.OpaqueIds)              // persist as they arrive
    case *identity.BulkCreateUsersFrame_Summary:
        log.Printf("created %d of %d (capped %d)",
            f.Summary.Created, f.Summary.Requested, f.Summary.Capped)
    }
}
Unary IdentityService.GetUser Read one user 🔒 mTLS

Returns a user’s status and timestamps.

Request — GetUserRequest

FieldTypeNotes
opaque_idstringrequiredThe user to retrieve.

Response — GetUserResponse

FieldTypeNotes
opaque_idstringoptionalThe user’s ID.
statusUserStatusoptionalACTIVE or LOCKED.
created_atstringoptionalISO-8601 UTC.
last_authenticated_atstringoptionalISO-8601 UTC.
last_activity_atstringoptionalISO-8601 UTC.
totp_lockedbooloptionalTrue when the user’s codes are locked after 100 wrong codes in a row. VerifyUserTotp answers TOTP_LOCKED until IssueUserTotpEnrollment(reuse_existing=false) issues a new secret. status is not changed by it.

Errors

ReasonRaised when
USER_NOT_FOUNDno user with that opaque ID

Examples

res, err := client.GetUser(ctx, &identity.GetUserRequest{OpaqueId: opaqueID})
if err != nil { /* handle */ }
fmt.Println(res.Status, res.LastAuthenticatedAt)
Server-streaming IdentityService.ExportUsers Stream every user out 🔒 mTLS

Streams all of the relying party’s users in server-sized batches — for a backup or an offline reconciliation. Resume from a cursor.

Batches stream until the roster is exhausted. Use the last ID you saw as start_after_user_id to resume.

Request — ExportUsersRequest

FieldTypeNotes
start_after_user_idstringoptionalOpaque ID to start after. Empty starts from the beginning.

Response — ExportUsersFrame (stream)

FieldTypeNotes
usersGetUserResponse[]optionalA batch of identities; the server dictates the batch size.

Errors

ReasonRaised when
INVALID_ARGUMENTthe cursor is malformed

Examples

stream, err := client.ExportUsers(ctx, &identity.ExportUsersRequest{})
if err != nil { /* handle */ }
for {
    batch, err := stream.Recv()
    if err == io.EOF { break }
    if err != nil { /* handle */ }
    for _, u := range batch.Users { archive(u) }   // server dictates batch size
}
Unary IdentityService.UpdateUserStatus Lock or unlock a user 🔒 mTLS

Locks or unlocks a user without deleting them. A locked user cannot complete new ceremonies.

Request — UpdateUserStatusRequest

FieldTypeNotes
opaque_idstringrequiredThe user to update.
statusUserStatusrequiredACTIVE or LOCKED.

Response — UpdateUserStatusResponse

FieldTypeNotes
new_statusUserStatusoptionalThe status now in effect.

Errors

ReasonRaised when
USER_NOT_FOUNDno user with that opaque ID
INVALID_ARGUMENTthe status is UNSPECIFIED

Examples

_, err := client.UpdateUserStatus(ctx, &identity.UpdateUserStatusRequest{
    OpaqueId: opaqueID,
    Status:   identity.UserStatus_USER_STATUS_LOCKED,
})
if err != nil { /* handle */ }
Unary IdentityService.DeleteUser Delete a user for good 🔒 mTLS

Removes a user and all of their device bindings. Irreversible.

⚠ Critical gotcha

This is permanent and cascades to every device the user enrolled. To pause a user instead, use UpdateUserStatus.

Request — DeleteUserRequest

FieldTypeNotes
opaque_idstringrequiredThe user to delete.

Response — DeleteUserResponse

FieldTypeNotes
successbooloptionalTrue when the user and bindings were removed.

Errors

ReasonRaised when
USER_NOT_FOUNDno user with that opaque ID

Examples

_, err := client.DeleteUser(ctx, &identity.DeleteUserRequest{OpaqueId: opaqueID})
if err != nil { /* handle */ }
Unary IdentityService.UnlockUserRequestLimits Clear a rate-limit quarantine 🔒 mTLS

Clears a user’s per-user rate limits at once, once you have resolved the cause. It does not unlock a user you locked with UpdateUserStatus (USER_LOCKED), nor codes locked after 100 wrong codes (TOTP_LOCKED): only IssueUserTotpEnrollment(reuse_existing=false) unlocks those.

⚠ Gotcha

Quarantines also clear on their own when auto_expires_at passes — see Limits & quotas. Only clear early once the abuse is understood, and treat it as a last resort: it resets every rate limit on that account, not just the one that tripped, so calling it repeatedly weakens real defenses. Confirm with the user before clearing, and don’t automate it — no scheduled jobs, no auto-retry-then-unlock loops.

Request — UnlockUserRequestLimitsRequest

FieldTypeNotes
opaque_idstringrequiredThe quarantined user.

Response — UnlockUserRequestLimitsResponse

FieldTypeNotes
successbooloptionalTrue when the quarantine was cleared.

Errors

ReasonRaised when
USER_NOT_FOUNDno user with that opaque ID
RATE_LIMIT_EXCEEDEDthis user’s unlocks are used up: 3 at once, then 1 every 10 minutes; once used up, none for 30 minutes

Examples

_, err := client.UnlockUserRequestLimits(ctx,
    &identity.UnlockUserRequestLimitsRequest{OpaqueId: opaqueID})
if err != nil { /* handle */ }

Devices4 RPCs

Unary IdentityService.ListDevicesForUser List a user’s devices 🔒 mTLS

Returns every device enrolled for a user, of every status: active, inactive and banned. Inactive and banned rows are kept so you have the full forensic history.

Request — ListDevicesForUserRequest

FieldTypeNotes
opaque_idstringrequiredThe user whose devices to list.

Response — ListDevicesForUserResponse

FieldTypeNotes
devicesDeviceSummary[]optionalEach carries device_id, hardware permanent_id, platform, status, timestamps, and an optional name.

Errors

ReasonRaised when
USER_NOT_FOUNDno user with that opaque ID

Examples

res, err := client.ListDevicesForUser(ctx,
    &identity.ListDevicesForUserRequest{OpaqueId: opaqueID})
if err != nil { /* handle */ }
for _, d := range res.Devices {
    fmt.Println(d.DeviceId, d.Platform, d.Status) // INACTIVE rows included
}
Unary IdentityService.RevokeDevice Voluntarily deactivate a device 🔒 mTLS

Deactivates one device — the user swapped phones, or you are tidying up. The device can no longer sign; nothing is blocklisted. If the device was active, the user’s pending challenges end with it. The user’s TOTP secret is not replaced: the device’s copy keeps giving valid codes until IssueUserTotpEnrollment with reuse_existing false replaces it.

⚠ Gotcha

Revoke is a voluntary deactivation, not a blocklist. For a compromised or stolen device, use BanDevice, which denies the hardware and deactivates the active enrollment on it, if there is one (at most one per relying party).

Request — RevokeDeviceRequest

FieldTypeNotes
opaque_idstringrequiredThe user the device belongs to.
device_idstringrequiredThe server-assigned UUID from ListDevicesForUser.

Response — RevokeDeviceResponse

FieldTypeNotes
successbooloptionalTrue on first revoke and on idempotent retries alike.

Errors

ReasonRaised when
DEVICE_NOT_FOUNDthe (user, device) pair does not resolve for the calling RP

Examples

_, err := client.RevokeDevice(ctx, &identity.RevokeDeviceRequest{
    OpaqueId: opaqueID,
    DeviceId: deviceID, // from ListDevicesForUser
})
if err != nil { /* handle */ }
Unary IdentityService.BanDevice Blocklist compromised hardware 🔒 mTLS

Persistent denial for a compromised or stolen device. Resolves the row’s hardware permanent_id, blocklists it, and deactivates the active enrollment on that hardware, if there is one (at most one per relying party).

⚠ Critical gotcha

A ban targets the hardware, not just this one enrollment — it deactivates the active enrollment on the device’s permanent_id, if there is one (at most one per relying party), whichever user it belongs to. UnbanDevice lifts the block but does not resurrect that enrollment; the user must re-enroll.

⚠ Gotcha

A ban does not replace the user’s TOTP secret: the banned phone’s copy keeps giving valid codes. If the phone may be in someone else’s hands, call IssueUserTotpEnrollment with reuse_existing false after the ban.

Request — BanDeviceRequest

FieldTypeNotes
opaque_idstringrequiredA user whose device row identifies the hardware to ban.
device_idstringrequiredThe server-assigned UUID; the handler bans the hardware behind it.
reasonstringoptionalFree-form note persisted for forensic audit (e.g. “lost report”).

Response — BanDeviceResponse

FieldTypeNotes
successbooloptionalTrue on first ban and on idempotent re-bans.
cascaded_device_countint32optionalHow many active enrollments the ban deactivated: 0 or 1 (0 is normal if already revoked).

Errors

ReasonRaised when
DEVICE_NOT_FOUNDthe (user, device) pair does not resolve for the calling RP

Examples

res, err := client.BanDevice(ctx, &identity.BanDeviceRequest{
    OpaqueId: opaqueID,
    DeviceId: deviceID,
    Reason:   "lost device report", // optional, persisted for audit
})
if err != nil { /* handle */ }
log.Printf("banned; %d active enrollments deactivated", res.CascadedDeviceCount)
Unary IdentityService.UnbanDevice Lift a hardware ban 🔒 mTLS

Removes a device’s hardware from the blocklist so it may enroll again. It does not reactivate the enrollments the ban deactivated.

⚠ Gotcha

Unbanning clears the blocklist only. The previously-banned enrollments stay INACTIVE — the user re-enrolls from a fresh invite. See Enroll & link devices.

Request — UnbanDeviceRequest

FieldTypeNotes
opaque_idstringrequiredThe user whose device identifies the banned hardware.
device_idstringrequiredThe server-assigned UUID of a row on that hardware.

Response — UnbanDeviceResponse

FieldTypeNotes
successbooloptionalTrue on first unban and on idempotent retries.

Errors

ReasonRaised when
DEVICE_NOT_FOUNDthe (user, device) pair does not resolve for the calling RP

Examples

_, err := client.UnbanDevice(ctx, &identity.UnbanDeviceRequest{
    OpaqueId: opaqueID,
    DeviceId: deviceID,
})
if err != nil { /* handle */ }
// the user must re-enroll — old enrollments are NOT restored

Challenges3 RPCs

Unary IdentityService.IssueAuthChallenge Ask a user to approve an action 🔒 mTLS

Issues a challenge over an exact action at the strength you require, and returns a challenge ID plus a Universal-Link QR to render or push to the device.

⚠ Gotcha

Put the exact action in context — the amount, the payee, the operation. It is hashed into the signed payload, so a man-in-the-middle who changes what the user sees invalidates the signature. And always set min_strength explicitly; there is no implicit tier.

Request — IssueAuthChallengeRequest

FieldTypeNotes
opaque_idstringrequiredThe target user; must be ACTIVE and owned by the calling RP.
ttl_secondsuint32optionalLifetime in [30, 300]. Zero means the 60-second server default.
contextbytesoptionalDisplay string (≤256 B) shown on the approval screen. Hashed into the signed payload, so altering the text invalidates the signature.
min_strengthAuthStrengthrequiredThe minimum tier the signed response must satisfy: DEVICE_ACCESS or BIOMETRIC. There is no default.
location_constraintLocationConstraintoptionalOptional geofence; evaluated on-device, only a coarse bucket returns.

Response — IssueAuthChallengeResponse

FieldTypeNotes
challenge_idstringoptionalA 26-character ULID. The QR carries only this — no identity leaks on the wire.
qr_encodedstringoptionalThe full Universal-Link URL to render as a QR (you supply the QR library).
expires_atstringoptionalISO-8601 UTC; when the challenge stops being fetchable or signable.

Errors

ReasonRaised when
USER_NOT_FOUNDno such ACTIVE user for the calling RP
MIN_STRENGTH_REQUIREDmin_strength was left UNSPECIFIED
DEVICE_INCAPABLEthe user’s device cannot meet the requested tier
INVALID_ARGUMENTttl_seconds out of range, or context too long
CHALLENGE_TOO_SOONthis user’s last challenge is still open and less than 15 seconds old; nothing was spent. Retry after auto_expires_at, or once the user has answered the open one. The newest open challenge’s own challenge_id again answers ALREADY_EXISTS instead, and spends a try as before
RATE_LIMIT_EXCEEDEDthis user’s challenges are spent. Each challenge spends one; an approval gives them all back, while denied, dismissed and expired ones keep counting, and so does an approval refused for the wrong number or the wrong place. How often a user may be challenged between approvals is yours to limit. See Limits

Examples

iss, err := client.IssueAuthChallenge(ctx, &identity.IssueAuthChallengeRequest{
    OpaqueId:    opaqueID,
    TtlSeconds:  120, // [30,300]; 0 = 60s default
    Context:     []byte("Approve a $4,200 transfer to ...8842"),
    MinStrength: identity.AuthStrength_AUTH_STRENGTH_BIOMETRIC, // level 2 — biometric, no passcode fallback
})
if err != nil { /* handle */ }
render(iss.QrEncoded) // draw the Universal-Link QR, or push it to the device
Server-streaming IdentityService.AwaitChallenge Wait for the signed outcome 🔒 mTLS

Opens a server stream that yields the challenge’s terminal outcome — no polling, no webhook. The stream closes when the challenge reaches a terminal state.

The stream yields frames as the challenge progresses and closes on a terminal state — your Recv loop ends at EOF. It does not stay open after APPROVED / DENIED / EXPIRED.
⚠ Gotcha

Read more than the verdict. auth_strength is the tier the device reached. On an approval it is at or above the min_strength you asked for; on any other status treat it as a report of what the device did rather than a promise about your floor. Alongside it, location_result is the bucket it reported: see Authorize an action. signature is not something you can check — the deployment verified it, and this surface returns neither the device’s public key nor the remaining payload fields you would need to rebuild the digest. Keep it as a record if you want one; do not present it as a check you performed.

Request — AwaitChallengeRequest

FieldTypeNotes
challenge_idstringrequiredThe 26-character ULID from IssueAuthChallenge.

Response — AwaitChallengeFrame (stream)

FieldTypeNotes
statusAuthChallengeStatusoptionalPENDING → DELIVERED → a terminal APPROVED / DENIED / REJECTED / EXPIRED / SUPERSEDED / DISMISSED.
signaturebytesoptionalECDSA-P256 (or hybrid) signature over the canonical payload, on APPROVED, DENIED and REJECTED. The deployment has already verified it; you cannot, because this surface returns neither the device key nor the rest of the payload.
expressionAuthChallengeExpressionoptionalWhat the user said, as the device attested it. Separate from status: an approval can still end REJECTED.
decline_reasonDeclineReasonoptionalPresent iff DENIED, and only where the user gave a reason.
verification_failureVerificationFailureoptionalWhich check refused. Present iff REJECTED.
auth_strengthAuthStrengthoptionalThe tier the device reached. Treat it as a floor guarantee on APPROVED and nowhere else — on any other status it is a report of what the device did, not a promise about your min_strength. Unset on EXPIRED, SUPERSEDED and any non-terminal frame.
location_resultLocationResultoptionalCoarse bucket + accuracy, iff the device reported one.
emitted_atstringoptionalRFC 3339 Nano; when the server emitted the frame.

Errors

ReasonRaised when
CHALLENGE_NOT_FOUNDno challenge with that ID
CHALLENGE_ALREADY_TERMINALthe challenge already reached a terminal state

Examples

stream, err := client.AwaitChallenge(ctx,
    &identity.AwaitChallengeRequest{ChallengeId: challengeID})
if err != nil { /* handle */ }
for {
    frame, err := stream.Recv()
    if err == io.EOF { break } // stream closes on a terminal state
    if err != nil { /* handle */ }
    switch frame.Status {
    case identity.AuthChallengeStatus_AUTH_CHALLENGE_STATUS_APPROVED:
        grant(frame.AuthStrength) // the tier reached, which may beat what you asked for
    case identity.AuthChallengeStatus_AUTH_CHALLENGE_STATUS_DENIED:
        log.Printf("denied: %v", frame.DeclineReason)
    }
}
Unary IdentityService.ListActiveChallenges List a user’s in-flight challenges 🔒 mTLS

Lists a user’s non-terminal challenges — PENDING (not yet picked up) or DELIVERED (fetched, awaiting a response). For a dashboard, or to avoid issuing a duplicate.

Request — ListActiveChallengesRequest

FieldTypeNotes
opaque_idstringrequiredThe user whose active challenges to list.

Response — ListActiveChallengesResponse

FieldTypeNotes
challengesActiveChallengeSummary[]optionalEach carries challenge_id, status, echoed context, any geofence, and issued / expires / delivered timestamps.

Errors

ReasonRaised when
USER_NOT_FOUNDno user with that opaque ID

Examples

res, err := client.ListActiveChallenges(ctx,
    &identity.ListActiveChallengesRequest{OpaqueId: opaqueID})
if err != nil { /* handle */ }
for _, c := range res.Challenges {
    fmt.Println(c.ChallengeId, c.Status, c.ExpiresAt)
}

Enrollment & recovery2 RPCs

Unary IdentityService.IssueEnrollmentInvite Invite a new device to enroll 🔒 mTLS

Mints a single-use invite that lets a user enroll a new device for an existing account. The relying party renders the QR; the user scans it in their Ydentifi app.

⚠ Gotcha

The invite is single-use and short-lived. The context is stored with the invite: the phone does not show it and the enrollment does not sign it. Before consent, the phone shows the host of the deployment the code points to. See Enroll & link devices.

Request — IssueEnrollmentInviteRequest

FieldTypeNotes
opaque_idstringrequiredThe ACTIVE user the device will be enrolled for.
ttl_secondsuint32optionalHow long the invite is scannable, in [60, 600]. Zero means the 300-second default.
contextbytesoptionalOptional string (≤256 B), stored with the invite. Not shown on the phone and not signed.

Response — IssueEnrollmentInviteResponse

FieldTypeNotes
invite_tokenbytesoptionalA 256-bit opaque token — the lookup key for the server-side invite record.
qr_payloadbytesoptionalThe QR-renderable form (URL-safe base64 of the token). You draw the QR.
expires_atstringoptionalISO-8601 UTC; when the invite stops being valid.

Errors

ReasonRaised when
USER_NOT_FOUNDno such ACTIVE user for the calling RP
INVALID_ARGUMENTttl_seconds out of range, or context too long
RATE_LIMIT_EXCEEDEDthis user’s limit for this call is spent; see Limits
MAX_DEVICES_REACHEDthe user already has 3 active devices, the most allowed; revoke one, then invite again — see Enroll

Examples

inv, err := client.IssueEnrollmentInvite(ctx, &identity.IssueEnrollmentInviteRequest{
    OpaqueId:   opaqueID,
    TtlSeconds: 300, // [60,600]; 0 = 300 default
    Context:    []byte("Add a device for Acme Bank"),
})
if err != nil { /* handle */ }
render(inv.QrPayload) // show as a QR for the user's Ydentifi app to scan
Unary IdentityService.IssueRecoveryInvite Recover an account on a new device 🔒 mTLS

Mints a recovery invite for a user who lost their device. Issuing it atomically revokes every currently-active device for that user, then lets them enroll fresh.

⚠ Critical gotcha

This is destructive by design: the moment the invite is issued, every active device for the user can no longer sign in or approve challenges — the whole point of lost-phone recovery. It does not replace the user’s TOTP secret: call IssueUserTotpEnrollment with reuse_existing false, or the lost phone’s codes stay valid. Use IssueEnrollmentInvite to add a device without revoking the others.

Request — IssueRecoveryInviteRequest

FieldTypeNotes
opaque_idstringrequiredThe ACTIVE user being recovered. All their active devices are revoked as the invite is created.
ttl_secondsuint32optionalScannable window in [60, 600]. Zero means the 300-second default.
contextbytesoptionalOptional string (≤256 B), stored with the invite. Not shown on the phone and not signed.

Response — IssueRecoveryInviteResponse

FieldTypeNotes
invite_tokenbytesoptionalA 256-bit opaque token the new device presents to claim the invite.
qr_payloadbytesoptionalThe QR-renderable form (URL-safe base64 of the token).
expires_atstringoptionalISO-8601 UTC; when the invite stops being valid.
revoked_device_countuint32optionalHow many active devices the cascade revoked (0 if the user had none).

Errors

ReasonRaised when
USER_NOT_FOUNDno such ACTIVE user for the calling RP
INVALID_ARGUMENTttl_seconds out of range, or context too long
RATE_LIMIT_EXCEEDEDthis user’s limit for this call is spent; see Limits

Examples

inv, err := client.IssueRecoveryInvite(ctx, &identity.IssueRecoveryInviteRequest{
    OpaqueId:   opaqueID,
    TtlSeconds: 300,
    Context:    []byte("Recover your account on a new device"),
})
if err != nil { /* handle */ }
// Issuing the invite ALREADY revoked every active device for this user.
log.Printf("%d active devices revoked", inv.RevokedDeviceCount)
render(inv.QrPayload)

TOTP2 RPCs

Unary IdentityService.IssueUserTotpEnrollment Enroll a TOTP secret 🔒 mTLS

Mints (or re-issues) a user’s time-based one-time-password secret and returns it three ways: a ready QR image, the raw otpauth:// URL, and the Base32 secret for manual entry. The Ydentifi app can hold it, or any third-party authenticator.

⚠ Gotcha

secret_base32 is returned in-band — TLS is the confidentiality boundary. Never log it or persist it in plaintext; hand it to the user and forget it. The Ydentifi app bundles TOTP with anti-phishing hardening, but the same secret works in any RFC-6238 authenticator via totp_url.

Request — IssueUserTotpEnrollmentRequest

FieldTypeNotes
opaque_idstringrequiredThe user to enroll.
reuse_existingbooloptionalTrue re-issues the current secret; false mints a fresh one, and every copy of the old one stops giving valid codes. Removing a device never does this. Only false unlocks codes locked by TOTP_LOCKED.

Response — IssueUserTotpEnrollmentResponse

FieldTypeNotes
qr_code_png_base64stringoptionalA 512×512 PNG of the enrollment QR, base64-encoded.
totp_urlstringoptionalThe raw RFC-6238 otpauth:// URL; edit the label to add a username.
secret_base32stringoptionalThe Base32 secret for manual entry. Sensitive — see the gotcha.

Errors

ReasonRaised when
USER_NOT_FOUNDno user with that opaque ID
TOTP_SECRET_KEY_UNAVAILABLEthe server’s TOTP encryption key was removed by the operator
RATE_LIMIT_EXCEEDEDthis user’s limit for this call is spent; see Limits

Examples

inv, err := client.IssueUserTotpEnrollment(ctx, &identity.IssueUserTotpEnrollmentRequest{
    OpaqueId:      opaqueID,
    ReuseExisting: false, // false mints a fresh secret; true re-issues the current one
})
if err != nil { /* handle */ }
// inv.TotpUrl is the otpauth:// URL; inv.QrCodePngBase64 is a ready 512x512 QR.
// inv.SecretBase32 is sensitive — never log or persist it in plaintext.
render(inv.QrCodePngBase64)
Unary IdentityService.VerifyUserTotp Verify a 6-digit code 🔒 mTLS

Checks a user-entered TOTP code against their secret, within an optional clock-drift window.

⚠ Gotcha

Each code is accepted once. A code already accepted answers valid: false and counts as a wrong code, so never retry a call with the same code: after a timeout, ask the user for the next code. Treat RATE_LIMIT_EXCEEDED as a signal to back off, not to retry. Read the typed reason rather than the message text.

Request — VerifyUserTotpRequest

FieldTypeNotes
opaque_idstringrequiredThe user whose code to verify.
totp_codestringrequiredThe 6-digit code the user entered (RFC 6238).
skewTotpSkewoptionalClock-drift tolerance. Default is ±1 step (30 s); TOTP_SKEW_0 is ±0; at most ±2 steps (60 s). Wider is refused INVALID_ARGUMENT.

Response — VerifyUserTotpResponse

FieldTypeNotes
validbooloptionalWhether the code matched the user’s secret and is newer than the last code accepted for the user. A used code, or an older one, is false.

Errors

ReasonRaised when
USER_NOT_FOUNDno user with that opaque ID
TOTP_NOT_ENROLLEDthe user has no TOTP secret
RATE_LIMIT_EXCEEDEDtoo many verification attempts for this user; see Limits
TOTP_SECRET_KEY_UNAVAILABLEthe server’s TOTP encryption key was removed
TOTP_LOCKEDthe user’s codes are locked after 100 wrong codes in a row; every code is refused, a correct one too, until IssueUserTotpEnrollment(reuse_existing=false) issues a new secret

Examples

res, err := client.VerifyUserTotp(ctx, &identity.VerifyUserTotpRequest{
    OpaqueId: opaqueID,
    TotpCode: "492817",
})
if err != nil {
    // Typed reason lives in the error details — read it, don't parse strings.
    if d := identity.ErrorDetailFrom(err); d != nil {
        switch d.Reason {
        case identity.ErrorReason_RATE_LIMIT_EXCEEDED: // back off
        case identity.ErrorReason_TOTP_NOT_ENROLLED: // prompt enrollment
        }
    }
    return err
}
if !res.Valid { /* wrong code */ }

Certificates3 RPCs

Unary IdentityService.GetCertificateExpiry Read your certificate’s validity 🔒 mTLS

Returns the issuance, expiry, and fingerprint of the relying party’s currently-pinned mTLS certificate, plus its stored next-rotation TTL preference. Use it to drive your own expiry warnings.

⚠ Gotcha

There is no automatic client-cert renewal by design — rotation stays a deliberate, operator-driven act. Poll this to warn well before expires_at, then rotate. See Certificates & PKI.

Request — GetCertificateExpiryRequest

No fields — the relying party is identified by its mTLS certificate.

Response — GetCertificateExpiryResponse

FieldTypeNotes
issued_atstringoptionalRFC 3339 Nano. Empty if the RP has no cert yet.
expires_atstringoptionalRFC 3339 Nano. When the pinned cert expires.
fingerprintstringoptionalHex SHA-256 over the cert DER — the value the server pins. Echo it to confirm a rotation landed.
cert_ttl_daysint32optionalYour stored next-rotation TTL preference, in days (defaults to 90 if never set).

Errors

No domain-specific errors — only the shared transport reasons apply.

Examples

exp, err := client.GetCertificateExpiry(ctx, &identity.GetCertificateExpiryRequest{})
if err != nil { /* handle */ }
fmt.Println(exp.ExpiresAt, exp.Fingerprint, exp.CertTtlDays)
// Drive your own "cert expires in N days" warning off exp.ExpiresAt.
Unary IdentityService.SetCertificateTTL Set your next-rotation lifetime 🔒 mTLS

Stores the lifetime, in days, that the next RotateCertificate will request. Does not change the current certificate.

Request — SetCertificateTTLRequest

FieldTypeNotes
cert_ttl_daysint32requiredThe desired lifetime in days for your next rotation.

Response — SetCertificateTTLResponse

FieldTypeNotes
cert_ttl_daysint32optionalEcho of the value actually stored (the server may clamp it).

Errors

ReasonRaised when
INVALID_ARGUMENTcert_ttl_days is outside the allowed range

Examples

res, err := client.SetCertificateTTL(ctx, &identity.SetCertificateTTLRequest{
    CertTtlDays: 730, // preference applied to your NEXT rotation
})
if err != nil { /* handle */ }
fmt.Println("stored ttl days:", res.CertTtlDays) // echoed back
Unary IdentityService.RotateCertificate Rotate to a fresh certificate 🔒 mTLS

Issues a new mTLS certificate from a CSR you supply, and returns it with a grace window during which the old certificate is still accepted — so you can hot-reload without downtime. If the response never reaches you, send the same CSR again while the window is open: you get back the same certificate rather than a second one, and the certificate you are still presenting keeps working until you do.

⚠ Gotcha

Generate the new keypair yourself — the private key never leaves you. Install cert_pem, then reload your listeners within grace_period_seconds; the old certificate keeps working until the window closes, so connections never drop. Lost the response? Re-send the identical CSR, not a fresh one — a fresh CSR starts a second rotation and spends another of your daily allowance.

Request — RotateCertificateRequest

FieldTypeNotes
csr_pembytesrequiredA PEM-encoded PKCS#10 CSR for a keypair you generated.
immediate_cutoverbooloptionalRetires the old certificate the moment the new one is stored, leaving no window. Set it when the key you are replacing is compromised. It costs you the retry above: once the old certificate stops being accepted, a lost response leaves you nothing to connect with, and recovery has to come from your operator. A CSR carrying the key already pinned is refused when this is set.

Response — RotateCertificateResponse

FieldTypeNotes
cert_pembytesoptionalThe newly-issued PEM certificate to install.
issued_atstringoptionalRFC 3339 Nano; the new cert’s issuance.
expires_atstringoptionalRFC 3339 Nano; the new cert’s expiry.
fingerprintstringoptionalHex SHA-256 of the new cert.
grace_period_secondsint32optionalHow long the OLD cert stays accepted — your hot-reload window.

Errors

ReasonRaised when
INVALID_ARGUMENTthe CSR is malformed or its key is unacceptable

Examples

res, err := client.RotateCertificate(ctx, &identity.RotateCertificateRequest{
    CsrPem: csrPEM, // you generate a fresh keypair + CSR
})
if err != nil { /* handle */ }
install(res.CertPem)
// The OLD cert stays valid for res.GracePeriodSeconds — hot-reload
// your listeners inside that window to avoid dropping connections.
Opens on 01/01/2027

Available at launch

Until then, you can try the whole ceremony on your own phone.