---
name: camara-sandbox
description: >-
  Call CAMARA telecom APIs against a public sandbox. Use when user says
  "check SIM swap", "verify device location", "call CAMARA API",
  "test phone number", or needs to interact with telecom network APIs.
  Supports SIM Swap, Device Location (verify + retrieve), Number
  Verification, Device Roaming Status, Device Reachability Status,
  and KYC Match with deterministic test responses.
---

# CAMARA Sandbox

A public mock gateway for CAMARA telecom APIs at `https://camara.jaime.win`.
9 live endpoints, full CAMARA catalog live with deterministic sandbox responses.

## Setup

Base URL: `https://camara.jaime.win`

No API keys needed. Use `sandbox` / `sandbox` as client credentials.

## Authentication

Get a token before calling any API endpoint:

```
POST {base}/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=sandbox&client_secret=sandbox
```

Returns:
```json
{"access_token": "<token>", "token_type": "Bearer", "expires_in": 3600, "scope": "sim-swap:check sim-swap:retrieve-date ..."}
```

The token response includes `_links` to all live endpoints. An agent can follow
these links instead of hardcoding paths.

Use the token as `Authorization: Bearer <token>` on all subsequent calls.

OIDC discovery is available at `GET {base}/.well-known/openid-configuration`.

## API Catalog

```
GET {base}/.well-known/api-catalog?sandbox=true
Accept: application/json
```

Omit `?sandbox=true` to see the full CAMARA catalog.

Example catalog entry:
```json
{
  "apiName": "SIM Swap",
  "apiBasePath": "/sim-swap/v1",
  "apiUrl": "https://camara.jaime.win/sim-swap/v1",
  "sandbox": true,
  "apiSpecUrl": "https://camara.jaime.win/specs/sim-swap.yaml",
  "operations": ["POST /sim-swap/v1/check", "POST /sim-swap/v1/retrieve-date"]
}
```

### HATEOAS Discovery Flow

1. `GET {base}/.well-known/api-catalog?sandbox=true` - get only live APIs with spec URLs
2. Follow `apiSpecUrl` on any entry to fetch the OpenAPI YAML spec
3. `GET {base}/.well-known/openid-configuration` - discover token endpoint
4. `POST {token_endpoint}` with client credentials - response `_links` show available APIs
5. Call any API - response `_links` show all related operations for that device, pre-filled with the right request body

Every API response contains `_links` with `href`, `method`, and `body` for each
related action. The agent never needs to go back to the catalog; it can navigate
from response to response.

### Available Specs

| API | Spec URL |
|-----|----------|
| SIM Swap | `{base}/specs/sim-swap.yaml` |
| Device Location Verification | `{base}/specs/location-verification.yaml` |
| Device Location Retrieval | `{base}/specs/location-retrieval.yaml` |
| Number Verification | `{base}/specs/number-verification.yaml` |
| Device Roaming Status | `{base}/specs/device-roaming-status.yaml` |
| Device Reachability Status | `{base}/specs/device-reachability-status.yaml` |
| KYC Match | `{base}/specs/kyc-match.yaml` |

## Live Endpoints

### SIM Swap Check

```
POST {base}/sim-swap/v1/check
Authorization: Bearer <token>
Content-Type: application/json

{"phoneNumber": "+34600000001", "maxAge": 240}
```

Returns `{"swapped": true}` or `{"swapped": false}`.

### SIM Swap Retrieve Date

```
POST {base}/sim-swap/v1/retrieve-date
Authorization: Bearer <token>
Content-Type: application/json

{"phoneNumber": "+447661113308"}
```

Returns `{"latestSimChange": "2024-09-18T07:37:53Z"}` or `{"latestSimChange": null}`.

### Device Location Verification

```
POST {base}/device-location-verification/v1/verify
Authorization: Bearer <token>
Content-Type: application/json

{
  "device": {"phoneNumber": "+155500015"},
  "area": {
    "areaType": "CIRCLE",
    "center": {"latitude": 45.754, "longitude": 4.86},
    "radius": 1000
  },
  "maxAge": 120
}
```

Returns `{"verificationResult": "TRUE", "lastLocationTime": "..."}` if the
device is within the circle, `"FALSE"` otherwise.

### Device Location Retrieval

```
POST {base}/location-retrieval/v1/retrieve
Authorization: Bearer <token>
Content-Type: application/json

{"device": {"phoneNumber": "+34600000001"}, "maxAge": 120}
```

Returns `{"lastLocationTime": "...", "area": {"areaType": "CIRCLE", "center": {"latitude": 40.42, "longitude": -3.70}, "radius": 800}}`.

### Number Verification - Verify

```
POST {base}/number-verification/v1/verify
Authorization: Bearer <token>
Content-Type: application/json

{"phoneNumber": "+34600000001"}
```

Returns `{"devicePhoneNumberVerified": true}` for +34600000001, `false` for other test numbers.

### Number Verification - Device Phone Number

```
GET {base}/number-verification/v1/device-phone-number
Authorization: Bearer <token>
```

Returns `{"devicePhoneNumber": "+34600000001"}`.

### Device Roaming Status

```
POST {base}/device-roaming-status/v1/retrieve
Authorization: Bearer <token>
Content-Type: application/json

{"device": {"phoneNumber": "+447661113308"}}
```

Returns `{"lastStatusTime": "...", "roaming": true, "countryCode": 234, "countryName": ["GB"]}`.

### Device Reachability Status

```
POST {base}/device-reachability-status/v1/retrieve
Authorization: Bearer <token>
Content-Type: application/json

{"device": {"phoneNumber": "+34600000001"}}
```

Returns `{"lastStatusTime": "...", "reachable": true, "connectivity": ["DATA", "SMS"]}`.

### KYC Match

