Authentication
All Identity API endpoints use HTTP Basic Auth. Pass your clientId as the username and clientSecret as the password on every request.
curl https://api.sandbox.sardine.ai/v1/identity/... \
-u "$CLIENT_ID:$CLIENT_SECRET"
Never make these calls from a browser or mobile client. Your clientSecret must remain server-side only.
Step 1 — Check if the customer exists
Before creating a new customer, check whether one already exists for the user’s phone number to avoid duplicate records.
curl -X POST https://api.sandbox.sardine.ai/v1/identity/entities/search \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{ "phoneNumber": "+14155551234" }'
If customerId is returned, the customer already exists — skip to Step 3. If the response is empty, proceed to Step 2.
Step 2 — Create the customer
Register the user with their phone number. The customerId returned here is your durable reference to this user in all subsequent API calls.
curl -X POST https://api.sandbox.sardine.ai/v1/identity/entities \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{ "phoneNumber": "+14155551234" }'
{
"customerId": "3f8c1a22-1234-4abc-9def-000000000001",
"createdAt": "2026-06-01T10:00:00Z"
}
Store customerId against your user record in your own database.
Call this endpoint from your backend to get a hosted widget URL for the user. Choose the right flow and scope for your use case.
curl -X POST https://api.sandbox.sardine.ai/v1/identity/consents/widget \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"customerId": "3f8c1a22-1234-4abc-9def-000000000001",
"successUrl": "https://yourapp.com/kyc/success",
"manualKycUrl": "https://yourapp.com/kyc/manual",
"scope": ["profile", "doc_kyc"],
"flow": "kyc_input"
}'
{
"widgetUrl": "https://identity.sardine.ai/?client_token=abc123&consent_id=xyz789&success_url=..."
}
Choosing a flow
| Flow | When to use |
|---|
kyc_input | New users, or when you need the user to verify additional scopes not yet on file |
kyc_sharing | User has already been verified by another Sardine partner and you want them to consent to share that identity with you |
Choosing scopes
| Scope | What it collects |
|---|
profile | Name, date of birth, address, email, phone number |
doc_kyc | Government ID scan (front + back) and biometric liveness check |
liveness | Liveness check only |
ssn | Social Security Number (US users, when required) |
Pass multiple scopes together: ["profile", "doc_kyc"] is the standard full-KYC combination.
manualKycUrl
Provide a manualKycUrl as a fallback destination if the user cannot be verified automatically (e.g. document scan quality is too low). This is optional but recommended for production.
Step 4 — Redirect the user
Return the widgetUrl to your frontend and redirect the user to it, or embed it in an iframe.
// Server response to your frontend
res.json({ widgetUrl: data.widgetUrl });
// Frontend redirect
window.location.href = widgetUrl;
The widget handles all verification steps. When the user completes (or abandons) the flow, they are redirected to your successUrl.
Step 5 — Retrieve the verified identity
After the user returns to your successUrl, call this endpoint from your backend to retrieve the verified data.
curl https://api.sandbox.sardine.ai/v1/identity/entities/3f8c1a22-1234-4abc-9def-000000000001 \
-u "$CLIENT_ID:$CLIENT_SECRET"
Response
{
"profile": {
"userId": "3f8c1a22-1234-4abc-9def-000000000001",
"clientId": "your-client-id",
"consentId": "b1c2d3e4-5678-4abc-9def-000000000002",
"consentedAt": "2026-06-01T10:30:00Z",
"revokedAt": null,
"primaryIdentity": true,
"fullName": "Jane Smith",
"dateOfBirth": "1990-06-15",
"emailAddress": "jane@example.com",
"phoneNumber": "+14155551234",
"address": {
"street": "123 Main St",
"city": "San Francisco",
"region": "CA",
"postalCode": "94105"
}
},
"documentData": {
"documentType": "DRIVERS_LICENSE",
"documentNumber": "D1234567",
"dateOfBirth": "1990-06-15",
"expiryDate": "2028-06-15",
"issuingCountry": "US",
"firstName": "Jane",
"lastName": "Smith"
},
"documentKyc": {
"front": "<base64-encoded image>",
"back": "<base64-encoded image>",
"selfie": "<base64-encoded image>"
}
}
What’s included per scope
| Field | Requires scope |
|---|
profile | profile |
documentData | doc_kyc |
documentKyc (images) | doc_kyc |
Fields outside the consented scopes are returned as null.
Consent states
If the identity belongs to another client (i.e. kyc_sharing flow), the API enforces consent state:
| State | Behaviour |
|---|
| Consent not found | 400 Consent not found |
| Consent pending (user has not approved yet) | 400 Pending user consent |
| Consent revoked | 400 Consent has been revoked |
| Consent active | 200 with full data |
Error reference
| Status | Message | Resolution |
|---|
400 | Phone number is required | Include phoneNumber in the request body |
400 | Customer already exists | Use the existing customerId from /identity/entities/search |
400 | Customer ID is required | Include customerId in the widget request |
400 | Customer not found for customer ID | Verify the customerId was created by this client |
400 | Invalid scope | Use one of: profile, doc_kyc, liveness, ssn |
400 | User already consented to this flow | The user has already completed kyc_input for this client |
401 | Unauthorized | Check that clientId and clientSecret are correct and being sent as Basic Auth |
Go to production
- Test the full flow end-to-end in sandbox.
- Confirm identity data is retrieved correctly after widget completion.
- Contact your Sardine integration contact to complete the review.
- Swap
api.sandbox.sardine.ai → api.sardine.ai and replace with production credentials.