Validate credential
Confirm a credential with its one-time code. A `cred_id` that names nothing, or names a credential belonging to somebody else, is refused exactly as a wrong code is rather than reported as missing, and never reaches that credential's attempt budget. The same holds for a credential whose value another account has already confirmed. `400 otp_m.otp_invalid`, `400 otp_m.otp_expired` and `429 otp_m.cooling_period_active` are therefore the whole refusal vocabulary of this endpoint.
Authorization
bearerAuth In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
ValidateCredentialInput: the credential is addressed by user_id and cred_id, not by credential_id.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/v1/users/credentials/validate" \ -H "Content-Type: application/json" \ -d '{ "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "cred_id": "0a7f530e-cca8-4c90-8518-dced55521236", "otp": "string" }'{
"message": "Your account has been successfully verified and activated.",
"status": "success"
}{
"message": "Invalid OTP. 2 attempt(s) left",
"status": "invalid"
}{
"status": 401,
"message": "Unauthorized",
"code": "common.unauthorized",
"class": "business"
}{
"status": 403,
"message": "No access to the record",
"code": "common.rbac_no_rec_access",
"class": "business"
}{
"message": "Credential already exists",
"status": "duplicate"
}{
"message": "In cooling period for 1 minute, 60 second(s) left",
"status": "cooling",
"wait": 60
}{
"message": "An error occurred while verifying the code",
"status": "error"
}{
"overall_status": "unhealthy",
"message": "Service is shutting down",
"timestamp": "2026-08-27T15:04:05Z"
}Add a credential to a user. A value already confirmed on another account answers exactly as a free one does, with the same `201` and the same body, and is stored unvalidated exactly as a free one is — the partial unique index permits the row, and no code that could confirm it is ever sent. The owner of that value is notified instead. A value the caller already holds on another credential of their own still answers `409 users_m.duplicate_credential` — that is their own account. Creating a credential for another user requires the caller to be a scoped administrator in the target's customer context, otherwise `403`, and `404` when the target does not exist. With `send_otp: true` the endpoint issues a one-time code, so the OTP refusals reach this path: `429 otp_m.cooling_period_active`, `500 otp_m.failed_send_otp`, `500 otp_m.cache_error`. Send pacing is the exception: it issues no second code but still answers success while the credential holds a usable one, with `code_sent: false` and the `retry_after` to wait. Only where no code is left does it answer `429 otp_m.resend_too_soon` or `429 otp_m.resend_limit_exceeded`.
Get all credentials