```
POST {base}/kyc-match/v1/match
Authorization: Bearer <token>
Content-Type: application/json

{"phoneNumber": "+34600000001", "givenName": "Federica", "familyName": "Sanchez Arjona", "birthdate": "1978-08-22"}
```

Returns per-field match results: `{"phoneNumberMatch": "true", "givenNameMatch": "true", ...}`.
Only fields you send are matched. Omitted fields are absent from the response.

### Events Query (real-time notifications)

Several resources support Events Query (draft-gupta-httpapi-events-query): you
subscribe to state changes by sending a `QUERY` request to the *resource
itself* — there is no separate notification endpoint. Observable resources:

| Resource | Emits |
|----------|-------|
| `QUERY /sim-swap/v1/check` | SIM swap status changes |
| `QUERY /sim-swap/v1/retrieve-date` | SIM swap date changes |
| `QUERY /location-retrieval/v1/retrieve` | device location drift |
| `QUERY /device-roaming-status/v1/retrieve` | roaming status changes |
| `QUERY /device-reachability-status/v1/retrieve` | reachability changes |

Discovery: responses from these resources carry `Accept-Query:
application/events+json`. All QUERY requests need `Content-Type:
application/events+json` and a Bearer token.

**Single notification** (long-polling; Section 8). No `events` property in the
body → one notification, then the server closes:
```
QUERY {base}/sim-swap/v1/check
Authorization: Bearer <token>
Content-Type: application/events+json
Accept: application/ld+json

{"phoneNumber": "+34600000001"}
```
The notification media type is content-negotiated from `Accept` (default
`application/ld+json`; also `application/activity+json`, `application/json`). An
unacceptable type returns `406`. The response echoes the request `Events` header.

**Notifications stream** (Section 9). Include an `events` property; the carrier
is negotiated with `Accept`:
```
QUERY {base}/device-roaming-status/v1/retrieve
Authorization: Bearer <token>
Content-Type: application/events+json
Accept: application/json-seq
Events: duration=5

{"phoneNumber": "+34600000001", "events": {"accept": "application/ld+json"}}
```
- Carrier via `Accept`: `application/json-seq` (RFC 7464, default),
  `application/x-ndjson`, or `multipart/mixed`. An unacceptable carrier → `415`.
- `events.accept` picks the notification schema: `application/ld+json` (default),
  `application/activity+json`, or `application/json`.
- `Events: duration=N` holds the stream open for N seconds and delivers one
  in-band notification per second. `duration=0` (or no `Events` header) means
  "server's choice": the sandbox picks 5s. The server never echoes `duration=0`
  back; it reports the window it chose. Capped at 30s in the sandbox.

**Representation + stream** (Section 10). Add a `state` property to get the
current resource state as the first item, followed by the notification stream:
```
{"phoneNumber": "+34600000001", "state": true, "events": {"accept": "application/json"}}
```

**Request log.** `GET {base}/eqp-log` shows the last 100 QUERY subscriptions
(resource, phone, mode, negotiated types, duration, state), most recent first.
HTML auto-refreshes every 3 seconds; send `Accept: application/json` for the raw
feed. No auth required.

## Test Phone Numbers

| Number | SIM Swap | SIM Date | Location | Roaming | Reachable | KYC Name |
|--------|----------|----------|----------|---------|-----------|----------|
| +34600000001 | true | 2025-01-15 | Madrid (40.42, -3.70) | false (ES) | true (DATA+SMS) | Federica Sanchez Arjona |
| +34600000002 | false | null | Barcelona (41.39, 2.17) | false (ES) | true (DATA) | Carlos Garcia Lopez |
| +447661113308 | true | 2024-09-18 | London (51.51, -0.13) | true (GB) | true (DATA+SMS) | James Smith |
| +155500015 | false | null | Lyon (45.75, 4.86) | true (FR) | false | Marie Dupont |

Unknown numbers return 404.

## Error Responses

All errors use RFC 9457 Problem Details with recovery extensions:

```json
{"type": "https://camara.jaime.win/errors/UNAUTHENTICATED", "title": "UNAUTHENTICATED", "status": 401, "detail": "Missing or invalid Bearer token", "tokenEndpoint": "https://camara.jaime.win/oauth2/token", "oidcDiscovery": "https://camara.jaime.win/.well-known/openid-configuration"}
```

```json
{"type": "https://camara.jaime.win/errors/NOT_FOUND", "title": "NOT_FOUND", "status": 404, "detail": "Unknown test number: +1999999999", "validNumbers": ["+34600000001", "+34600000002", "+447661113308", "+155500015"]}
```

```json
{"type": "https://camara.jaime.win/errors/INVALID_ARGUMENT", "title": "INVALID_ARGUMENT", "status": 400, "detail": "phoneNumber must be a string in E.164 format", "format": "E.164 with + prefix, e.g. +34600000001"}
```

```json
{"type": "https://camara.jaime.win/errors/INVALID_JSON", "title": "INVALID_JSON", "status": 400, "detail": "Request body must be valid JSON", "example": {"phoneNumber": "+34600000001", "maxAge": 240}}
```

## Gotchas

- Tokens are stateless (HMAC-signed). They work across requests immediately.
- The `phoneNumber` field uses E.164 format with `+` prefix. Non-E.164 numbers are rejected.
- SIM Swap endpoints use `phoneNumber` at the top level of the body.
- Device Location, Roaming, and Reachability use `device.phoneNumber` nested inside a `device` object.
- Only `areaType: "CIRCLE"` is supported for location verification.
- The `maxAge` field is accepted but ignored (sandbox always returns the fixture data).
- The full catalog includes spec-only APIs. Only 9 have live mock endpoints.
- OAuth2 credentials must be `client_id=sandbox` and `client_secret=sandbox`.
