External Identifiers#

For: developers

How OpenSPP API V2 uses namespaced external identifiers (URNs) instead of database IDs, and how to construct, send, and look up records by them.

Prerequisites#

  • A working API client and OAuth token (see Authentication)

  • Familiarity with URN syntax and URL-encoded query parameters

Why External Identifiers?#

OpenSPP API V2 never exposes internal database IDs. Instead, all resources use external identifiers with namespace URIs.

The Problem with Database IDs#

// ❌ WRONG - Database ID exposed
{
  "id": 12345,
  "name": "Maria Santos"
}

Issues:

  • Database IDs leak internal implementation details

  • IDs can change during database migrations or merges

  • No way to identify the ID system (National ID? Voter ID? Internal?)

  • Security risk: sequential IDs are predictable

  • Data sovereignty: external systems shouldn't reference internal IDs

The Solution: Namespaced External IDs#

// ✅ CORRECT - External identifier with namespace
{
  "identifier": [
    {
      "system": "urn:gov:ph:psa:national-id",
      "value": "PH-123456789"
    }
  ],
  "name": {
    "given": "Maria",
    "family": "Santos"
  }
}

Benefits:

  • Unambiguous: The namespace tells you what kind of ID this is

  • Portable: IDs work across systems and migrations

  • Multiple IDs: One person can have many identifiers

  • Standards-aligned: Compatible with G2P Connect, FHIR, DCI

Identifier Structure#

Every identifier has two required fields:

{
  "system": "urn:gov:ph:psa:national-id",
  "value": "PH-123456789"
}

Field

Type

Description

system

string (required)

Namespace URI identifying the ID system

value

string (required)

The actual identifier value

period

object (optional)

Time period when identifier is/was valid

Namespace URI Format#

Important

OpenSPP-specific format: identifier system URIs in OpenSPP use the format urn:openspp:vocab:id-type#<code>, where <code> is a registered vocabulary code (e.g., national_id, passport, refugee_id). The <code> part determines what kind of identifier this is. The full URI must match a row in the spp.vocabulary.code table — otherwise the API returns 422.

The conceptual examples below (urn:gov:ph:psa:national-id, etc.) illustrate the idea of namespaced identifiers as you'd find in FHIR or G2P Connect documentation. In an actual OpenSPP API request, replace these with urn:openspp:vocab:id-type#<code> for the <code> your administrator has registered.

When constructing URLs, the # character MUST be URL-encoded as %23 — it is the URL fragment delimiter, and unencoded # will be silently stripped.

The conceptual URN form for namespaced identifiers is:

urn:{authority}:{path}

Conceptual examples (illustrative — not the actual OpenSPP format):

Type

Conceptual namespace URI

Philippine National ID

urn:gov:ph:psa:national-id

Philippine PhilHealth

urn:gov:ph:philhealth

Kenya National ID

urn:gov:ke:nira:national-id

ISO Gender Code

urn:iso:std:iso:5218

FAO Crop Vocabulary

urn:fao:agrovoc

Actual OpenSPP system URIs (use these in requests):

Type

Actual OpenSPP system URI

National ID (default seed)

urn:openspp:vocab:id-type#national_id

Passport

urn:openspp:vocab:id-type#passport

ID type configured by your admin

urn:openspp:vocab:id-type#<code>

Multiple Identifiers#

A person typically has multiple identifiers:

{
  "type": "Individual",
  "identifier": [
    {
      "system": "urn:gov:ph:psa:national-id",
      "value": "PH-123456789"
    },
    {
      "system": "urn:gov:ph:philhealth",
      "value": "12-345678901-2"
    },
    {
      "system": "urn:openspp:registry:individual",
      "value": "5c8f9b4e-3d7a-4f2b-9e1c-8a7b6d5e4f3c"
    },
    {
      "system": "urn:gov:ph:pagibig",
      "value": "1234567890123"
    }
  ]
}

