IdentityService
Resolve verified provider logins to canonical users. Your app forwards the credential it obtained from the provider's own login flow; the API verifies it with the provider and returns the matching user, creating a new UNAPPROVED user if nobody matches. This service authenticates with API key only and does not require an on_behalf_of user ID.
There are two trust models for identifying users:
- Assertion (
On-Behalf-Ofheader): your app authenticates users itself and asserts identity via theOn-Behalf-Ofheader. Appropriate for trusted clients such as bots. - Verified (this service): your app forwards the provider credential to the API, which verifies it with the provider. Use the returned
user.idinOn-Behalf-Offor subsequent requests.
Usage
from vclient import identity_service
svc = identity_service(company_id="COMPANY_ID")
Methods
| Method | Returns | Description |
|---|---|---|
identify(request=None, **kwargs) |
IdentityResolution |
Resolve a verified provider login to a user |
identify() Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
provider |
IdentityProvider |
Yes | One of "apple", "google", "discord", "github" |
token |
str |
Yes | OIDC ID token (apple/google) or OAuth access token (discord/github) |
username |
str \| None |
No | Username to use only if a new user is created |
email |
str \| None |
No | Email to use on create, only if the provider supplied none |
Resolution Order
The API tries the following steps in order and reports which one succeeded in the resolution field:
matched: an existing user already holds this provider identity.linked: the provider supplied a verified email that matches an existing user, so the identity was attached to that user.created: a newUNAPPROVEDuser was registered.
Errors
| Error | Status | code |
Meaning |
|---|---|---|---|
UnprocessableEntityError |
422 | TOKEN_VERIFICATION_FAILED |
The provider rejected the credential |
UnprocessableEntityError |
422 | EMAIL_REQUIRED |
Creation needs an email the provider didn't send |
ServerError |
503 | PROVIDER_UNAVAILABLE |
The provider could not be reached |
Audience Registration
Apple and Google tokens carry an audience claim identifying the app they were issued to (for example, a bundle ID like com.example.iosapp or an OAuth client ID). The API accepts a token only when its audience appears in the combined allowlist formed by two sources:
- The server's own environment allowlists (configured by the platform operator).
- The per-developer audiences you register on your developer profile via
update_me(provider_audiences={"apple": [...], "google": [...]})on the Developer Service.
If your app's identifier is absent from both sources, identify() raises UnprocessableEntityError with code TOKEN_VERIFICATION_FAILED. Register your bundle ID or client ID once, and the API will accept tokens from that app.
Discord and GitHub use OAuth access tokens, which do not carry audience claims, so they are not affected by this requirement.
Examples
Identify a Provider Login (Async)
from vclient import VClient
async with VClient(base_url="https://api.valentina-noir.com", api_key="...") as client:
svc = client.identity(company_id="COMPANY_ID")
result = await svc.identify(provider="google", token=google_id_token)
print(f"{result.resolution}: {result.user.username}")
# Use the canonical user for subsequent requests
users = client.users(result.user.id, company_id="COMPANY_ID")
Identify a Provider Login (Sync)
from vclient import SyncVClient
with SyncVClient(base_url="https://api.valentina-noir.com", api_key="...") as client:
svc = client.identity(company_id="COMPANY_ID")
result = svc.identify(provider="apple", token=apple_id_token)
Handle a Rejected Token
from vclient.exceptions import UnprocessableEntityError
try:
result = await svc.identify(provider="apple", token=token)
except UnprocessableEntityError as e:
if e.code == "TOKEN_VERIFICATION_FAILED":
... # ask the user to sign in again
elif e.code == "EMAIL_REQUIRED":
result = await svc.identify(provider="apple", token=token, email=collected_email) # prompt the user for an email address, then retry
Related Documentation
- Users -
link_identity()for connect-your-account flows, and the merge workflow for cleaning up pre-existing duplicates