# Onairos SDK API Changes - Developer Guide

**Version:** 2026-04-24  
**Status:** Live  

**Training status URL:** Prefer **`GET https://api2.onairos.uk/training/status`** (Bearer token). Legacy alias **`/mobile-training/status`** is identical. With `environment: 'development'`, the SDK uses **`https://dev-api.onairos.uk`** as the API base; the backend’s `training.statusUrl` in the authorize response is authoritative—always poll that URL if present.

---

## Summary of Changes

This document outlines the API response changes and new background training feature for developers integrating with Onairos.

---

## 1. API Response Format Changes

### Authorization Response (getAPIurl / getAPIurlMobile)

**OLD Format:**
```json
{
  "apiUrl": "https://api2.onairos.uk/combinedInference",
  "token": "eyJ...",
  "userData": {
    "email": "user@example.com",
    "username": "johndoe",
    "connectedPlatforms": ["linkedin", "youtube"],
    "isReturningUser": true
  }
}
```

**NEW Format:**
```json
{
  "apiUrl": "https://api2.onairos.uk/combinedInference",
  "token": "eyJ...",
  "authorizedData": {
    "traits": true,
    "personality": true,
    "rawMemories": false
  },
  "rawMemoriesPolicy": {
    "offered": false,
    "code": "APP_NOT_APPROVED_FOR_RAW_MEMORIES"
  },
  "traitIntent": { },
  "training": {
    "ready": true,
    "statusUrl": "https://api2.onairos.uk/training/status",
    "pollIntervalMs": 5000
  }
}
```

### Key Changes:

| Old Field | New Field | Notes |
|-----------|-----------|-------|
| `userData.email` | **REMOVED** | Security: No PII exposed |
| `userData.username` | **REMOVED** | Security: No PII exposed |
| `userData.connectedPlatforms` | **REMOVED** | Available in traits response |
| `userData.isReturningUser` | **REMOVED** | Use `training.ready` instead |
| *(new)* | `authorizedData` | What data types user authorized (`rawMemories` is authoritative for LLM/raw export) |
| *(new)* | `rawMemoriesPolicy` | UX hint (`offered`, `code`) — SDK hides raw UI when `offered: false` |
| *(new)* | `traitIntent` | Training/inference intent from handoff (no separate resolve call) |
| `authorizedData.llmData` | **legacy** | Read only when `rawMemories` is absent during backend transition |
| *(new)* | `training.ready` | `true` = fetch now, `false` = poll first |
| *(new)* | `training.statusUrl` | Endpoint to check training progress |
| *(new)* | `training.pollIntervalMs` | Recommended polling interval |

---

## 2. Traits/Data Response Changes

### Data Response (combinedInference, traitsOnly, etc.)

**NEW Format:**
```json
{
  "InferenceResult": { "output": [[0.8, 0.2]] },
  "traits": {
    "positive_traits": { "Curious": 92, "Analytical": 88 },
    "traits_to_improve": { "Patience": 45 },
    "user_summary": "You're a thoughtful technologist...",
    "top_traits_explanation": "Your engagement patterns show...",
    "archetype": "Strategic Thinker",
    "nudges": [
      { "text": "Try journaling a decision you're mulling over" }
    ]
  },
  "userProfile": {
    "user_summary": "You're a thoughtful technologist...",
    "top_traits_explanation": "Your engagement patterns show...",
    "archetype": "Strategic Thinker",
    "nudges": [...]
  },
  "connectedPlatforms": ["linkedin", "youtube"]
}
```

### Key Additions:

| Field | Description |
|-------|-------------|
| `traits.user_summary` | 2-3 paragraph personality summary |
| `traits.top_traits_explanation` | Why these traits were identified |
| `traits.archetype` | 2-3 word personality archetype |
| `traits.nudges` | Personalized action suggestions |
| `userProfile` | Convenient access to profile fields |

---

## 3. Background Training Mode

### Overview

When a user authorizes your app, their traits may need to be generated (training). You have two options:

1. **Wait Mode** (default): User sees loading screen while training completes
2. **Background Mode**: SDK returns immediately, you poll for completion

### Web npm SDK (`backgroundLoadData`)

On **`OnairosButton`**, set **`backgroundLoadData={true}`** with **`autoFetch={true}`** so that after connections/PIN/consent the SDK **skips the built-in training loading screen**, navigates to the data-request step, and completes with **`onComplete({ apiUrl, token, training, authorizedData, ... })`** while traits may still be generating. The SDK starts training in the background (socket **`start-training`**) so status polling is meaningful. Your app should:

1. Read **`training.ready`**. If `true`, **`POST apiUrl`** with `Authorization: Bearer <token>`.
2. If `false`, call **`pollTrainingStatus`** from **`onairos`** (or your own loop) on **`training.statusUrl`** until ready, then **`POST apiUrl`**.

See **`docs/CROSS_SDK_PARITY.md`** and **`docs/ONAIROS_BUTTON_PROPS.md`** for the full matrix with **`autoFetch`**.

### When to Use Background Mode

- User experience is time-sensitive
- You want to show your app UI immediately
- You'll fetch traits later (e.g., on next screen)

