# Conventions (/conventions)

Shared rules that apply across every endpoint of the e-Próspera API. Most reference pages link here instead of restating these conventions.



If you are using the official CLI, start with [CLI](/cli). The CLI handles most of these wire-level details for you while still using the same credentials, scopes, and API behavior.

## Base URL [#base-url]

All paths in this documentation are relative to one of these origins:

| Environment | Origin                                 |
| ----------- | -------------------------------------- |
| Production  | `https://portal.eprospera.com`         |
| Staging     | `https://staging-portal.eprospera.com` |

So `GET /api/v1/legal_entities/{id}` in production is `GET https://portal.eprospera.com/api/v1/legal_entities/{id}`.

There is no separate `api.eprospera.com` host — the API ships from the same origin as the consumer portal.

## Authentication [#authentication]

Every authenticated request uses the `Authorization` header with a Bearer token:

```text
Authorization: Bearer <token>
```

The token can be one of four things:

| Prefix / shape | Type                   | Notes                                                                           |
| -------------- | ---------------------- | ------------------------------------------------------------------------------- |
| `sk-...`       | Standard API key       | Personal / first-party use. Never share. Not scoped.                            |
| `ak-...`       | Agent Key              | Delegated, scope-checked. See [Agent Keys](/agent-keys).                        |
| `pk-...`       | Partner Key            | Admin-issued, expiring, organization-scoped. See [Partner Keys](/partner-keys). |
| JWT (RS256)    | OAuth 2.0 access token | Issued by `/api/oauth/token`. Carries scopes from the user's consent grant.     |

Not every endpoint accepts every credential — see the **Authentication** block on each reference page. In particular, `/api/v1/me/*` does **not** accept standard API keys.

## Content type [#content-type]

* Request bodies on `POST` endpoints are `application/json` unless the endpoint documents another type. Private proof uploads use `multipart/form-data`.
* OAuth `/api/oauth/token` is `application/x-www-form-urlencoded` per RFC 6749.
* All responses are `application/json` unless explicitly noted.

## Response envelopes [#response-envelopes]

Most endpoints wrap successful payloads:

