Build it

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.

Opt-in, per tenant. If you never provision a directory app, none of this exists and the backend stores no end-user PII. Turning it on is what makes phone numbers and display names appear.

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.
We hold no telecom account. DLT registration, sender ids and carrier relationships stay entirely yours. That is deliberate: it keeps you in control of deliverability and compliance in your market.

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.