Complete TOTP factor enrollment
/v1/identity/auth/mfa/totp/enroll/verifyAuthentication
- Bearer Token
AuthorizationJWT access token
Request body
enrollment_token *stringThe sealed enrollment_token returned by the `start` endpoint. Carries the provisional TOTP secret server-side.
code *string6-digit code from the authenticator app.
label *stringUser-facing nickname for the factor ("iPhone 15", "Work Laptop"). Shown on the factor-management surface.
Code samples
curl -X POST "https://api.canopy.dev/v1/identity/auth/mfa/totp/enroll/verify" \
-H "Authorization: Bearer $CANOPY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enrollment_token": "string",
"code": "123456",
"label": "iPhone 15"
}'const response = await fetch("https://api.canopy.dev/v1/identity/auth/mfa/totp/enroll/verify", {
method: "POST",
headers: {
"Authorization": "Bearer $CANOPY_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
"enrollment_token": "string",
"code": "123456",
"label": "iPhone 15"
}),
});
const data = await response.json();import requests
response = requests.post(
"https://api.canopy.dev/v1/identity/auth/mfa/totp/enroll/verify",
headers={
"Authorization": "Bearer $CANOPY_TOKEN",
"Content-Type": "application/json"
},
json={
"enrollment_token": "string",
"code": "123456",
"label": "iPhone 15",
},
)
data = response.json()package main
import (
"bytes"
"encoding/json"
"net/http"
)
func main() {
payload := map[string]interface{}{
"enrollment_token": "string",
"code": "123456",
"label": "iPhone 15",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.canopy.dev/v1/identity/auth/mfa/totp/enroll/verify", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer $CANOPY_TOKEN")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}Responses
200 Verifies a 6-digit code against the secret carried by the enrollment token and persists the factor. Returns the 10 single-use recovery codes — this is the only time they are shown in plaintext, and only on the first factor enrollment (subsequent enrollments return `recovery_codes: null` so the existing batch stays valid).
{
"factor": {
"id": "00000000-0000-0000-0000-000000000000",
"type": "totp",
"label": "iPhone 15",
"enrolled_at": "2026-04-20T12:00:00.000Z",
"last_used_at": "2026-04-20T12:00:00.000Z"
},
"recovery_codes": [
"ABCD-EFGH-IJKL-MNOP",
"QRST-UVWX-YZ23-4567",
"..."
],
"recovery_codes_generation": 0
}application/json
factor *MfaFactorResponseDtorecovery_codes *string[]Ten single-use recovery codes. **Shown exactly once** — the server only stores hashes. Display, allow copy/download, and proceed only after the user confirms they have saved them.
recovery_codes_generation *numberMonotonically-increasing generation number for this batch. Used by the portal admin surface to show "X of 10 remaining" against the current batch.
400 Enrollment token is missing, malformed, or expired
401 Invalid or expired token
403 This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)