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. 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 |
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 |
| 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.