Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 191 additions & 13 deletions code/API_definitions/device-identifier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,29 @@ info:

# API functionality

The API defines three service endpoints:
The API defines four service endpoints:

- `POST /retrieve-identifier` to get details about the specific device being used by a given mobile subscriber, including IMEI / IMEISV and the type of device
- `POST /retrieve-type` to get details only about the type (i.e. manufacturer and model) of device being used by a given mobile subscriber
- `POST /retrieve-ppid` to get a pseudonymised identifier for the specific device being used by a given mobile subscriber
- `POST /match-identifier` to check whether a device identifier provided by the API consumer matches the one the network currently associates with a given mobile subscription

To call any of these endpoints, the API consumer must first obtain a valid access token from the token endpoint, which is then passed as an Authorization header. When a 2-legged access token is used, the API consumer must also pass at least one of the available mobile subscription identifiers in the body of the request.

If the request is valid, the API response is a JSON object containing the data that the end user has consented to sharing with the API consumer.
- When calling endpoint `retrieve-identifier`, the response will always contain `imei`
- When calling endpoint `retrieve-type`, the response will always contain `tac`
- When calling endpoint `retrieve-ppid`, the response will always contain `ppid`
- When calling endpoint `match-identifier`, the response will always contain `match`
- Responses will also always contain a `lastChecked` field, indicating when the information provided was last confirmed to be correct
- Other response parameters are implementation dependent, and thus optional