---

## 4. Implementation Guide

### Step 1: Check if Traits Are Ready

After authorization, check `training.ready`:

```javascript
const authResult = await onairosSDK.authorize();

if (authResult.training.ready) {
  // Traits exist - fetch immediately
  const userData = await fetchTraits(authResult.apiUrl, authResult.token);
  displayUserData(userData);
} else {
  // Training in progress - poll for completion
  showLoadingState();
  const userData = await pollForTraits(authResult);
  displayUserData(userData);
}
```

### Step 2: Fetch Traits (When Ready)

```javascript
async function fetchTraits(apiUrl, token) {
  const response = await fetch(apiUrl, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
  
  if (!response.ok) {
    throw new Error(`Failed to fetch traits: ${response.status}`);
  }
  
  return await response.json();
}
```

### Step 3: Poll for Training Completion (When Not Ready)

Use each response’s **`polling.recommendedIntervalMs`** when present; otherwise fall back to **`training.pollIntervalMs`** from the authorize payload. On **HTTP 429**, honor **`Retry-After`** (header or seconds) and any **`recommendedIntervalMs`** / **`retryAfter`** in the JSON body before retrying.

```javascript
async function pollForTraits(authResult) {
  const { apiUrl, token, training } = authResult;
  const { statusUrl, pollIntervalMs } = training;
  
  const maxWaitMs = 300000; // 5 minutes max
  const startTime = Date.now();
  
  while (Date.now() - startTime < maxWaitMs) {
    const res = await fetch(statusUrl, {
      headers: { 'Authorization': `Bearer ${token}` }
    });

    if (res.status === 429) {
      const errBody = await res.json().catch(() => ({}));
      const retryAfterHeader = res.headers.get('Retry-After');
      const waitSec = retryAfterHeader ? parseInt(retryAfterHeader, 10) : (errBody.retryAfter ?? 5);
      await sleep(Math.max((waitSec || 5) * 1000, errBody.recommendedIntervalMs || 0));
      continue;
    }

    const status = await res.json();
    
    // Update UI with progress (optional)
    if (status.progress) {
      updateProgressBar(status.progress.progressPercent);
      updateETA(`${status.progress.remainingSeconds}s remaining`);
      updateMessage(status.progress.message);
    }
    
    // Check if training complete
    if (!status.trainingStatus.isCurrentlyTraining) {
      // Training done! Fetch traits
      return await fetchTraits(apiUrl, token);
    }
    
    const waitMs = status.polling?.recommendedIntervalMs ?? pollIntervalMs ?? 5000;
    await sleep(waitMs);
  }
  
  throw new Error('Training timeout - please try again');
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
```

---

## 5. Training Status Response

### Endpoint

```
GET https://api2.onairos.uk/training/status
Authorization: Bearer <token>
```

Legacy path (same behavior): `GET https://api2.onairos.uk/mobile-training/status`

### Response (During Training)

```json
{
  "success": true,
  "trainingStatus": {
    "isCurrentlyTraining": true,
    "canTrainAgain": false,
    "lastTrainingDate": "2026-03-09T05:30:00Z",
    "minutesSinceLastTraining": 2
  },
  "progress": {
    "progressPercent": 45,
    "elapsedSeconds": 54,
    "estimatedTotalSeconds": 120,
    "remainingSeconds": 66,
    "stage": "processing",
    "message": "Analyzing your data..."
  },
  "polling": {
    "recommendedIntervalMs": 3000,
    "maxPollTimeMs": 300000
  }
}
```

### Response (Training Complete)

```json
{
  "success": true,
  "trainingStatus": {
    "isCurrentlyTraining": false,
    "canTrainAgain": true,
    "lastTrainingDate": "2026-03-09T05:32:00Z"
  },
  "progress": null,
  "polling": {
    "recommendedIntervalMs": 10000,
    "maxPollTimeMs": 300000
  }
}
```

### Field Reference

| Field | Type | Description |
|-------|------|-------------|
| `trainingStatus.isCurrentlyTraining` | boolean | `true` = still training, keep polling |
| `trainingStatus.canTrainAgain` | boolean | Whether user can retrain |
| `progress.progressPercent` | number | 0-100, completion percentage |
| `progress.remainingSeconds` | number | Estimated seconds until complete |
| `progress.message` | string | Human-readable status message |
| `polling.recommendedIntervalMs` | number | How often to poll (ms) |

---

## 6. Rate Limits

| Endpoint | Limit | Response if Exceeded |
|----------|-------|---------------------|
| `/training/status` (legacy: `/mobile-training/status`) | 30 requests/minute | `429 Too Many Requests` |

If you receive a `429` response:
```json
{
  "success": false,
  "error": "Too many requests. Please slow down polling.",
  "retryAfter": 60,
  "recommendedIntervalMs": 5000
}
```

Check the `Retry-After` header and wait before retrying.

---

## 7. Complete Example

### React/JavaScript

