API Changes
Contents
API Changes#
This guide is for developers migrating API integrations and REST endpoints from OpenSPP V1 to V2.
Overview#
V2 changes API endpoints to match the new namespace:
Endpoint paths:
/api/v1/g2p/*→/api/v2/spp/*Model references in payloads
Authentication (enhanced OAuth 2.0)
External identifiers (UUIDs instead of DB IDs)
Endpoint Path Changes#
Registry API Endpoints#
V1 Endpoint |
V2 Endpoint |
Description |
|---|---|---|
|
|
List registrants |
|
|
Get registrant (now uses UUID) |
|
|
Create registrant |
|
|
Update registrant |
Program API Endpoints#
V1 Endpoint |
V2 Endpoint |
Description |
|---|---|---|
|
|
List programs |
|
|
Get program |
|
|
Enroll beneficiary |
|
|
List program cycles |
Entitlement API Endpoints#
V1 Endpoint |
V2 Endpoint |
Description |
|---|---|---|
|
|
List entitlements |
|
|
Get entitlement |
|
|
Approve entitlement |
Request/Response Changes#
Database IDs → External Identifiers#
V1 used database IDs (integers):
// V1 Request
GET /api/v1/g2p/registry/registrant/42
// V1 Response
{
"id": 42,
"name": "John Doe",
"program_id": 5
}
V2 uses external IDs (UUIDs):
// V2 Request
GET /api/v2/spp/registry/registrant/a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d
// V2 Response
{
"ext_id": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
"name": "John Doe",
"program_ext_id": "5f6a7b8c-9d0e-4f5a-6b7c-8d9e0f1a2b3c"
}
Why? External IDs are safe to share between systems and don't expose database internals.
Model Names in Responses#
// V1 Response
{
"id": 42,
"model": "g2p.program",
"registrant_id": 10,
"registrant_model": "g2p.registrant"
}
// V2 Response
{
"ext_id": "a1b2c3d4...",
"model": "spp.program",
"registrant_ext_id": "5f6a7b8c...",
"registrant_model": "spp.registrant"
}
Authentication Changes#
V1 Authentication (Basic/Session)#
# V1 - Basic auth or session cookies
import requests
response = requests.get(
'http://localhost:8069/api/v1/g2p/registry/registrant',
auth=('admin', 'password')
)
V2 Authentication (OAuth 2.0)#
# V2 - OAuth 2.0 token-based
import requests
# 1. Get access token
token_response = requests.post(
'http://localhost:8069/oauth2/token',
data={
'grant_type': 'client_credentials',
'client_id': 'your_client_id',
'client_secret': 'your_client_secret',
'scope': 'registry:read program:read'
}
)
access_token = token_response.json()['access_token']
# 2. Use token in requests
response = requests.get(
'http://localhost:8069/api/v2/spp/registry/registrant',
headers={'Authorization': f'Bearer {access_token}'}
)
OAuth 2.0 Scopes#
V2 introduces granular scopes:
Scope |
Access |
|---|---|
|
Read registrant data |
|
Create/update registrants |
|
Read program data |
|
Manage programs |
|
Read entitlements |
|
Create/approve entitlements |
|
Read payments |
|
Process payments |
Migration Examples#
Example 1: List Registrants#
# Before (V1)
import requests
response = requests.get(
'http://localhost:8069/api/v1/g2p/registry/registrant',
auth=('admin', 'password'),
params={'limit': 10}
)
for registrant in response.json():
print(f"ID: {registrant['id']}, Name: {registrant['name']}")
# After (V2)
import requests
# Get OAuth token first
token_response = requests.post(
'http://localhost:8069/oauth2/token',
data={
'grant_type': 'client_credentials',
'client_id': 'your_client_id',
'client_secret': 'your_client_secret',
'scope': 'registry:read'
}
)
access_token = token_response.json()['access_token']
# Use token
response = requests.get(
'http://localhost:8069/api/v2/spp/registry/registrant',
headers={'Authorization': f'Bearer {access_token}'},
params={'limit': 10}
)
for registrant in response.json()['data']: # Note: wrapped in 'data'
print(f"Ext ID: {registrant['ext_id']}, Name: {registrant['name']}")
Example 2: Create Registrant#
# Before (V1)
response = requests.post(
'http://localhost:8069/api/v1/g2p/registry/registrant',
auth=('admin', 'password'),
json={
'name': 'Jane Smith',
'birthdate': '1990-05-15',
'gender': 'female'
}
)
new_id = response.json()['id']
# After (V2)
response = requests.post(
'http://localhost:8069/api/v2/spp/registry/registrant',
headers={'Authorization': f'Bearer {access_token}'},
json={
'name': 'Jane Smith',
'birthdate': '1990-05-15',
'gender': 'female'
}
)
new_ext_id = response.json()['data']['ext_id'] # UUID, not integer
Example 3: Enroll in Program#
# Before (V1)
response = requests.post(
f'http://localhost:8069/api/v1/g2p/programs/{program_id}/enroll',
auth=('admin', 'password'),
json={
'registrant_id': 42
}
)
# After (V2)
response = requests.post(
f'http://localhost:8069/api/v2/spp/programs/{program_ext_id}/enroll',
headers={'Authorization': f'Bearer {access_token}'},
json={
'registrant_ext_id': 'a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d'
}
)
Change Request API#
The Change Request API was significantly refactored in V2:
V1 Change Request (Multiple Endpoints)#
# V1 had separate endpoints for each change type
POST /api/v1/g2p/change_request/add_farmer
POST /api/v1/g2p/change_request/edit_farmer
POST /api/v1/g2p/change_request/create_farm
# ... 10+ different endpoints
V2 Change Request (Unified Endpoint)#
# V2 has single endpoint with type parameter
POST /api/v2/spp/change_request
{
"type": "add_farmer", # or edit_farmer, create_farm, etc.
"data": {
# type-specific data
}
}
Migration:
# Before (V1)
response = requests.post(
'http://localhost:8069/api/v1/g2p/change_request/add_farmer',
auth=('admin', 'password'),
json={
'farmer_name': 'John Farmer',
'farm_size': 10.5
}
)
# After (V2)
response = requests.post(
'http://localhost:8069/api/v2/spp/change_request',
headers={'Authorization': f'Bearer {access_token}'},
json={
'type': 'add_farmer',
'data': {
'farmer_name': 'John Farmer',
'farm_size': 10.5
}
}
)
Error Response Changes#
V1 Error Response#
{
"error": "Registrant not found",
"error_code": "NOT_FOUND"
}
V2 Error Response (RFC 7807 Problem Details)#
{
"type": "https://openspp.org/errors/not-found",
"title": "Registrant Not Found",
"status": 404,
"detail": "No registrant found with ext_id: a1b2c3d4...",
"instance": "/api/v2/spp/registry/registrant/a1b2c3d4..."
}
Pagination Changes#
V1 Pagination#
# V1 - Simple limit/offset
GET /api/v1/g2p/registry/registrant?limit=10&offset=20
V2 Pagination (Cursor-based)#
# V2 - Cursor-based pagination for better performance
GET /api/v2/spp/registry/registrant?limit=10&cursor=eyJpZCI6MjB9
# Response includes next cursor
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MzB9",
"has_more": true
}
}
Compatibility Layer#
V2 provides a temporary compatibility layer for V1 endpoints:
# V1 endpoints still work (with deprecation warnings)
GET /api/v1/g2p/registry/registrant # Still works, logs warning
# Response includes deprecation header
Deprecation: true
Sunset: 2026-12-31
Link: <https://docs.openspp.org/migration>; rel="deprecation"
Deprecation timeline:
V2.0 (Dec 2025): V1 endpoints work with warnings
V2.5 (Jun 2026): V1 endpoints return 410 Gone
V3.0 (Dec 2026): V1 endpoints removed
Testing API Migration#
# tests/test_api_migration.py
import requests
from odoo.tests import TransactionCase
class TestAPIMigration(TransactionCase):
def setUp(self):
super().setUp()
# Get OAuth token
self.token = self._get_oauth_token()
def _get_oauth_token(self):
"""Helper to get OAuth token"""
# Implementation depends on your setup
return "test_token"
def test_list_registrants_v2(self):
"""Test V2 registrant listing"""
response = requests.get(
f'{self.base_url}/api/v2/spp/registry/registrant',
headers={'Authorization': f'Bearer {self.token}'}
)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('data', data)
def test_external_ids_used(self):
"""Verify external IDs are used, not DB IDs"""
response = requests.get(
f'{self.base_url}/api/v2/spp/registry/registrant',
headers={'Authorization': f'Bearer {self.token}'},
params={'limit': 1}
)
registrant = response.json()['data'][0]
self.assertIn('ext_id', registrant)
self.assertNotIn('id', registrant) # DB ID not exposed
See Also#
Namespace Changes (g2p.* → spp.*) - Module and model renames
Model and Field Renames - Model reference changes
openspp.org