Turnkey identity
The optional layer that removes the "how do users log in?" problem. Your client talks to Ollacore directly: phone number, one-time code, contacts, conversations, tokens. No app-server of your own.
1 · Provision an app #
A directory app is a public app_id (safe to embed in your mobile build, like a
Firebase project id) plus how OTPs get delivered. It resolves the tenant for every unauthenticated
directory call.
curl -X POST https://api.example.com/v1/account/directory-apps \
-H "Authorization: Bearer as_…" \
-H "Content-Type: application/json" \
-d '{"delivery_webhook_url":"https://your-server.example.com/otp"}'
# → { "app_id": "da_9f2c…", ← public, embed in your app
# "delivery_mode": "webhook",
# "delivery_secret": "…" } ← shown once; verifies our signature Or let us call your SMS vendor with credentials you supply:
-d '{"provider":{"kind":"twilio","config":{
"account_sid":"AC…","auth_token":"…","from":"+15550001111",
"message_template":"Your code is {code}"}}}'
# kinds: twilio | plivo | msg91 2 · Deliver the code #
In webhook mode we generate and verify the code but you put it on the wire. The plaintext code never persists on our side — only a hash — so this POST is your one chance to send it.
POST https://your-server.example.com/otp
x-ollacore-signature: t=1757500000,v1=<hex hmac-sha256>
{ "app_id": "da_9f2c…", "phone": "+14155550123",
"code": "418322", "expires_at": "2026-09-10T11:09:00Z" }
# Verify: HMAC-SHA256(delivery_secret, "<t>.<raw body>") == v1, then send by SMS. 3 · Sign the user in #
POST /v1/directory/otp/request { "app_id": "da_…", "phone": "+14155550123" }
# → { "dispatched": true, "expires_at": "…" } (uniform — never reveals if registered)
POST /v1/directory/otp/verify { "app_id": "da_…", "phone": "+14155550123", "code": "418322" }
# → { "session_token": "du_…", "user_id": "…", "display_name": null } - Codes are argon2-hashed, expire in 5 minutes, and allow at most 5 attempts.
- Rate limited three ways — per phone, per IP and per app. The per-app cap is what blunts SMS-pumping fraud, which would otherwise run up your vendor bill.
- Responses are uniform: requesting a code never reveals whether the number is registered.
- The returned
du_…session is opaque, hashed at rest, and long-lived.
4 · Profile and contacts #
GET /v1/directory/me # { id, phone, display_name }
PATCH /v1/directory/me # { "display_name": "Alice" }
POST /v1/directory/contacts/lookup # { "phones": ["+1…", …] } ≤ 500
# → { "contacts": [ { user_id, phone, display_name } ] } registered subset, this tenant only Contact lookup is scoped to the caller's tenant, so one customer's user base is never discoverable from another's app.
5 · Conversations #
POST /v1/directory/conversations/direct { "peer_user_id": "…" }
# → { room_id, kind: "direct" } idempotent: same pair always returns the same room
POST /v1/directory/conversations/group { "member_user_ids": ["…"], "name": "Team" }
GET /v1/directory/conversations # room_id, kind, name, created_at
GET /v1/directory/conversations/{room_id}/token # → the room-token bundle Directory conversations are ordinary rooms — everything in Chat and Calls applies unchanged once you hold the token. Peers must be directory users of the same tenant; anything else is refused.
6 · The inbox #
The chat list, in one request: peer or group name, last-message preview, unread count, newest first.
GET /v1/directory/inbox?limit=50
{
"conversations": [
{ "room_id": "0f5c…", "kind": "direct", "name": null,
"peer": { "user_id": "…", "display_name": "Bob", "phone": "+1…" },
"unread_count": 3,
"last_message": { "event_seq": 4131, "sender_id": "…", "kind": "text",
"preview": "see you at six", "created_at": "…" } }
]
} 7 · Devices #
Register each device's push token so the user can be reached when offline — see Push & ringing. Tokens are stamped with the app they were registered under, which selects the right FCM/APNs credentials at send time.
What it does not do #
- No avatars, block/mute, or account deletion yet.
- No session refresh/rotation — sessions are long-lived instead.
- Group membership is set at creation; there is no add/remove-after-the-fact on the directory plane (the server plane can do it).
- Credentials you store (SMS vendor, FCM, APNs) are held in plaintext at rest, matching the rest of the platform's secret posture. There is no encryption-at-rest layer yet.