Best Practice: Always include at least one stable, widely-recognized identifier (like National ID).

Time-Limited Identifiers#

Some identifiers expire or change validity:

{
  "system": "urn:openspp:program:4ps:id",
  "value": "4PS-2024-001234",
  "period": {
    "start": "2024-01-01",
    "end": "2024-12-31"
  }
}

Using Identifiers in API Requests#

Reading a Resource#

Use the pipe (|) separator in the URL:

GET /api/v2/spp/Individual/{system}|{value}

URL-encode the system if it contains special characters. For OpenSPP URIs, the # MUST be encoded as %23:

# Actual OpenSPP format (the # is the critical character)
urn:openspp:vocab:id-type#national_id|IND-001

# URL-encoded (use this in requests)
urn:openspp:vocab:id-type%23national_id|IND-001

# Conceptual / FHIR-style (illustrative only)
urn:gov:ph:psa:national-id|PH-123456789

# URL-encoded
urn%3Agov%3Aph%3Apsa%3Anational-id|PH-123456789

Example: curl#

curl "https://{your-domain}/api/v2/spp/Individual/urn%3Agov%3Aph%3Apsa%3Anational-id|PH-123456789" \
  -H "Authorization: Bearer YOUR_TOKEN"

Example: Python#

import requests
from urllib.parse import quote

def get_individual_by_identifier(system, value, token, base_url):
    """Fetch individual using a specific identifier."""
    # URL-encode the system
    encoded_system = quote(system, safe='')
    identifier = f"{encoded_system}|{value}"

    headers = {"Authorization": f"Bearer {token}"}
    response = requests.get(
        f"{base_url}/Individual/{identifier}",
        headers=headers
    )
    response.raise_for_status()
    return response.json()

# Usage
individual = get_individual_by_identifier(
    system="urn:gov:ph:psa:national-id",
    value="PH-123456789",
    token=token,
    base_url="https://{your-domain}/api/v2/spp"
)

Example: JavaScript#

function getIndividualByIdentifier(system, value, token, baseUrl) {
  // URL-encode the system
  const encodedSystem = encodeURIComponent(system);
  const identifier = `${encodedSystem}|${value}`;

  return fetch(`${baseUrl}/Individual/${identifier}`, {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  }).then(response => {
    if (!response.ok) {
      throw new Error(`Request failed: ${response.statusText}`);
    }
    return response.json();
  });
}

// Usage
const individual = await getIndividualByIdentifier(
  'urn:gov:ph:psa:national-id',
  'PH-123456789',
  token,
  'https://{your-domain}/api/v2/spp'
);

Searching by Identifier#

Use the identifier search parameter with the same system|value format:

GET /api/v2/spp/Individual?identifier=urn:gov:ph:psa:national-id|PH-123456789

Example: Python#

def search_by_identifier(system, value, token, base_url):
    """Search for individuals by identifier."""
    headers = {"Authorization": f"Bearer {token}"}
    params = {
        "identifier": f"{system}|{value}"
    }

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Usage
results = search_by_identifier(
    system="urn:gov:ph:philhealth",
    value="12-345678901-2",
    token=token,
    base_url="https://{your-domain}/api/v2/spp"
)

if results["meta"]["total"] > 0:
    print(f"Found {results['meta']['total']} matching individuals")

Searching Without System#

Search across all identifier systems:

GET /api/v2/spp/Individual?identifier=PH-123456789

Note: This searches the value field across all systems. Less efficient than specifying the system.

Creating Resources with Identifiers#

When creating a resource, provide at least one identifier:

POST /api/v2/spp/Individual
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "type": "Individual",
  "identifier": [
    {
      "system": "urn:gov:ph:psa:national-id",
      "value": "PH-987654321"
    }
  ],
  "name": {
    "given": "Juan",
    "family": "Dela Cruz"
  },
  "birthDate": "1990-05-15"
}

Example: Python#