| Endpoint shape                         | Envelope                                                 |
| -------------------------------------- | -------------------------------------------------------- |
| Single resource (`GET /resource/{id}`) | `{ "data": { ... } }`                                    |
| Collection (`GET /resource`)           | `{ "data": [ ... ] }` (see [Pagination](#pagination))    |
| Create flows                           | `{ "data": { ... }, "nextSteps": { ... } }`              |
| Voucher-pay endpoints                  | `{ "success": true, "data": { ... }, "message": "..." }` |
| Search (`POST .../search`)             | `{ "results": [ ... ] }`                                 |
| Verify (`POST /verify_rpn`)            | `{ "result": "...", "active": true \| false }`           |

OAuth and JWKS endpoints follow their own RFCs (RFC 6749 / RFC 7517 / OIDC Core 1.0) and do **not** use the `data` wrapper.

## Error envelope [#error-envelope]

Errors return a non-2xx status and a JSON body shaped like:

```json
{
  "error": "short_machine_readable_code_or_message",
  "error_description": "Human-readable explanation. Optional.",
  "details": [
    /* Zod issues or per-field errors. Optional. */
  ]
}
```

Some legacy endpoints return only `{ "error": "..." }`. Treat both shapes as the same contract: `error` is always present on a non-2xx response.

`details` is most often a Zod issue array on `400` responses:

```json
{
  "error": "Invalid request",
  "details": [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "undefined",
      "path": ["applicationData", "name"],
      "message": "Required"
    }
  ]
}
```

## HTTP status codes [#http-status-codes]

| Status | Meaning in this API                                                                                   |
| ------ | ----------------------------------------------------------------------------------------------------- |
| `200`  | Success.                                                                                              |
| `400`  | Validation error or conflicting state (e.g. application already submitted).                           |
| `401`  | Missing, malformed, invalid, expired, or revoked credential; inactive Partner Integration.            |
| `403`  | Credential is valid but lacks the required Agent/Partner Key scope or sufficient OAuth scope.         |
| `404`  | Resource does not exist **or** the caller cannot see it. The two are intentionally indistinguishable. |
| `409`  | Conflict (e.g. legal-entity name, stale partner version, or idempotency-key reuse).                   |
| `429`  | Rate limit exceeded. See [Rate limits](#rate-limits).                                                 |
| `500`  | Server error. Safe to retry with backoff for idempotent operations only.                              |

## Timestamps [#timestamps]

All timestamps are **strings in ISO 8601 / RFC 3339 UTC** form, e.g. `2024-01-15T10:30:00.000Z`. Never assume local time. Date-only fields (none currently in the API) would use `YYYY-MM-DD`.

## Nullability [#nullability]

Optional fields are explicitly typed as `T | null` in this documentation and are present in responses with the value `null` rather than being omitted. If a field is documented as non-nullable, expect it to always be present.

## IDs [#ids]

All identifiers are RFC 4122 v4 UUIDs (string form, lowercased, dashed) unless noted. Resident Permit Numbers (RPNs) are 14-digit numeric strings that start with `8` (legal entity) or `9` (natural person).

## Rate limits [#rate-limits]

Rate limiting is per-endpoint or credential class, not global. Currently:

| Endpoint                                        | Limits                                             |
| ----------------------------------------------- | -------------------------------------------------- |
| `POST /api/v1/verify_rpn`                       | 5,000 requests / 24 h and 50 / minute, per API key |
| `POST /api/v1/registries/legal_entities/search` | 5,000 / 24 h and 50 / minute, per API key          |
| Partner reads                                   | 120/min per key; 300/min per integration           |
| Partner updates and submission                  | 60/min per key; 150/min per integration            |
| Partner application creation                    | 10/min per key; 50/hour per integration            |
| Partner uploads                                 | 20/min per key; 100/hour per integration           |
| Partner payments and vouchers                   | 5/min per key; 20/hour per integration             |
| Other endpoints                                 | No documented limit today.                         |

Partner throttling returns `429` with a `Retry-After` header. Invalid Partner Key attempts are limited to 20/minute per source IP. Other rate-limited endpoints may omit `Retry-After`; use exponential backoff when it is absent.

## Pagination [#pagination]

Most list endpoints are not currently paginated. Two exceptions are `/api/v1/nomadlayer/applications` and the Partner Key application list. Partner applications use an opaque cursor:

```text
GET /api/v1/partner/residency_applications?limit=50&cursor=<application-id>
```

The partner `limit` defaults to 50 and cannot exceed 100. Read `pagination.nextCursor` for the next page.

The Nomad Layer endpoint uses page-based pagination:

```json
{
  "data": [
    /* ... */
  ],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalCount": 134,
    "pageCount": 7
  }
}
```

Other list endpoints return the full result set and have no pagination parameters today.

## Versioning [#versioning]

All public endpoints live under `/api/v1/`. Breaking changes will go to a new `/api/v2/` prefix; existing `/api/v1/` endpoints remain stable. Additive changes (new fields, new optional query params, new endpoints) can land under `/api/v1/` without notice — clients should ignore unknown fields rather than reject them.

## Idempotency [#idempotency]

Partner application creation, checkout creation, and voucher payment require `Idempotency-Key`. Keys are scoped to the integration and route and retained for 24 hours. Identical completed requests replay the saved response; different input or an in-progress duplicate returns `409`.

`POST /legal_entity_applications` remains non-idempotent and can create a duplicate Draft when retried. Implement caller-side deduplication for that endpoint.

## Webhooks [#webhooks]

There are no public webhooks. Approval, payment, AOC, and verification state must be discovered by polling the relevant `GET` endpoint. Partner responses summarize the applicant-only AOC/Veriff prerequisites in `nextSteps`; they never expose Veriff URLs or decision payloads. Recommended polling cadence is 5–30 seconds during an active flow, then exponential back-off.