An example of a JSON response object is as follows:
```
{
"lastChecked": "2024-02-20T10:41:38.657Z",
"imeisv": "49015420323751800",
"imei": "4901542032375181",
"imeisv": "4901542032375180",
"imei": "490154203237518",
"tac": "49015420",
"model": "3110",
"manufacturer": "Nokia"
Expand Down Expand Up @@ -322,6 +324,52 @@ paths:
"429":
$ref: '#/components/responses/429TooManyRequests'

"/match-identifier":
post:
summary: Check if a provided device identifier matches the one associated with the subscription
description: Check if a provided device identifier matches the one the network currently associates with a given mobile subscription
operationId: matchIdentifier
tags:
- Verify Device Identifiers
security:
- openId:
- device-identifier:match-identifier
parameters:
- $ref: "#/components/parameters/x-correlator"

requestBody:
description: Parameters to identify the mobile subscription and provide the device identifier to match
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MatchRequestBody"
examples:
Match Device By 3-Legged Access Token:
$ref: '#/components/examples/MatchDeviceBy3LeggedToken'
Match Device By Phone Number:
$ref: '#/components/examples/MatchDeviceByPhoneNumber'
Match Device By IP Address:
$ref: '#/components/examples/MatchDeviceByIPAddress'
Match Device By Multiple Identifiers:
$ref: '#/components/examples/MatchDeviceByMultipleIdentifiers'

responses:
"200":
$ref: '#/components/responses/200MatchIdentifier'
"400":
$ref: '#/components/responses/400BadRequest'
"401":
$ref: '#/components/responses/401Unauthorized'
"403":
$ref: '#/components/responses/403Forbidden'
"404":
$ref: '#/components/responses/404NotFound'
"422":
$ref: '#/components/responses/422UnprocessableContent'
"429":
$ref: '#/components/responses/429TooManyRequests'

components:
securitySchemes:
openId:
Expand Down Expand Up @@ -365,8 +413,8 @@ components:
description: Device identifier has been successfully retrieved when the device subscription was identified by a 3-legged access token or single device subscription identifier
value:
lastChecked: "2024-02-20T10:41:38.657Z"
imeisv: "49015420323751800"
imei: "4901542032375181"
imeisv: "4901542032375180"
imei: "490154203237518"
tac: "49015420"
model: "3110"
manufacturer: "Nokia"
Expand All @@ -376,8 +424,8 @@ components:
device:
phoneNumber: "+123456789"
lastChecked: "2024-02-20T10:41:38.657Z"
imeisv: "49015420323751800"
imei: "4901542032375181"
imeisv: "4901542032375180"
imei: "490154203237518"
tac: "49015420"
model: "3110"
manufacturer: "Nokia"
Expand Down Expand Up @@ -442,8 +490,55 @@ components:
lastChecked: "2024-02-20T10:41:38.657Z"
ppid: "b083f65ccdad365d7489fff24b6d5074b30c12b6d81db3404d25964ffd908813"

200MatchIdentifier:
description: |
A match result has been successfully determined for the provided device identifier.
HTTP Status Code Mapping:
- 200 + match=true → The provided identifier MATCHES the network's record
- 200 + match=false → The provided identifier does NOT match (subscription and device are known, confirmed mismatch)
- 404 → Subscription cannot be resolved from device/token
- 422 → Subscription resolved but no deterministic result available (policy/regulation/no device info)
- 5xx → Transient or technical failures (timeouts, upstream errors)
Note: match=false is ONLY returned when a definitive comparison has been performed.
headers:
x-correlator:
$ref: "#/components/headers/X-Correlator"
content:
application/json:
schema:
required:
- lastChecked
- match
allOf:
- $ref: "#/components/schemas/CommonResponseBody"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR currently defines 200 as: match (bool) + lastChecked from CommonResponseBody.

However, it does not explicitly differentiate between:

  • false because the IMEI/TAC does not match,
  • false because no device info is available,
  • or cases where it should be 422 (not applicable) or 5xx (inconclusive/technical failure).

Do we want to reserve an optional field of type result (e.g., MATCH, MISMATCH, NO_DEVICE_INFO, INCONCLUSIVE) in CommonResponseBody or MatchResult, even though only MATCH/MISMATCH are currently used?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not, at the very least, it must be made clear in the description of /match-identifier that:

  • 404 = subscription not found
  • 422 = subscription found but service not applicable (due to policy/segment, etc.)
  • 200 + match=false = valid subscription and known device, but mismatch (not “no info”).

What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should go with the second option B:

I'd recommend not introducing a result enum (MATCH, MISMATCH, NO_DEVICE_INFO, INCONCLUSIVE) at this stage for the following reasons:

CAMARA design minimalism, speculative fields create contract obligations before those scenarios are well-defined.

Boolean is sufficient, the endpoint answers a binary question; edge cases like "no device info" are error conditions, not match results.

Consistency, other CAMARA match/verify APIs (e.g., Number Verification) use plain booleans
Non-breaking extensibility, an optional enum can always be added later without breaking existing consumers.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Ali, I'm fine with going with option B and keeping minimal for now. If we treat “no device info” / “not applicable” strictly as error conditions rather than match outcomes, then I think it becomes even more important to spell out the HTTP mapping explicitly in the spec. Something along these lines would already solve my concern:

  • 404 Not Found → subscription cannot be resolved from the device / token.
  • 422 Unprocessable Content → subscription is resolved, but the service cannot provide a result in a deterministic way (e.g. no device info available for this line, or local policy/regulation prevents returning the information) – i.e. business / applicability issues, not technical ones.
  • 200 with match = false → subscription resolved and device info available, comparison successfully performed, and the identifiers do not match (not “no info” / “not applicable”).

This keeps the response body minimal (just match + lastChecked), reserves false for a real mismatch, and aligns with your point that other match/verify APIs also rely on boolean + HTTP status to convey the outcome.

We can further reinforce this behaviour later on in the test suite, but having this mapping clearly stated in the description of /match-identifier would already give implementers a very concrete contract to follow.

- $ref: "#/components/schemas/MatchResult"
examples:
Successful Match:
description: Device identifier match has been successfully determined when the device subscription was identified by a 3-legged access token or single device subscription identifier
value:
lastChecked: "2024-02-20T10:41:38.657Z"
match: true
Successful Non-Match:
description: Device identifier does not match when the device subscription was identified by a 3-legged access token or single device subscription identifier
value:
lastChecked: "2024-02-20T10:41:38.657Z"
match: false
Successful Match With Device Disambiguation:
description: Device identifier match has been successfully determined when a 2-legged access token and multiple device subscription identifiers were provided
value:
device:
phoneNumber: "+123456789"
lastChecked: "2024-02-20T10:41:38.657Z"
match: true

400BadRequest:
description: Bad Request
description: |
Invalid request syntax or malformed JSON.
This status is reserved for:
- Malformed JSON in the request body
- Invalid data types
- Invalid format (not matching required patterns)
Business validation errors should return 422 instead.
headers:
x-correlator:
$ref: "#/components/headers/X-Correlator"
Expand Down Expand Up @@ -553,7 +648,9 @@ components:
message: Client does not have sufficient permissions to perform this action.

404NotFound:
description: Not found
description: |
Subscription cannot be resolved from the provided device identifiers or access token.
The network cannot map the request to a known subscription.
headers:
x-correlator:
$ref: "#/components/headers/X-Correlator"
Expand All @@ -571,15 +668,22 @@ components:
enum:
- IDENTIFIER_NOT_FOUND
examples:
Device Cannot Be Found:
DeviceNotFound:
description: The provided identifier cannot be matched to a device known to the API provider
value:
status: 404
code: IDENTIFIER_NOT_FOUND
message: The provided identifier cannot be matched to a device.

422UnprocessableContent:
description: Unprocessable Content
description: |
Subscription found but the service cannot provide a deterministic match result.
This status is used for business validation errors:
- MISSING_IDENTIFIER: 2-legged token provided without any subscription identifier
- UNNECESSARY_IDENTIFIER: 3-legged token provided with additional device identifier
- Service cannot provide deterministic result due to:
* No usable device information available for the subscription
* Local policy or regulatory restrictions prevent returning match information
headers:
x-correlator:
$ref: "#/components/headers/X-Correlator"
Expand Down Expand Up @@ -683,11 +787,11 @@ components:
imeisv:
type: string
description: IMEISV of the device
example: "49015420323751800"
example: "4901542032375180"
imei:
type: string
description: IMEI of the device
example: "4901542032375181"
example: "490154203237518"

DeviceType:
description: |
Expand Down Expand Up @@ -834,6 +938,45 @@ components:
pattern: ^[a-zA-Z0-9-_:;.\/<>{}]{0,256}$
example: "b4333c46-49c0-4f62-80d7-f0ef930f1c46"

MatchRequestBody:
description: Request body for match-identifier operation, containing an optional device subscription identifier and the device identifier to match against
allOf:
- $ref: "#/components/schemas/RequestBody"
- type: object
required:
- providedIdentifierType
- providedIdentifier
properties:
providedIdentifierType:
$ref: "#/components/schemas/ProvidedIdentifierType"
providedIdentifier:
type: string
description: The device identifier value to match against. Must match the format constraints for the specified identifier type.
example: "490154203237518"
# Pattern will be validated based on providedIdentifierType

ProvidedIdentifierType:
type: string
enum:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
enum:
enum:
- PPID

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API consumer would check that the PPID have associated with their end-user's account is the same as the last time.

Copy link

@albertoramosmonagas albertoramosmonagas Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @AxelNennker, I understand the use case you’re describing, but from my side, that use case is already covered by the existing retrieve-ppid endpoint: the client stores the PPID and simply compares the new value with the one they had before. We don’t really need a new /match-identifier call for that.

The whole point of /match-identifier was to offer a PPID-free, data-minimising binding check for cases where the partner has an IMEI/TAC and only wants a yes/no answer from the network. If we allow PPID as a providedIdentifierType, this endpoint stops being that alternative and essentially becomes “verify my persistent token”.

I’d strongly prefer to keep providedIdentifierType limited to IMEI / IMEISV / TAC and rely on retrieve-ppid for PPID-based checks.

Copy link
Collaborator

@AxelNennker AxelNennker Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should the whole-point be to provide a ppid free endpoint? PPID is the privacy respecting and data-minimizing alternative to the other identifier. "verify my token" is good.
Regarding access tokens, it is more secure to have a match-identifer access token on file compared to a retrieve-identifier access token. Storing IMEI/IMEISV makes then easier to steal and to use for tracking a sites that are not privacy respecting as sites that use PPID.

For example if the API consumer is selling phone insurance that is bound to the device identifier then they should use the PPID. We should reduce the use of IMEI etc to get to a more privacy friendly environment or charge 10 times for the IMEI compared to the PPID.

Globally unique, persistent identifiers esse delendam.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Axel, I really see your point: for use cases that need a long-lived device binding, PPID is clearly preferable to exposing raw IMEI/IMEISV. Your phone-insurance example fits exactly there, and from my side retrieve-ppid is the right tool in that scenario.

Where I think this endpoint is targeting a different class of use cases:

  • In many fraud / ATO / KYC flows, partners explicitly do not want to store any persistent token at all (neither IMEI nor PPID). They already have an IMEI/TAC in their flow and only want to ask the network:

    “For this subscription and this device identifier, do you currently see the same device, yes or no?”

  • In that context, a one-shot boolean is even more data-minimising than introducing a reusable PPID, even if PPID is better than IMEI when persistence is needed.

Also, the “verify my token” pattern for PPID is already possible today with retrieve-ppid + client-side comparison; the network doesn’t really add new semantics there.

That’s why, at least for this first version, I would keep /match-identifier scoped to IMEI/IMEISV/TAC only, and leave PPID verification to retrieve-ppid. That way we preserve a clear separation:

  • PPID = persistent pseudonymous binding when you actually need it
  • /match-identifier = PPID-free, point-in-time binding check for flows that only need a yes/no answer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AxelNennker, I guess there is no need for PPID in this endpoint.
As:

The “verify my token” pattern for PPID is already possible today with retrieve-ppid + client-side comparison; the network doesn’t really add new semantics there.

- IMEI
- IMEISV
- TAC
description: |
Type of the provided device identifier. Format requirements:
- IMEI: 15 digits (pattern: ^[0-9]{15}$)
- IMEISV: 16 digits (pattern: ^[0-9]{16}$)
- TAC: 8 digits (pattern: ^[0-9]{8}$)

MatchResult:
description: |
The result of matching the provided device identifier against the network's identifier for the subscription
type: object
properties:
match:
type: boolean
description: True if the provided device identifier matches the one the network currently associates with the subscription
example: true

examples:
IdentifyDeviceBy3LeggedToken:
description: Empty JSON when device is identified by access token
Expand Down Expand Up @@ -863,3 +1006,38 @@ components:
publicAddress: "84.125.93.10"
publicPort: 59765
networkAccessIdentifier: "123456789@example.com"

MatchDeviceBy3LeggedToken:
description: Match identifier when device is identified by access token
value:
providedIdentifierType: "IMEI"
providedIdentifier: "4901542032375181"

MatchDeviceByPhoneNumber:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MatchDeviceByPhoneNumber:
MatchDeviceByPhoneNumberProvidePPID:
description: Matching device identifier by phone number and provided PPID
value:
device:
phoneNumber: "+123456789"
providedIdentifierType: "PPID"
providedIdentifier: "b083f65ccdad365d7489fff24b6d5074b30c12b6d81db3404d25964ffd908813"
MatchDeviceByPhoneNumber:

description: Matching device identifier by phone number and provided IMEI
value:
device:
phoneNumber: "+123456789"
Copy link

@albertoramosmonagas albertoramosmonagas Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question: if we are using 3-legged, does it make sense to keep the phoneNumber here? I understand that this example would be for Multiple Identifiers? or just remove the phoneNumber?

providedIdentifierType: "IMEI"
providedIdentifier: "490154203237518"

MatchDeviceByIPAddress:
description: Matching device identifier by IP address and provided TAC
value:
device:
ipv4Address:
publicAddress: "84.125.93.10"
publicPort: 59765
providedIdentifierType: "TAC"
providedIdentifier: "49015420"

MatchDeviceByMultipleIdentifiers:
description: Matching device identifier by multiple subscription identifiers and provided IMEISV
value:
device:
phoneNumber: "+123456789"
ipv4Address:
publicAddress: "84.125.93.10"
publicPort: 59765
providedIdentifierType: "IMEISV"
providedIdentifier: "49015420323751800"
Loading