def create_individual(identifier_system, identifier_value, name, birth_date, token, base_url):
    """Create a new individual with an identifier."""
    data = {
        "type": "Individual",
        "identifier": [
            {
                "system": identifier_system,
                "value": identifier_value
            }
        ],
        "name": {
            "given": name["given"],
            "family": name["family"]
        },
        "birthDate": birth_date
    }

    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    response = requests.post(
        f"{base_url}/Individual",
        headers=headers,
        json=data
    )
    response.raise_for_status()
    return response.json()

# Usage
individual = create_individual(
    identifier_system="urn:gov:ph:psa:national-id",
    identifier_value="PH-987654321",
    name={"given": "Juan", "family": "Dela Cruz"},
    birth_date="1990-05-15",
    token=token,
    base_url="https://{your-domain}/api/v2/spp"
)

print(f"Created individual: {individual['identifier'][0]['value']}")

Updating Identifiers#

To add or update identifiers, use PUT with the complete resource:

Updating Identifiers (PUT)#

PUT /api/v2/spp/Individual/urn:gov:ph:psa:national-id|PH-123456789
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
If-Match: "3"

{
  "type": "Individual",
  "identifier": [
    {
      "system": "urn:gov:ph:psa:national-id",
      "value": "PH-123456789"
    },
    {
      "system": "urn:gov:ph:philhealth",
      "value": "12-345678901-2"
    },
    {
      "system": "urn:gov:ph:pagibig",
      "value": "1234567890123"
    }
  ],
  "name": { ... },
  ...
}

References Between Resources#

Use identifiers to reference other resources:

{
  "type": "ProgramMembership",
  "program": {
    "reference": "Program/urn:openspp:program|4Ps",
    "display": "Pantawid Pamilyang Pilipino Program"
  },
  "beneficiary": {
    "reference": "Individual/urn:gov:ph:psa:national-id|PH-123456789",
    "display": "Maria Santos"
  },
  "status": "enrolled"
}

Reference format:

{ResourceType}/{system}|{value}

Common Identifier Systems#

Philippines#

System

Namespace URI

Example

PhilSys (National ID)

urn:gov:ph:psa:national-id

PH-1234-5678-9012

PhilHealth

urn:gov:ph:philhealth

12-345678901-2

SSS

urn:gov:ph:sss

34-1234567-8

Pag-IBIG

urn:gov:ph:pagibig

1234567890123

TIN

urn:gov:ph:bir:tin

123-456-789-000

Kenya#

System

Namespace URI

Example

National ID

urn:gov:ke:nira:national-id

12345678

Huduma Number

urn:gov:ke:huduma

HN-12345678

NHIF

urn:gov:ke:nhif

1234567890

OpenSPP Internal#

System

Namespace URI

Format

Individual Registry

urn:openspp:registry:individual

UUID v4

Group Registry

urn:openspp:registry:group

UUID v4

Program

urn:openspp:program

Program code

Source Tracking#

When you create or update a resource via the API, OpenSPP automatically records the source as:

urn:openspp:api-client:{client_id}

This appears in the resource's meta.source field and provides an audit trail of which API client created or last modified each record.

Identifier Validation#

The API validates identifiers on create/update:

Duplicate Detection#

Creating a resource with an existing identifier fails:

{
  "type": "urn:openspp:error:validation",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Identifier urn:gov:ph:psa:national-id|PH-123456789 already exists",
  "errors": [
    {
      "field": "identifier[0]",
      "message": "Duplicate identifier"
    }
  ]
}

Format Validation#

Some identifier systems have format requirements:

# Philippine PhilSys format: XX-NNNN-NNNN-NNNN
PHILSYS_PATTERN = r'^PH-\d{4}-\d{4}-\d{4}$'

# Philippine PhilHealth format: NN-NNNNNNNNN-N
PHILHEALTH_PATTERN = r'^\d{2}-\d{9}-\d$'

Invalid formats trigger validation errors:

