Integration Guide
This guide covers best practices for integrating the Ownli API into your application. Whether you're building with React Native, Flutter, or any other framework, these patterns will help you ship a secure and reliable integration.
Architecture Overview
Your integration uses two types of tokens — admin tokens for backend operations and user tokens for your mobile app:
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ Mobile App │──(1)──▶│ Your Backend │──(2)──▶│ Ownli API │
│ │◀──(3)──│ │◀───────│ │
│ │──(4)──▶│ │ │ │
│ │────────┼─────────────────┼──(5)──▶│ │
└──────────────┘ └─────────────────┘ └─────────────┘
- App requests an Ownli token from your backend
- Your backend authenticates with Ownli using
scope: "admin"for backend operations, orscope: "user"for the app - Your backend returns the user-scoped token to the app
- Your backend calls admin endpoints directly (create users, list users, bulk operations) using the admin token
- App calls Ownli API directly with the user-scoped token for check-ins, rewards, photos, etc.
Your clientId, clientSecret, and partnerId must live on your server, never in the mobile app. These values can be extracted from app bundles regardless of platform (React Native, Flutter, native, etc.).
A user-scoped token cannot list all users, create users, or access admin endpoints — even if someone intercepts it from the app. This limits exposure to only what that user session needs. Admin tokens stay on your server where credentials are already protected.
1. Set Up Your Backend Token Management
Your backend manages two tokens:
- Admin token (
scope: "admin") — used by your backend for admin operations (create users, list users). Cache this and refresh when it expires. - User token (
scope: "user") — requested on behalf of your app users and returned to the mobile app. Restricted access only.
Create lightweight endpoints on your backend. This can be as simple as a couple of serverless functions (AWS Lambda, Google Cloud Function, etc.).
- Node.js / Express
- Python / Flask
const express = require('express');
const app = express();
const OWNLI_API = 'https://api.sandbox.ownli.app';
const CREDENTIALS = {
clientId: process.env.OWNLI_CLIENT_ID,
clientSecret: process.env.OWNLI_CLIENT_SECRET,
partnerId: process.env.OWNLI_PARTNER_ID,
};
// Internal: get an admin token for backend operations
let adminToken = null;
let adminTokenExpiresAt = 0;
async function getAdminToken() {
if (adminToken && Date.now() < adminTokenExpiresAt - 5 * 60 * 1000) {
return adminToken;
}
const response = await fetch(`${OWNLI_API}/api/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...CREDENTIALS, scope: 'admin' }),
});
const data = await response.json();
adminToken = data.access_token;
adminTokenExpiresAt = Date.now() + data.expires_in * 1000;
return adminToken;
}
// Endpoint for your mobile app: returns a user-scoped token
// Protect this with your own auth (e.g. Firebase, Auth0, etc.)
app.post('/api/ownli/token', async (req, res) => {
const response = await fetch(`${OWNLI_API}/api/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...CREDENTIALS, scope: 'user' }),
});
const data = await response.json();
res.json({
access_token: data.access_token,
expires_in: data.expires_in,
scope: data.scope,
});
});
import os
import time
import requests
from flask import Flask, jsonify
app = Flask(__name__)
OWNLI_API = "https://api.sandbox.ownli.app"
CREDENTIALS = {
"clientId": os.environ["OWNLI_CLIENT_ID"],
"clientSecret": os.environ["OWNLI_CLIENT_SECRET"],
"partnerId": os.environ["OWNLI_PARTNER_ID"],
}
# Internal: admin token for backend operations
_admin_token = None
_admin_token_expires_at = 0
def get_admin_token():
global _admin_token, _admin_token_expires_at
if _admin_token and time.time() < _admin_token_expires_at - 300:
return _admin_token
response = requests.post(
f"{OWNLI_API}/api/auth/token",
json={**CREDENTIALS, "scope": "admin"},
)
data = response.json()
_admin_token = data["access_token"]
_admin_token_expires_at = time.time() + data["expires_in"]
return _admin_token
# Endpoint for your mobile app: returns a user-scoped token
# Protect this with your own auth
@app.route("/api/ownli/token", methods=["POST"])
def get_user_token():
response = requests.post(
f"{OWNLI_API}/api/auth/token",
json={**CREDENTIALS, "scope": "user"},
)
data = response.json()
return jsonify({
"access_token": data["access_token"],
"expires_in": data["expires_in"],
"scope": data["scope"],
})
Protect your token relay endpoint with your own authentication. Only authenticated users of your app should be able to request an Ownli token.
2. Create Users When They Opt In
Create an Ownli user when the user accepts the Ownli terms of service in your app. This ensures you only create records for users who have consented to data sharing.
User creation requires an admin token (scope: "admin"). Your backend should call this endpoint — never the mobile app directly.
The flow:
- User navigates to the Ownli data-sharing screen in your app
- User accepts the Ownli Terms of Service
- Your app calls your backend, which calls
POST /api/usersusing the admin token
On the screen where the user opts in, place a line like this next to the primary action button:
By continuing, you agree to Ownli's Terms of Service and Privacy Policy.
Linking both documents at the moment of consent is what authorizes you to create the Ownli user and share their data — so keep the links exactly as shown and don't call POST /api/users until the user has proceeded past this screen.
- cURL
curl -X POST https://api.sandbox.ownli.app/api/users?returnExistingUserIfExists=true \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"phone": "+15556667777",
"address": {
"state": "CA"
}
}'
Key points:
- Always use
returnExistingUserIfExists=true— This makes the call idempotent. If the user already exists (e.g. they reinstalled the app), you'll get the existing user back instead of a409error. - Store the returned
id— Save the Ownli useridin your own database, mapped to your internal user. You'll need it for subsequent API calls. - User creation should happen server-side — Your backend should call the Ownli API to create the user, not the mobile app directly. This keeps your credentials secure and lets you reliably store the user mapping.
3. Grant Rewards from Your Own Actions (Optional)
If a user does something in your own systems that should earn them a reward — buying a device, completing an in-app purchase, migrating to a new app version, completing a referral tracked on your side — you can mint a reward directly with POST /api/rewards/grant. The reward then appears in the user's reward list and can be claimed through the standard claim flow, just like a reward earned from a check-in.
Granting rewards requires an admin token (scope: "admin"). This is server-to-server only — your mobile app must never call this endpoint, because a compromised user-scoped token would otherwise be able to mint arbitrary rewards. A call with a user-scoped JWT receives 403 INSUFFICIENT_SCOPE.
This endpoint is freely available in sandbox so you can build and test the integration. In production, it is gated and only enabled for partners under a commercial agreement with Ownli that covers budget prepayment and clawback terms — because every successful call mints real economic value owed to the user. Reach out to Ownli before pointing this flow at the production base URL; production calls without an active agreement are rejected.
Idempotency. Always send a clientReference that uniquely identifies the action on your side (your order ID, transaction ID, event ID — whatever is stable and unique per partner). A repeat call with the same clientReference returns the existing reward instead of minting a new one, so retries are safe. A repeat with the same clientReference but a different userId or amount returns 409 ERROR_REWARD_CONFLICT — that is your signal that you've reused a reference for a different action by mistake.
actionCode is a free-form label you choose (e.g. device-purchase, app-migration, referral-completed). Ownli does not validate or enforce it; it's stored on the reward purely for your own grouping and reporting in the rewards list endpoints.
- cURL
curl -X POST https://api.sandbox.ownli.app/api/rewards/grant \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-d '{
"userId": "XT_USRabc123",
"amount": 25.00,
"actionCode": "device-purchase",
"clientReference": "order-12345"
}'
A successful response returns the granted reward including an idempotentReplay flag — true if this call returned a previously-created reward instead of minting a new one.
To inspect what you've granted, the existing GET /api/rewards endpoint accepts an actionCode query parameter (exact match) so you can pull all rewards for a given action — for example GET /api/rewards?actionCode=device-purchase returns every reward minted under that label, paginated and scoped to your dataBuyerId.
{
"id": "XT_DBRabc123",
"userId": "XT_USRabc123",
"amount": 25.00,
"type": "Custom",
"actionCode": "device-purchase",
"clientReference": "order-12345",
"status": "Confirmed",
"dateRewarded": "2026-04-25T10:15:30Z",
"idempotentReplay": false
}
4. Handle Token Refresh
Ownli JWTs expire after 1 hour. Your app should handle the user-scoped token refresh gracefully. Your backend manages its own admin token refresh internally (see the code samples in Step 1).
Proactive refresh — Track when the token was issued and request a new one before it expires.
class OwnliTokenManager {
constructor(fetchTokenFromBackend) {
this.fetchToken = fetchTokenFromBackend;
this.token = null;
this.expiresAt = 0;
}
async getToken() {
// Refresh if token expires within 5 minutes
if (!this.token || Date.now() >= this.expiresAt - 5 * 60 * 1000) {
const { access_token, expires_in } = await this.fetchToken();
this.token = access_token;
this.expiresAt = Date.now() + expires_in * 1000;
}
return this.token;
}
}
// Usage — fetches a user-scoped token from your backend
const tokenManager = new OwnliTokenManager(async () => {
const res = await fetch('https://your-backend.com/api/ownli/token', {
method: 'POST',
headers: { /* your own auth headers */ },
});
return res.json();
});
// Before any Ownli API call from the app
const token = await tokenManager.getToken();
401 fallback — If a request returns 401 Unauthorized, request a fresh token and retry once. If you receive 403 Forbidden with INSUFFICIENT_SCOPE, the endpoint requires an admin token and should be called from your backend instead.
async function callOwnliApi(path, options = {}) {
let token = await tokenManager.getToken();
let response = await fetch(`https://api.sandbox.ownli.app${path}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
// If unauthorized, refresh token and retry once
if (response.status === 401) {
tokenManager.token = null; // force refresh
token = await tokenManager.getToken();
response = await fetch(`https://api.sandbox.ownli.app${path}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
}
return response;
}
Putting It All Together
Here's the typical integration flow from start to finish:
| Step | Where | Token | What happens |
|---|---|---|---|
| 1 | Your backend | — | Store Ownli credentials (clientId, clientSecret, partnerId) as environment variables |
| 2 | Your backend | Admin | Fetch an admin token (scope: "admin") and cache it for backend operations |
| 3 | Your backend | — | Create a /api/ownli/token endpoint that returns user-scoped tokens to your app |
| 4 | Your backend | Admin | When user accepts Ownli TOS, create the Ownli user using the admin token |
| 5 | Your backend | — | Store the Ownli user id mapped to your internal user |
| 6 | Mobile app | User | Request a user-scoped token via your backend's token endpoint |
| 7 | Mobile app | User | Call Ownli API directly for check-ins, rewards, photos, vehicle lookups |
| 8 | Mobile app | User | Refresh the token proactively or on 401 |
| 9 | Your backend | Admin | (Optional) When the user completes a partner-side action that should earn a reward, call POST /api/rewards/grant with a clientReference so retries are safe |
What each token can do
| Operation | Admin token | User token |
|---|---|---|
| Create users | Yes | No |
| List all users | Yes | No |
| Create vehicles | Yes | No |
| List all vehicles / rewards / payouts | Yes | No |
Grant rewards from external actions (/api/rewards/grant) | Yes | No |
| Look up a user by ID, phone, email | Yes | Yes |
| Submit check-ins (mileage, condition, etc.) | Yes | Yes |
| View rewards for a user | Yes | Yes |
| Upload photos / files | Yes | Yes |
| Claim rewards | Yes | Yes |
Endpoints that require an admin token are marked "Admin Token Required" in the API reference.
Sandbox vs. Production
| Sandbox | Production | |
|---|---|---|
| Base URL | https://api.sandbox.ownli.app | https://api.ownli.app |
| Credentials | Test credentials | Production credentials |
| Data | Test data only | Real user data |
Start with the sandbox environment. When you're ready to go live, swap the base URL and credentials — no code changes needed.