```javascript
import { useState, useEffect } from 'react';

function OnairosIntegration() {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [progress, setProgress] = useState(null);
  const [error, setError] = useState(null);

  async function handleAuthorization(authResult) {
    setLoading(true);
    setError(null);
    
    try {
      if (authResult.training.ready) {
        // Traits ready - fetch immediately
        const data = await fetchTraits(authResult.apiUrl, authResult.token);
        setUserData(data);
      } else {
        // Training in progress - poll
        const data = await pollForTraits(authResult);
        setUserData(data);
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
      setProgress(null);
    }
  }

  async function fetchTraits(apiUrl, token) {
    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${token}` }
    });
    return response.json();
  }

  async function pollForTraits(authResult) {
    const { apiUrl, token, training } = authResult;
    const maxWait = 300000;
    const start = Date.now();
    
    while (Date.now() - start < maxWait) {
      const status = await fetch(training.statusUrl, {
        headers: { 'Authorization': `Bearer ${token}` }
      }).then(r => r.json());
      
      // Update progress UI
      if (status.progress) {
        setProgress({
          percent: status.progress.progressPercent,
          message: status.progress.message,
          eta: status.progress.remainingSeconds
        });
      }
      
      // Check if done
      if (!status.trainingStatus.isCurrentlyTraining) {
        return await fetchTraits(apiUrl, token);
      }
      
      // Wait
      await new Promise(r => setTimeout(r, status.polling.recommendedIntervalMs));
    }
    
    throw new Error('Training timeout');
  }

  return (
    <div>
      {loading && progress && (
        <div className="progress">
          <div className="progress-bar" style={{ width: `${progress.percent}%` }} />
          <p>{progress.message}</p>
          <p>ETA: {progress.eta} seconds</p>
        </div>
      )}
      
      {userData && (
        <div className="user-profile">
          <h2>{userData.userProfile.archetype}</h2>
          <p>{userData.userProfile.user_summary}</p>
          
          <h3>Your Traits</h3>
          {Object.entries(userData.traits.positive_traits).map(([trait, score]) => (
            <div key={trait}>
              <span>{trait}</span>
              <span>{score}%</span>
            </div>
          ))}
        </div>
      )}
      
      {error && <p className="error">{error}</p>}
    </div>
  );
}
```

### Python

```python
import time
import requests

def get_user_traits(auth_result):
    """
    Get user traits, polling if training is in progress.
    """
    api_url = auth_result['apiUrl']
    token = auth_result['token']
    training = auth_result['training']
    
    headers = {'Authorization': f'Bearer {token}'}
    
    # If traits ready, fetch immediately
    if training['ready']:
        response = requests.post(api_url, headers=headers)
        return response.json()
    
    # Otherwise, poll for training completion
    status_url = training['statusUrl']
    poll_interval = training['pollIntervalMs'] / 1000  # Convert to seconds
    max_wait = 300  # 5 minutes
    start_time = time.time()
    
    while time.time() - start_time < max_wait:
        # Check status
        status = requests.get(status_url, headers=headers).json()
        
        # Show progress
        if status.get('progress'):
            print(f"Progress: {status['progress']['progressPercent']}%")
            print(f"ETA: {status['progress']['remainingSeconds']}s")
        
        # Check if done
        if not status['trainingStatus']['isCurrentlyTraining']:
            response = requests.post(api_url, headers=headers)
            return response.json()
        
        # Wait before next poll
        time.sleep(status['polling']['recommendedIntervalMs'] / 1000)
    
    raise TimeoutError('Training timeout')


# Usage
auth_result = onairos_sdk.authorize()
user_data = get_user_traits(auth_result)

print(f"Archetype: {user_data['userProfile']['archetype']}")
print(f"Summary: {user_data['userProfile']['user_summary']}")
```

---

## 8. Migration Checklist

If you were using the old API format:

- [ ] Remove references to `userData.email` (no longer available)
- [ ] Remove references to `userData.username` (no longer available)
- [ ] Update to use `training.ready` instead of `userData.isReturningUser`
- [ ] Implement polling logic for `training.ready === false`
- [ ] Update trait parsing to use new `userProfile` structure
- [ ] Handle rate limiting (429 responses)

---

## 9. FAQ

### Q: Why was email/username removed?

**A:** For security and privacy. The user consented to share their traits with your app, not their email address. Use the internal user ID if you need to track users.

### Q: How long does training take?

**A:** Typically 60-120 seconds for first-time users. The `progress.estimatedTotalSeconds` field provides a real-time estimate.

### Q: What if training fails?

**A:** Check `trainingStatus.canTrainAgain`. If true, the user can re-authorize to retry training.

### Q: Can I skip polling and just wait?

**A:** Yes, if your SDK is in "wait mode" (backgroundMode: false), the SDK handles this internally and returns traits directly.

### Q: What's in the `userProfile` object?

**A:** 
- `user_summary`: 2-3 paragraph personality description
- `top_traits_explanation`: Why these traits were identified
- `archetype`: 2-3 word personality label (e.g., "Strategic Thinker")
- `nudges`: Personalized suggestions/tips

---

## 10. Support

For questions or issues:
- Email: support@onairos.uk
- Documentation: https://docs.onairos.uk
- Status: https://status.onairos.uk