{
  "type": "urn:openspp:error:validation",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Invalid PhilSys ID format. Expected: PH-NNNN-NNNN-NNNN",
  "errors": [
    {
      "field": "identifier[0].value",
      "message": "expected pattern ^PH-\\d{4}-\\d{4}-\\d{4}$"
    }
  ]
}

Best Practices#

1. Always Specify the System#

# ✅ Good - Explicit system
identifier = {
    "system": "urn:gov:ph:psa:national-id",
    "value": "PH-123456789"
}

# ❌ Bad - No system
identifier = {
    "value": "PH-123456789"
}

2. Use Stable Identifiers for Lookups#

Prefer government-issued IDs over program-specific IDs:

# ✅ Good - National ID is stable
system = "urn:gov:ph:psa:national-id"

# ⚠️ Caution - Program ID may change
system = "urn:openspp:program:4ps:id"

3. Store the Full Identifier Object#

Don't just store the value; store the system too:

# ✅ Good - Full identifier preserved
stored_identifiers = [
    {"system": "urn:gov:ph:psa:national-id", "value": "PH-123456789"},
    {"system": "urn:gov:ph:philhealth", "value": "12-345678901-2"}
]

# ❌ Bad - Lost system context
stored_ids = ["PH-123456789", "12-345678901-2"]  # Which is which?

4. Handle Multiple Matches Gracefully#

When searching by identifier, check if multiple records match:

results = search_by_identifier(system, value, token, base_url)

if results["meta"]["total"] == 0:
    print("No match found")
elif results["meta"]["total"] == 1:
    individual = results["data"][0]
    print(f"Found: {individual['name']['text']}")
else:
    print(f"Warning: Multiple matches ({results['meta']['total']})")
    # Decide how to handle duplicates

Common mistakes#

Getting 404 Not Found?

Check that you're URL-encoding the system correctly:

# Correct
encoded = quote("urn:gov:ph:psa:national-id", safe='')
# Result: urn%3Agov%3Aph%3Apsa%3Anational-id

Which identifier system should I use?

Use the most widely-recognized, stable identifier available. For Philippines: PhilSys (National ID). For Kenya: National ID (NIRA).

Can I search by partial identifier value?

No. Identifier searches are exact-match only. Use name search for fuzzy matching.

What if someone doesn't have a government ID?

OpenSPP can generate a UUID-based identifier:

{
  "system": "urn:openspp:registry:individual",
  "value": "5c8f9b4e-3d7a-4f2b-9e1c-8a7b6d5e4f3c"
}

How do I know what identifier systems are supported?

Check the capability statement:

curl https://{your-domain}/api/v2/spp/metadata

Look for identifierSystems in the response.

Complete Example#

import requests
from urllib.parse import quote

class IdentifierHelper:
    """Helper class for working with OpenSPP identifiers."""

    @staticmethod
    def format_identifier_path(system, value):
        """Format system|value for URL path."""
        encoded_system = quote(system, safe='')
        return f"{encoded_system}|{value}"

    @staticmethod
    def format_identifier_param(system, value):
        """Format system|value for query parameter."""
        return f"{system}|{value}"

    @staticmethod
    def find_identifier(identifiers, system):
        """Find an identifier by system in a list."""
        for identifier in identifiers:
            if identifier["system"] == system:
                return identifier
        return None

# Usage
helper = IdentifierHelper()

# Fetch by identifier
path = helper.format_identifier_path(
    "urn:gov:ph:psa:national-id",
    "PH-123456789"
)
individual = requests.get(
    f"https://{your-domain}/api/v2/spp/Individual/{path}",
    headers={"Authorization": f"Bearer {token}"}
).json()

# Find specific identifier in response
philhealth_id = helper.find_identifier(
    individual["identifier"],
    "urn:gov:ph:philhealth"
)

if philhealth_id:
    print(f"PhilHealth: {philhealth_id['value']}")
else:
    print("No PhilHealth ID on file")

What's next#