{
  "openapi": "3.1.0",
  "info": {
    "title": "AletaPlanet API",
    "version": "1.0.0",
    "summary": "Card issuing and money movement.",
    "description": "Issue virtual cards, hold value in accounts, and authorize transactions in\nreal time.\n\nNew to the platform? Start with\n[core concepts](https://developer.aletaplanet.com/docs/products/card-issuing/core-concepts) —\nthe object model everything here assumes.\n\n## Not every program has every endpoint\n\nA program is bound to a card processor, and processors do not cover the same\nground. An operation the one behind your programs cannot perform is **not\nmounted**: the path returns `404` with `not_found`, in the same envelope as\na misspelled URL.\n\nThat is deliberate. An endpoint that existed and always failed would read as\na service that is nearly finished, and a `502` would tell you the processor\nwas unavailable when in fact the operation does not exist for you at all.\n\nOperations that depend on a capability carry `x-aleta-capability` below.\nProbe for a `404` at integration time rather than at run time — the set does\nnot change without notice, and probing is the answer that cannot be stale.\n",
    "termsOfService": "https://aletaplanet.com/terms",
    "contact": {
      "name": "AletaPlanet developer support",
      "url": "https://developer.aletaplanet.com/docs/support/contacting-support",
      "email": "it@aletaplanet.com"
    },
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-AletaPlanet-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.aletaplanet.com",
      "description": "Both environments. Your access token decides which one a request runs against — `at_test_` for sandbox, `at_live_` for production."
    }
  ],
  "security": [
    {
      "oauth2ClientCredentials": []
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Obtaining the access token every other operation requires. One\nendpoint, reached without a token.\n"
    },
    {
      "name": "Programs",
      "description": "The card products your organization may issue against. A program is\noperated by AletaPlanet and attached to your organization; you read the\ncatalogue, you do not create rows in it.\n\nEverything you create belongs to exactly one program. An account names\none when it is opened and everything that account owns inherits it, so\nthe `prog_` id is the first value an integration needs.\n"
    },
    {
      "name": "Cardholders",
      "description": "The person a card belongs to. Every account and card references one.\nIdentity verification state gates card issuance.\n"
    },
    {
      "name": "Accounts",
      "description": "Where value is held. One currency per account. Carries three balances —\nsettled, held, and available.\n\nAn account can be funded before any card is issued on it. The balance\nbelongs to the account, not to a card.\n"
    },
    {
      "name": "Cards",
      "description": "The payment instrument. A virtual card, spending from exactly one account.\n"
    },
    {
      "name": "3-D Secure",
      "description": "Cardholder authentication on an online purchase, before the\nauthorization. Almost all of it belongs to the Access Control Server: it\ndecides whether to challenge, picks the method, and verifies the answer.\n\nOne case reaches this API. An out-of-band challenge is answered in an\napplication of yours rather than on the merchant's page, so the\nauthentication becomes an object you read and answer while the merchant's\ncheckout waits.\n"
    },
    {
      "name": "Transactions",
      "description": "Settled money movement."
    },
    {
      "name": "eSIM Packages",
      "x-displayName": "Packages",
      "description": "The catalog of mobile data plans. A package is a purchasable plan: a data\nvolume, a validity period, and the countries it works in.\n\nRead-only and shared across the platform. It changes when coverage or\npricing changes, so cache it for minutes rather than days — and retrieve\nthe one package you are about to buy immediately beforehand, because\nordering a frozen or withdrawn package is refused.\n"
    },
    {
      "name": "eSIM Orders",
      "x-displayName": "Orders",
      "description": "Buying a package. An order allocates an eSIM from inventory and\nprovisions the package onto it before the call returns.\n\nThere is no queue and no callback: a `201` means the eSIM is ready, and\nalmost every failure means nothing was bought.\n\nThe exception is a timeout against the network that provisions the\nprofile. Then the outcome is genuinely unknown to us for a short while,\nand the order appears in your list without an `esim_id` until we settle\nit. That is the one case where an error does not mean \"nothing\nhappened\" — and it is why the list carries unsettled orders at all.\n"
    },
    {
      "name": "eSIM Profiles",
      "x-displayName": "Profiles",
      "description": "The provisioned profile a traveller installs, and the packages running on\nit. One eSIM accumulates packages as it is topped up.\n\nThe activation code is the profile. It is returned when you retrieve one\neSIM and deliberately left off a listing, so a page cannot become a\nhundred secrets in one response.\n"
    },
    {
      "name": "eSIM Top-ups",
      "x-displayName": "Top-ups",
      "description": "More data on a package already running. Nothing is installed and nothing\nis replaced: the traveller keeps the profile they have, so there is no new\nactivation code.\n\nWhich top-ups an eSIM can take depends on what is running on it, so read\nthe list immediately beforehand rather than trusting a cached one.\n"
    },
    {
      "name": "eSIM H5",
      "x-displayName": "H5",
      "description": "A link to a hosted mobile page for one cardholder. You create the link\nfrom your backend and send the cardholder to it; the page itself is\nours.\n\nA link lasts 15 minutes and can be opened any number of times\nuntil then. Create one when the cardholder asks for the page, not ahead\nof time.\n"
    }
  ],
  "x-tagGroups": [
    {
      "name": "Card issuing",
      "tags": [
        "Programs",
        "Cardholders",
        "Accounts",
        "Cards",
        "3-D Secure",
        "Transactions"
      ]
    },
    {
      "name": "eSIM",
      "tags": [
        "eSIM Packages",
        "eSIM Orders",
        "eSIM Profiles",
        "eSIM Top-ups",
        "eSIM H5"
      ]
    },
    {
      "name": "Platform",
      "tags": [
        "Authentication"
      ]
    }
  ],
  "paths": {
    "/oauth/token": {
      "post": {
        "operationId": "issueAccessToken",
        "summary": "Issue an access token",
        "description": "Exchanges a signed client assertion for an opaque access token.\n\nThe assertion is a JWT signed with your private key, carrying:\n\n| Claim | Value |\n| --- | --- |\n| `iss`, `sub` | Your client id (`oc_…`), both |\n| `aud` | This endpoint's absolute URL for the environment you are calling |\n| `exp` | At most five minutes ahead |\n| `jti` | A value unique among your own assertions; each is redeemable once |\n\nSigned with `ES256` or `RS256`. Every registered key must carry a\n`kid` once a client holds more than one; the JWT header's own `kid`\npicks which one verifies the assertion, and omitting it tries them\nall. Send it anyway — it makes a rotation legible in your own logs —\nbut a refusal will not name the key: every pre-verification failure,\nclock skew included, returns the same deliberately generic\n`invalid_client` so that an unauthenticated caller cannot learn\nwhether a client id is real.\n\nOmit `scope` to receive every scope the client holds. Name a scope the\nclient does not hold and the whole request is refused.\n\n**This endpoint answers in RFC 6749's error shape, not the envelope\nthe rest of the API uses.** Failures are `{\"error\": \"…\",\n\"error_description\": \"…\"}`, flat, because that is what OAuth client\nlibraries parse. Nothing here is retryable without changing the\nrequest — a fresh assertion, a corrected audience, or a scope the\nclient actually holds.\n",
        "tags": [
          "Authentication"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued. Not cacheable.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request, unsupported grant, an assertion already\nredeemed, or a scope the client does not hold.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "The client is unknown or revoked, the assertion did not verify,\nit was signed for another environment, or the source address is\nnot permitted.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "500": {
            "description": "An unexpected failure on our side, unrelated to the request.\n\nNo other operation in this document declares a `500` — everywhere\nelse, an unexpected failure still returns the site-wide\n`ErrorResponse` envelope, so leaving it undocumented is safe: a\nreader's assumption carries over correctly. It does not carry\nover here. This endpoint's failures, including this one, are\nRFC 6749's flat shape, not that envelope, and a client that\nassumes otherwise parses the wrong thing at exactly the moment it\nmost needs a useful log line — so this one 500 is spelled out.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "# CLIENT_ASSERTION is the JWT signed by your private key — see the\n# Python and Node samples for how it is produced.\ncurl -X POST https://api.aletaplanet.com/oauth/token \\\n  -d grant_type=client_credentials \\\n  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \\\n  -d \"client_assertion=$CLIENT_ASSERTION\"\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import json, time, uuid, urllib.parse, urllib.request\nimport jwt  # PyJWT[crypto]\n\nTOKEN_URL = \"https://api.aletaplanet.com/oauth/token\"\nnow = int(time.time())\nassertion = jwt.encode(\n    {\"iss\": CLIENT_ID, \"sub\": CLIENT_ID, \"aud\": TOKEN_URL,\n     \"iat\": now, \"exp\": now + 120, \"jti\": str(uuid.uuid4())},\n    PRIVATE_KEY_PEM, algorithm=\"ES256\", headers={\"kid\": \"k1\"},\n)\nform = urllib.parse.urlencode({\n    \"grant_type\": \"client_credentials\",\n    \"client_assertion_type\": \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\",\n    \"client_assertion\": assertion,\n}).encode()\nwith urllib.request.urlopen(urllib.request.Request(TOKEN_URL, data=form)) as r:\n    token = json.load(r)[\"access_token\"]\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "import { SignJWT, importPKCS8 } from 'jose';\nimport { randomUUID } from 'node:crypto';\n\nconst TOKEN_URL = 'https://api.aletaplanet.com/oauth/token';\nconst key = await importPKCS8(PRIVATE_KEY_PEM, 'ES256');\nconst assertion = await new SignJWT({})\n  .setProtectedHeader({ alg: 'ES256', kid: 'k1' })\n  .setIssuer(CLIENT_ID).setSubject(CLIENT_ID).setAudience(TOKEN_URL)\n  .setIssuedAt().setExpirationTime('2m').setJti(randomUUID())\n  .sign(key);\nconst res = await fetch(TOKEN_URL, {\n  method: 'POST',\n  body: new URLSearchParams({\n    grant_type: 'client_credentials',\n    client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',\n    client_assertion: assertion,\n  }),\n});\nconst { access_token } = await res.json();\n"
          }
        ]
      }
    },
    "/v1/programs": {
      "get": {
        "operationId": "listPrograms",
        "summary": "List your programs",
        "description": "Every program your organization can issue against in this environment.\n\nA program's ID is different in sandbox and production, so read it here\ninstead of hard-coding it.\n\nYour access token determines the organization and environment, so you\ndon't need to pass anything else. Use your production credentials to get\nyour production program IDs. The same products will have different\n`prog_` IDs.\n",
        "tags": [
          "Programs"
        ],
        "responses": {
          "200": {
            "description": "Every program attached to your organization in this environment.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "description": "Every program you hold, in a stable order.",
                          "items": {
                            "$ref": "#/components/schemas/Program"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://api.aletaplanet.com/v1/programs \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          }
        ]
      }
    },
    "/v1/cardholders": {
      "post": {
        "operationId": "createCardholder",
        "summary": "Create a cardholder",
        "description": "Creates the cardholder record that accounts and cards belong to.\n\nA new cardholder starts with a `kyc_status` of `not_started` or\n`pending`, depending on the program. A card can't be issued until the\nstatus reaches `approved`.\n\nOnly individual cardholders are supported. Business cardholders will be\nsupported in the future.\n",
        "tags": [
          "Cardholders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardholderCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cardholder created.",
            "headers": {
              "Aleta-Request-Id": {
                "$ref": "#/components/headers/AletaRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cardholder"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/cardholders \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"type\": \"individual\",\n    \"first_name\": \"Test\",\n    \"last_name\": \"Approved\",\n    \"email\": \"test.approved@example.com\",\n    \"address\": {\n      \"line1\": \"350 Orchard Road\",\n      \"city\": \"Singapore\",\n      \"postal_code\": \"238868\",\n      \"country\": \"SGP\"\n    },\n    \"identity_document\": {\n      \"type\": \"national_id\",\n      \"number\": \"S1234567D\"\n    }\n  }'\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const cardholder = await aleta.cardholders.create(\n  {\n    type: 'individual',\n    first_name: 'Test',\n    last_name: 'Approved',\n    email: 'test.approved@example.com',\n    address: {\n      line1: '350 Orchard Road',\n      city: 'Singapore',\n      postal_code: '238868',\n      country: 'SGP',\n    },\n    identity_document: { type: 'national_id', number: 'S1234567D' },\n  },\n  { idempotencyKey: randomUUID() },\n);\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "cardholder = aleta.cardholders.create(\n    type=\"individual\",\n    first_name=\"Test\",\n    last_name=\"Approved\",\n    email=\"test.approved@example.com\",\n    address={\n        \"line1\": \"350 Orchard Road\",\n        \"city\": \"Singapore\",\n        \"postal_code\": \"238868\",\n        \"country\": \"SGP\",\n    },\n    identity_document={\"type\": \"national_id\", \"number\": \"S1234567D\"},\n    idempotency_key=str(uuid.uuid4()),\n)\n"
          }
        ]
      },
      "get": {
        "operationId": "listCardholders",
        "summary": "List cardholders",
        "description": "Returns a page of cardholders, most recently created first.\nCursor-paginated: loop while `has_more`, passing `next_cursor` as\n`starting_after`.\n",
        "tags": [
          "Cardholders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "name": "kyc_status",
            "in": "query",
            "description": "Filter by verification state.",
            "schema": {
              "$ref": "#/components/schemas/KycStatus"
            }
          },
          {
            "name": "reference",
            "in": "query",
            "description": "Filter by your own identifier.",
            "schema": {
              "type": "string",
              "maxLength": 128
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of cardholders.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Cardholder"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/cardholders/{cardholder_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardholderId"
        }
      ],
      "get": {
        "operationId": "getCardholder",
        "summary": "Retrieve a cardholder",
        "description": "Returns a single cardholder, including its current verification state.\n",
        "tags": [
          "Cardholders"
        ],
        "responses": {
          "200": {
            "description": "The cardholder.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cardholder"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateCardholder",
        "summary": "Update a cardholder",
        "description": "Only the fields supplied are changed.",
        "tags": [
          "Cardholders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardholderUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated cardholder.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cardholder"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/v1/cardholders/{cardholder_id}/cards": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardholderId"
        }
      ],
      "get": {
        "operationId": "listCardholderCards",
        "summary": "List a cardholder's cards",
        "description": "Returns every card issued to this cardholder, across all of its\naccounts.\n",
        "tags": [
          "Cardholders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of cards.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Card"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/cardholders/{cardholder_id}/kyc": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardholderId"
        }
      ],
      "post": {
        "operationId": "reportKycOutcome",
        "summary": "Report a KYC outcome",
        "description": "Records the result of an identity check you performed and updates the\ncardholder's `kyc_status`.\n\n**You run the verification; we record the result.** The request is an\nattestation that says who verified the person, when, how, and what the\nresult was. Because this is a compliance record, `provider` and\n`verified_at` are required.\n\nReporting `approved` allows cards to be issued. If you report a new\nresult for a cardholder who was already verified, the new attestation\nreplaces the previous one as the current result. Both records are still\nkept and can be read.\n\n### When you also send the identity and documents\n\nUsually, you only send the attestation. The identity documents stay with\nyou and your verification provider.\n\nSome processors do their own identity checks and need the person's\ndetails and documents before opening an account. For those programs, you\ncan send that information here:\n\n- `identity` contains the required identity fields.\n- `documents` contains the document images.\n\nIf the program requires this evidence and you don't provide it, creating\nan account is rejected with `kyc_identity_required`. The attestation is\nstill recorded normally.\n\nThis is the only API request that accepts files, so it has a larger\nrequest size limit. It's designed for up to three documents, with each\nfile up to 20 MiB, base64 encoded.\n\nDocuments are stored encrypted outside the main database. We don't\nreturn the files later. The `documents` field in responses only contains\nmetadata, with no URL or file content.\n",
        "tags": [
          "Cardholders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KycAttestationRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The recorded attestation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KycAttestation"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/cardholders/chd_01HX8ZQ2K4M7P9RSTVWXYZABCD/kyc \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"status\": \"approved\",\n    \"verified_at\": \"2026-07-29T11:04:00Z\",\n    \"provider\": \"Acme Identity\",\n    \"provider_reference\": \"chk_9f31c7\",\n    \"method\": \"document_and_selfie\",\n    \"expires_at\": \"2028-07-29T00:00:00Z\"\n  }'\n"
          },
          {
            "lang": "curl",
            "label": "cURL, with identity and documents",
            "source": "# For a program whose processor runs its own verification. The three\n# content fields are base64 images, truncated here.\ncurl -X POST https://api.aletaplanet.com/v1/cardholders/chd_01HX8ZQ2K4M7P9RSTVWXYZABCD/kyc \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"status\": \"approved\",\n    \"verified_at\": \"2026-07-29T11:04:00Z\",\n    \"provider\": \"Acme Identity\",\n    \"provider_reference\": \"chk_9f31c7\",\n    \"method\": \"document_and_selfie\",\n    \"expires_at\": \"2028-07-29T00:00:00Z\",\n    \"reference\": \"kyc-20260729-110400\",\n    \"identity\": {\n      \"legal_name\": \"WEI CHEN\",\n      \"gender\": \"male\",\n      \"nationality\": \"SGP\",\n      \"country_of_birth\": \"SGP\",\n      \"document_expires_on\": \"2031-07-29\"\n    },\n    \"documents\": [\n      {\n        \"kind\": \"identity\",\n        \"file_name\": \"passport.jpg\",\n        \"content_type\": \"image/jpeg\",\n        \"content\": \"/9j/4AAQSkZJRgABAQEAYABgAAD...\"\n      },\n      {\n        \"kind\": \"residence\",\n        \"file_name\": \"utility-bill.jpg\",\n        \"content_type\": \"image/jpeg\",\n        \"content\": \"/9j/4AAQSkZJRgABAQEAYABgAAD...\"\n      },\n      {\n        \"kind\": \"selfie\",\n        \"file_name\": \"selfie.jpg\",\n        \"content_type\": \"image/jpeg\",\n        \"content\": \"/9j/4AAQSkZJRgABAQEAYABgAAD...\"\n      }\n    ]\n  }'\n"
          }
        ]
      },
      "get": {
        "operationId": "listKycAttestations",
        "summary": "List KYC attestations",
        "description": "Returns all identity verification attestations recorded for the\ncardholder, newest first.\n\nThe first item is the current one.\n\nIf a cardholder is verified again later, each verification is kept. This\ngives you the full history an auditor may need, without having to\nrebuild it from webhook events.\n",
        "tags": [
          "Cardholders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of attestations, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/KycAttestation"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/accounts": {
      "post": {
        "operationId": "createAccount",
        "summary": "Create an account",
        "description": "Opens a stored-value account for a cardholder in one currency.\n\nIf a cardholder needs two currencies, they need two separate accounts.\nThe API never converts between currencies automatically.\n\n**This is where you set the program.** `program_id` tells us which of\nyour products the account belongs to. Everything connected to the\naccount — its cards, authorizations, and transactions — inherits that\nprogram.\n\nIf `program_id` isn't one of your programs, the API returns\n`invalid_parameter`. It uses the same response whether the program\ndoesn't exist or simply isn't yours, so the API won't reveal whether\nanother program exists on the platform.\n",
        "tags": [
          "Accounts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "`cardholder_not_found`: no cardholder with that id, or it belongs to\nanother organization. The two are deliberately indistinguishable.\n\nNote which failure this is not. A `program_id` that is not one of\nyours is a `400` on the parameter, not a `404` — the program is a\nfield of the request, while the cardholder is a resource the request\nacts on.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/accounts \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"program_id\": \"prog_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    \"cardholder_id\": \"chd_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    \"currency\": \"SGD\"\n  }'\n"
          }
        ]
      },
      "get": {
        "operationId": "listAccounts",
        "summary": "List accounts",
        "description": "Returns a page of accounts. Filter by `cardholder_id` to get the\naccounts belonging to one cardholder, or by `program_id` to get one\nproduct's accounts.\n",
        "tags": [
          "Accounts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "cardholder_id",
            "in": "query",
            "description": "Return only accounts belonging to this cardholder.",
            "schema": {
              "$ref": "#/components/schemas/CardholderIdString"
            }
          },
          {
            "name": "program_id",
            "in": "query",
            "required": false,
            "description": "Return only accounts opened under this program. Omit it and the page\nspans every program your organization holds — which is what one\ncredential covering two products usually wants, and the reason this\nis a filter rather than something read off the token.\n",
            "schema": {
              "$ref": "#/components/schemas/ProgramIdString"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Account"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/accounts/{account_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AccountId"
        }
      ],
      "get": {
        "operationId": "getAccount",
        "summary": "Retrieve an account",
        "description": "Returns a single account, including its current balances.\n\nThe `ledger_balance` is the total value in the account. The\n`available_balance` is the value that can be spent, which is the ledger\nbalance minus any funds held by pending authorizations.\n",
        "tags": [
          "Accounts"
        ],
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/accounts/{account_id}/credit": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AccountId"
        }
      ],
      "post": {
        "operationId": "creditAccount",
        "x-aleta-capability": "account_credit",
        "summary": "Credit an account",
        "description": "Adds money to an account, increasing both `ledger_balance` and\n`available_balance`.\n\nThere are two ways to prevent duplicate credits, and you should use\nboth:\n\n- `Idempotency-Key` protects against retries. Using the same key within\n24 hours returns the stored response instead of creating another credit.\n- `client_transaction_id` identifies the actual credit. It must be\nunique for the account and doesn't expire. Reusing the same ID with the\nsame amount returns `200` with `Idempotent-Replay: true` and doesn't\nmove any money. Reusing it with a different amount returns `409\nadjustment_already_applied`.\n\nSend both on every funding request.\n",
        "tags": [
          "Accounts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountAdjustmentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated account. Carries `Idempotent-Replay: true` when the\n`client_transaction_id` had already been applied.\n",
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/MovementAlreadyApplied"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/v1/accounts/{account_id}/debit": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AccountId"
        }
      ],
      "post": {
        "operationId": "debitAccount",
        "x-aleta-capability": "account_debit",
        "summary": "Debit an account",
        "description": "Removes money from an account. If the `available_balance` is too low,\nthe request fails with `insufficient_funds` and a `402`. Money held by\npending authorizations can't be debited.\n\nThere are two ways to prevent duplicate debits, and you should use both:\n\n- `Idempotency-Key` protects against retries. Using the same key within\n24 hours returns the stored response instead of creating another debit.\n- `client_transaction_id` identifies the actual debit. It must be unique\nfor the account and doesn't expire. Reusing the same ID with the same\namount returns `200` with `Idempotent-Replay: true` and doesn't move any\nmoney. Reusing it with a different amount returns `409\nadjustment_already_applied`.\n\nSend both on every debit request.\n",
        "tags": [
          "Accounts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountAdjustmentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated account. Carries `Idempotent-Replay: true` when the\n`client_transaction_id` had already been applied.\n",
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "409": {
            "$ref": "#/components/responses/MovementAlreadyApplied"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/v1/accounts/{account_id}/cards": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AccountId"
        }
      ],
      "get": {
        "operationId": "listAccountCards",
        "summary": "List an account's cards",
        "description": "Returns every card that spends from this account. An account can fund\nseveral cards; a card spends from exactly one account.\n",
        "tags": [
          "Accounts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of cards.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Card"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/accounts/{account_id}/block": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AccountId"
        }
      ],
      "post": {
        "operationId": "blockAccount",
        "x-aleta-capability": "account_status",
        "summary": "Block an account",
        "description": "Suspends the account. Every card funded by it declines with\n`account_blocked` until the account is unblocked, and credits and debits\nare refused.\n\nBlocking is reversible and does not touch the balances. Holds already\nplaced remain in force and still release on clearing or expiry.\n",
        "tags": [
          "Accounts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountBlockRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The blocked account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/accounts/acct_01HX8ZQ2K4M7P9RSTVWXYZABCD/block \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"reason\": \"Suspected compromise, pending review\"}'\n"
          }
        ]
      }
    },
    "/v1/accounts/{account_id}/unblock": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AccountId"
        }
      ],
      "post": {
        "operationId": "unblockAccount",
        "x-aleta-capability": "account_status",
        "summary": "Unblock an account",
        "description": "Returns a blocked account to `active`. Cards funded by it resume\nauthorizing immediately.\n",
        "tags": [
          "Accounts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The unblocked account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/v1/cards": {
      "post": {
        "operationId": "createCard",
        "summary": "Issue a card",
        "description": "Issues a virtual or a physical card for an account.\n\nA virtual card needs no more than the cardholder and the account. A\nphysical card also needs `embossed_name`, the name printed on it, and a\n`delivery_address`. To have plastic made for a card you already hold, use\n`POST /v1/cards/{card_id}/request-physical` instead.\n\nThe response only includes `last_four`. The full card number and CVV are\nnever returned here. Getting them is a separate, separately scoped and\naudited operation.\n\nA card is never usable straight away. It goes through the same statuses\non every program:\n\n1. It starts as `pending` while the processor issues it. On a program\n   that issues at once, the response already says `inactive`.\n2. It becomes `inactive` when the processor has the card, and you get\n   `card.issued`.\n3. It becomes `active` when you call `POST /v1/cards/{card_id}/activate`,\n   and you get `card.activated`. Until then every authorization is\n   declined.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Card accepted. `pending` while the processor issues it, or\n`inactive` on a program that issues at once.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/cards \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"cardholder_id\": \"chd_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    \"account_id\": \"acct_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    \"form_factor\": \"virtual\"\n  }'\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const card = await aleta.cards.create(\n  {\n    cardholder_id: 'chd_01HX8ZQ2K4M7P9RSTVWXYZABCD',\n    account_id: 'acct_01HX8ZQ2K4M7P9RSTVWXYZABCD',\n    form_factor: 'virtual',\n  },\n  { idempotencyKey: randomUUID() },\n);\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "card = aleta.cards.create(\n    cardholder_id=\"chd_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    account_id=\"acct_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    form_factor=\"virtual\",\n    idempotency_key=str(uuid.uuid4()),\n)\n"
          }
        ]
      },
      "get": {
        "operationId": "listCards",
        "summary": "List cards",
        "description": "Returns a page of cards. Filter by `status` to find, for example,\nevery frozen card, or by `program_id` to narrow to one product.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Return only cards in this status.",
            "schema": {
              "$ref": "#/components/schemas/CardStatus"
            }
          },
          {
            "name": "form_factor",
            "in": "query",
            "description": "Return only cards of this form factor.",
            "schema": {
              "$ref": "#/components/schemas/CardFormFactor"
            }
          },
          {
            "name": "account_id",
            "in": "query",
            "description": "Return only cards that spend from this account.",
            "schema": {
              "$ref": "#/components/schemas/AccountIdString"
            }
          },
          {
            "name": "program_id",
            "in": "query",
            "required": false,
            "description": "Return only cards belonging to this program. Omit it and the page\nspans every program your organization holds.\n",
            "schema": {
              "$ref": "#/components/schemas/ProgramIdString"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of cards.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Card"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/cards/{card_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "get": {
        "operationId": "getCard",
        "summary": "Retrieve a card",
        "description": "Returns a single card. The full card number is never included —\n`last_four` is the only part of the number returned here.\n",
        "tags": [
          "Cards"
        ],
        "responses": {
          "200": {
            "description": "The card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/cards/{card_id}/freeze": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "freezeCard",
        "summary": "Freeze a card",
        "description": "Temporarily blocks the card. Reversible — use this rather than closing\nfor a card the cardholder has merely mislaid.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardReasonRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The frozen card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/v1/cards/{card_id}/unfreeze": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "unfreezeCard",
        "summary": "Unfreeze a card",
        "description": "Returns a frozen card to `active`.\n\nNote that the card returns to `active` regardless of what its state was\nbefore it was frozen — lifecycle and freeze state share one field. See\nthe card lifecycle guide.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The unfrozen card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/v1/cards/{card_id}/replace": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "replaceCard",
        "summary": "Replace a card",
        "description": "Issues a new card against the same cardholder and account, then closes\nthis one. Returns the new card. The new card has a new `id`, number and\nexpiry, and keeps the original's `reference`.\n\nThe new card follows the usual issuing flow: `active` straight away, or\n`pending` and then `active` with `card.issued` on a program that issues\nasynchronously. The original is closed and emits `card.closed`.\n\nThe new card is issued before the original is closed, so a failure never\nleaves the cardholder without a card. If the close fails, the response\nis an error naming both cards and the original stays `active`: close it\nwith `POST /v1/cards/{card_id}/close` and do not repeat the replacement,\nwhich would issue another card.\n\nA closed card cannot be replaced.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardReplaceRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The replacement card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/v1/cards/{card_id}/close": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "closeCard",
        "summary": "Close a card",
        "description": "Permanently terminates the card. **Not reversible.** Any network tokens\nderived from it are invalidated. To block temporarily, freeze instead.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardReasonRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The closed card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/v1/cards/{card_id}/activate": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "activateCard",
        "x-aleta-capability": "card_activation",
        "summary": "Activate a card",
        "description": "Makes an `inactive` card spendable. It is a server-to-server call: there is\nno hosted page and no URL to open, and the response is the card.\n\nThe same call activates a virtual card and a physical one. Call it when\nyou receive `card.issued` and you are ready for the card to be used.\n\n- A `pending` card is refused with `card_invalid_state`. Wait for\n  `card.issued`; the card is never issued by calling this endpoint.\n- A card that is already `active` is refused with `card_already_activated`.\n- The cardholder must still be verified. A cardholder who is no longer\n  `approved` is refused with `kyc_not_approved`.\n\nActivation takes effect at once. A card that fails to activate stays\n`inactive` and can be activated again.\n\nRequires the `cards:write` scope.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The card, now `active`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/cards/card_01HX8ZQ2K4M7P9RSTVWXYZABCD/activate \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\"\n"
          }
        ]
      }
    },
    "/v1/cards/{card_id}/request-physical": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "requestPhysicalCard",
        "x-aleta-capability": "card_physical_upgrade",
        "summary": "Request a physical card",
        "description": "Orders plastic for a card issued without it. The response is the same\ncard, not a new one: same `id`, same number, same balance, with\n`form_factor` now `physical` and `fulfillment.status` `pending`. It keeps\nworking while the plastic is made.\n\nThere is no request body. The name printed on the card and the address it\nis posted to were both fixed when the card was issued, and this operation\ncannot revise either.\n\n- A card that is not `active` is refused with `card_invalid_state`. So is\n  a card that is already `physical`, which is what a second request gets.\n- A closed card is refused with `card_closed`.\n- A program that does not issue physical cards is refused with\n  `unsupported_form_factor`.\n\nNo event is emitted. Nothing reports when a card is posted, so the next\nthing you learn is the cardholder telling you it arrived — call\n`POST /v1/cards/{card_id}/activate` then to enable the card for use in\nperson.\n\nYou can also issue a physical card directly, without a virtual card\nfirst, with `POST /v1/cards` and `form_factor: physical`.\n\nRequires the `cards:write` scope.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The card, now `physical`, with the plastic on its way.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/cards/card_01HX8ZQ2K4M7P9RSTVWXYZABCD/request-physical \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\"\n"
          }
        ]
      }
    },
    "/v1/cards/{card_id}/pin-key": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "createCardPinKey",
        "x-aleta-capability": "card_pin",
        "summary": "Get a PIN encryption key",
        "description": "Returns a short-lived RSA public key to encrypt a PIN to. It is the\nfirst of two server-to-server calls that set a PIN: you encrypt the PIN\nto this key, then send it with `POST /v1/cards/{card_id}/pin`.\n\nThe key is for one card and expires after a few minutes. Ask for it when\nthe cardholder is about to enter their PIN, not when the screen loads.\nIt opens one PIN: once it has been sent with a PIN, whatever the\noutcome, it can't be used again.\n\nEvery call returns a new key. `Idempotency-Key` is accepted like on every\nother `POST`, but it is not used here, because a replay would hand back a\nkey that has already been spent.\n\nOnly a physical card that is `active` has a PIN to set. Any other card\nis refused with `card_invalid_state`, and a closed card with\n`card_closed`.\n\nRequires the `cards:secrets` scope. A program whose processor does not\ntake a PIN by a call does not have this endpoint at all.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "201": {
            "description": "The key to encrypt the PIN to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CardPinKey"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/cards/card_01HX8ZQ2K4M7P9RSTVWXYZABCD/pin-key \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\"\n"
          }
        ]
      }
    },
    "/v1/cards/{card_id}/pin": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "setCardPin",
        "x-aleta-capability": "card_pin",
        "summary": "Set a PIN",
        "description": "Sets the PIN of a physical card from a PIN you have encrypted. It is a\nserver-to-server call: there is no page to open. The same call sets a first\nPIN and resets a forgotten one.\n\n1. Get a key with `POST /v1/cards/{card_id}/pin-key`.\n2. Encrypt the PIN, as its digits, to that key with RSA-OAEP using SHA-256,\n   and encode the result as base64.\n3. Send `key_id` and the encrypted PIN here.\n\n**Encrypt on the cardholder's device.** The PIN is ciphertext while it\npasses through your systems, and it stays out of PCI DSS scope only while\nyour servers never see it in the clear. A server that handles the PIN\nunencrypted is in scope.\n\nWe check only what a PIN needs to be usable: digits, in the length the key\nresponse gives as `pin_length`. Anything else is your policy, such as whether\nto refuse repeated digits or sequences, and is yours to validate before you\nencrypt. A PIN that isn't digits of that length is refused with\n`pin_invalid`. A key that is unknown, already used or expired is refused with\n`invalid_parameter`; get a new one.\n\nOnly a physical card that is `active` has a PIN to set. Any other card is\nrefused with `card_invalid_state`, and a closed card with `card_closed`.\n\nNever write the PIN, encrypted or not, to a log, an analytics event, web\nstorage or a support ticket.\n\nRequires the `cards:secrets` scope.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardPinRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "The PIN is set. Nothing is returned."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/cards/card_01HX8ZQ2K4M7P9RSTVWXYZABCD/pin \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"key_id\": \"3f9c0a6e1b7d4e2a\", \"encrypted_pin\": \"q3G1...base64...\"}'\n"
          }
        ]
      }
    },
    "/v1/cards/{card_id}/secrets": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CardId"
        }
      ],
      "post": {
        "operationId": "revealCardSecrets",
        "x-aleta-capability": "card_secrets",
        "summary": "Reveal card secrets",
        "description": "Returns the card's number and security code, each encrypted to a public key\nyou supply. It is a server-to-server call: there is no page to open, and\nonly the holder of the matching private key can read the values.\n\nSend an RSA public key in PEM form, of at least 2048 bits. The response\ncarries `encrypted_pan` and `encrypted_cvv`, each encrypted with RSA-OAEP\nusing SHA-256 and encoded as base64. `last_four` and `expires_at` come back\nin the clear.\n\nOnly an `active` card has details to show. Any other card is refused with\n`card_invalid_state`, and a closed card with `card_closed`.\n\nThe response is never cached. `Idempotency-Key` is accepted like on every\nother `POST`, but it is not used here: a replay would have to keep a copy of\nthe first response, so every call is answered afresh with a new ciphertext.\n\nNever write the decrypted values to a log, an analytics event, web storage\nor a support ticket.\n\nRequires the `cards:secrets` scope. A program whose processor cannot\nreturn card details does not have this endpoint at all.\n",
        "tags": [
          "Cards"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardSecretsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The card's number and security code, encrypted to your key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CardSecrets"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/cards/card_01HX8ZQ2K4M7P9RSTVWXYZABCD/secrets \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"$(jq -Rs '{public_key: .}' public.pem)\"\n"
          }
        ]
      }
    },
    "/v1/authentications": {
      "get": {
        "operationId": "listAuthentications",
        "x-aleta-capability": "authentications",
        "summary": "List authentications",
        "description": "Returns a page of out-of-band authentications, newest first.\n\nThis is a recovery path for a broken push channel, not a polling loop. A\nchallenge expires in minutes, so polling often enough to catch one costs\nmore rate limit than it is worth; subscribe to\n`authentication.challenged` and read this when your own notifications\nfail.\n\nRequires the `authentications:read` scope.\n",
        "tags": [
          "3-D Secure"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "card_id",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/CardIdString"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/AuthenticationStatus"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of authentications.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Authentication"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/authentications/{authentication_id}": {
      "parameters": [
        {
          "name": "authentication_id",
          "in": "path",
          "required": true,
          "schema": {
            "$ref": "#/components/schemas/AuthenticationIdString"
          }
        }
      ],
      "get": {
        "operationId": "getAuthentication",
        "x-aleta-capability": "authentications",
        "summary": "Retrieve an authentication",
        "description": "Returns a single authentication. `authentication.challenged` already\ncarries the whole object, so a handler that trusts the event does not\nneed this call.\n\nRequires the `authentications:read` scope.\n",
        "tags": [
          "3-D Secure"
        ],
        "responses": {
          "200": {
            "description": "The authentication.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Authentication"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/authentications/{authentication_id}/approve": {
      "parameters": [
        {
          "name": "authentication_id",
          "in": "path",
          "required": true,
          "schema": {
            "$ref": "#/components/schemas/AuthenticationIdString"
          }
        }
      ],
      "post": {
        "operationId": "approveAuthentication",
        "x-aleta-capability": "authentications",
        "summary": "Approve an authentication",
        "description": "Tells the Access Control Server the cardholder proved who they are, and\nreleases the merchant's checkout to continue.\n\nCall it only after a step-up on the device, immediately before: a\nbiometric or the device passcode. An application that approves because\nit happens to be unlocked has removed the authentication rather than\nmoved it.\n\nAnswer once. A second answer under a different `Idempotency-Key` returns\n`422 authentication_not_pending`.\n\nRequires the `authentications:write` scope.\n",
        "tags": [
          "3-D Secure"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The authentication, now `approved`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Authentication"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/authentications/authn_01HX8ZQ2K4M7P9RSTVWXYZABCD/approve \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\"\n"
          }
        ]
      }
    },
    "/v1/authentications/{authentication_id}/decline": {
      "parameters": [
        {
          "name": "authentication_id",
          "in": "path",
          "required": true,
          "schema": {
            "$ref": "#/components/schemas/AuthenticationIdString"
          }
        }
      ],
      "post": {
        "operationId": "declineAuthentication",
        "x-aleta-capability": "authentications",
        "summary": "Decline an authentication",
        "description": "Tells the Access Control Server the cardholder refused, or that your\nstep-up failed. The purchase fails at the merchant.\n\nDeclining is worth doing rather than letting the challenge expire: the\ncardholder who taps \"this wasn't me\" gets an immediate answer, and you\nget a record of a refusal instead of a timeout.\n\nRequires the `authentications:write` scope.\n",
        "tags": [
          "3-D Secure"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The authentication, now `declined`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Authentication"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/authentications/authn_01HX8ZQ2K4M7P9RSTVWXYZABCD/decline \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\"\n"
          }
        ]
      }
    },
    "/v1/transactions": {
      "get": {
        "operationId": "listTransactions",
        "x-aleta-capability": "transactions",
        "summary": "List transactions",
        "description": "Returns a page of transactions, most recently settled first.\nTransactions are read-only.\n",
        "tags": [
          "Transactions"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "card_id",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/CardIdString"
            }
          },
          {
            "name": "account_id",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/AccountIdString"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of transactions.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Transaction"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/transactions/{transaction_id}": {
      "parameters": [
        {
          "name": "transaction_id",
          "in": "path",
          "required": true,
          "schema": {
            "$ref": "#/components/schemas/TransactionIdString"
          }
        }
      ],
      "get": {
        "operationId": "getTransaction",
        "x-aleta-capability": "transactions",
        "summary": "Retrieve a transaction",
        "description": "Returns a single transaction.\n",
        "tags": [
          "Transactions"
        ],
        "responses": {
          "200": {
            "description": "The transaction.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transaction"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/esim/packages": {
      "get": {
        "operationId": "listEsimPackages",
        "summary": "List eSIM packages",
        "description": "Returns the packages on sale — where each one works, how long it\nlasts, and what it costs you.\n\n**Only `status: available` is listed.** A package we have frozen or\nwithdrawn is not here, so a storefront can render this response\nwholesale without checking each row. To read one anyway — an order\nfrom last month references it — retrieve it by id.\n\n**This list is not paginated, and it is not sortable.** The catalog is\nsmall and bounded, so the response carries all of it. There is no\n`limit`, no cursor, and no `has_more` to loop on. Narrow it with\n`country`; sort it yourself — you already hold every row, so a server\nround trip to reorder them would buy nothing.\n\nPackages come back ordered by `name`, ascending. The order is stable,\nso two calls with the same filter return the same sequence.\n\nNothing is provisioned here. A package becomes an eSIM profile only\nwhen it is ordered.\n\n`volume` is in megabytes, and it carries its own `type`. Read both:\nthe same `1024` is 1 GB on a `total` plan and 30 GB on a 30-day\n`daily` one.\n\nThe catalog is shared and changes rarely. Cache it and refresh on a\ntimer rather than calling this on every page view.\n",
        "tags": [
          "eSIM Packages"
        ],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "description": "Return only packages valid in this country, whether they are\nsingle-country or regional. Repeat the parameter to express OR.\n\nAccepts any code from\n[list supported countries](https://developer.aletaplanet.com/api-reference/resources/list-esim-countries).\nA code outside that list is well-formed but matches nothing.\n",
            "schema": {
              "$ref": "#/components/schemas/EsimCountryCode"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The catalog, in full.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The list envelope without cursors. The catalog is small and\nbounded, so this returns all of it — there is no `has_more`\nto loop on and no page to ask for next.\n",
                  "required": [
                    "object",
                    "data"
                  ],
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "list"
                    },
                    "url": {
                      "type": "string",
                      "examples": [
                        "/v1/esim/packages"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EsimPackage"
                      }
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl \"https://api.aletaplanet.com/v1/esim/packages?country=JP\" \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const packages = await aleta.esim.packages.list({ country: 'JP' });\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "packages = aleta.esim.packages.list(country=\"JP\")\n"
          }
        ]
      }
    },
    "/v1/esim/packages/{package_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EsimPackageId"
        }
      ],
      "get": {
        "operationId": "getEsimPackage",
        "summary": "Retrieve an eSIM package",
        "description": "Returns a single package, **whatever its status**. Unlike the list,\nthis resolves a package we have frozen or withdrawn, so an order\nplaced months ago still names something readable.\n\nRead a package here immediately before you charge for it. A cached\nlist can be stale on the two fields that matter at that moment —\n`price` and `status` — and neither changes in a way your copy can\ndetect on its own.\n",
        "tags": [
          "eSIM Packages"
        ],
        "responses": {
          "200": {
            "description": "The package.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EsimPackage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://api.aletaplanet.com/v1/esim/packages/epkg_01HX8ZQ2K4M7P9RSTVWXYZABCD \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const pkg = await aleta.esim.packages.retrieve('epkg_01HX8ZQ2K4M7P9RSTVWXYZABCD');\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "pkg = aleta.esim.packages.retrieve(\"epkg_01HX8ZQ2K4M7P9RSTVWXYZABCD\")\n"
          }
        ]
      }
    },
    "/v1/esim/countries": {
      "get": {
        "operationId": "listEsimCountries",
        "summary": "List supported countries",
        "description": "Returns every country the eSIM catalog covers.\n\nThis is the vocabulary the `country` filter on\n[list packages](https://developer.aletaplanet.com/api-reference/resources/list-esim-packages) accepts. A code\nthat is not here matches nothing, which is why it is worth reading\nonce at integration time rather than hardcoding a list of your own:\ncoverage moves as packages are added and withdrawn, and this list\nmoves with it.\n\nEvery country here has at least one `available` package. A country\nwhose only packages are frozen or withdrawn drops out.\n\nNot paginated — there are fewer than two hundred of these. Ordered by\n`name`, ascending, and stably.\n",
        "tags": [
          "eSIM Packages"
        ],
        "responses": {
          "200": {
            "description": "The covered countries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "object",
                    "data"
                  ],
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "list"
                    },
                    "url": {
                      "type": "string",
                      "examples": [
                        "/v1/esim/countries"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EsimCountry"
                      }
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://api.aletaplanet.com/v1/esim/countries \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const countries = await aleta.esim.countries.list();\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "countries = aleta.esim.countries.list()\n"
          }
        ]
      }
    },
    "/v1/esim/orders": {
      "post": {
        "operationId": "createEsimOrder",
        "summary": "Order an eSIM package",
        "description": "Buys one package.\n\n`package_id` names what you are buying; the package must be\n`available` at the moment of the call, which is why it is worth\n[retrieving it](https://developer.aletaplanet.com/api-reference/resources/get-esim-package)\nimmediately beforehand rather than trusting a cached list. Ordering a\nfrozen or withdrawn package is refused with\n`esim_package_unavailable`.\n\nAn eSIM is allocated from our inventory and the package provisioned\nonto it before this call returns. There is no queue and no callback:\na `201` means the eSIM is ready, and almost every failure means nothing\nwas bought.\n\n**The exception is a `502` from a timeout** against the network that\nprovisions the profile. We cannot tell in that moment whether it took\nthe order, so we neither charge you nor claim it failed. The order\nappears in your order list without an `esim_id` until we settle it.\nLook it up by your own `reference`. Do not re-send under a new\n`reference` — that is a second purchase. If you still hold the\n`Idempotency-Key`, replaying it is the better move.\n\n`reference` is your order id, and it must be unique within your\norganization.\n\n`activation_code` on the response is the LPA string a traveller\nscans. Encode it as a QR yourself — it is a short string, every\nplatform has a one-line library for it, and a hosted image would be a\nURL that installs somebody's eSIM for whoever opens it.\n\n`price` on the response is the price charged, captured at order time.\nIt does not track the catalog afterwards.\n\n**Send an `Idempotency-Key`.** A timed-out order retried without one\nis a second purchase, not a second look at the first.\n",
        "tags": [
          "eSIM Orders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EsimOrderCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EsimOrder"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/esim/orders \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"package_id\": \"epkg_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    \"reference\": \"order-4471\"\n  }'\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const order = await aleta.esim.orders.create(\n  { package_id: 'epkg_01HX8ZQ2K4M7P9RSTVWXYZABCD', reference: 'order-4471' },\n  { idempotencyKey: randomUUID() },\n);\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "order = aleta.esim.orders.create(\n    package_id=\"epkg_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    reference=\"order-4471\",\n    idempotency_key=str(uuid.uuid4()),\n)\n"
          }
        ]
      },
      "get": {
        "operationId": "listEsimOrders",
        "summary": "List eSIM orders",
        "description": "Returns a page of your orders, newest first.\n\nFilter by `reference` to answer the question this endpoint exists\nfor: **did my order go through?** A request that timed out has an\nunknown outcome, and your own order id is the only handle you are\nguaranteed to still hold. `reference` is unique within your\norganization, so this returns one row or none.\n\nIt answers in three ways, and the third is why the row exists:\n\n- **no row** — we never took the order; nothing was charged.\n- **a settled row** — it went through. A package order carries an\n  `esim_id`; a top-up carries a `package_id` and a `price`.\n- **an unsettled row** — we do not know yet. The provisioning\n  network timed out and we are settling it, usually within minutes.\n  Ask again rather than re-ordering; a new `reference` would be a\n  second purchase.\n\n**Branch on `object` before you check**, because there is no single\nfield that answers it for both. The two order kinds learn things in\nopposite orders, so each is still missing what the other already\nknows — see `EsimOrderSummary` and `EsimTopUpOrder` for which is\nwhich.\n\n`Idempotency-Key` covers the same ground for 24 hours and returns the\noriginal response outright, so reach for that first if you still have\nthe key. This is what is left when you do not.\n\nPackage purchases and top-ups share this ledger. **Branch on\n`object`** — `esim_order` against `esim_top_up_order` — rather than\nassuming every row has the same fields.\n\nNo row carries an `activation_code`, for the reason a list of eSIMs\ndoes not carry one either. Retrieve the order, or the eSIM, when you\nneed to deliver install instructions.\n",
        "tags": [
          "eSIM Orders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "reference",
            "in": "query",
            "description": "Return only the order carrying this reference.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of orders.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "oneOf": [
                              {
                                "$ref": "#/components/schemas/EsimOrderSummary"
                              },
                              {
                                "$ref": "#/components/schemas/EsimTopUpOrder"
                              }
                            ],
                            "discriminator": {
                              "propertyName": "object"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl \"https://api.aletaplanet.com/v1/esim/orders?reference=order-4471\" \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const page = await aleta.esim.orders.list({ reference: 'order-4471' });\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "page = aleta.esim.orders.list(reference=\"order-4471\")\n"
          }
        ]
      }
    },
    "/v1/esim/orders/{order_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EsimOrderId"
        }
      ],
      "get": {
        "operationId": "getEsimOrder",
        "summary": "Retrieve an eSIM order",
        "description": "Returns one order. **Branch on `object`:** a package purchase comes\nback as `esim_order` and carries the `activation_code` the create\ncall returned, while a top-up comes back as `esim_top_up_order` and\ncarries none — it added data to a profile that was already\ninstalled.\n\nRe-reading a package order is how you recover install instructions\nwithout keeping the secret yourself.\n\nIf you are here because a create request timed out and you no longer\nhave its `Idempotency-Key`, you will not have this id either — look\nthe order up by your own `reference` instead.\n\nAn order found that way may still be settling, in which case it comes\nback without an `esim_id` or an `activation_code` (without a\n`top_up_id`, for a top-up). That is not an error and not a `404`; it\nmeans the answer is not in yet.\n",
        "tags": [
          "eSIM Orders"
        ],
        "responses": {
          "200": {
            "description": "The order.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/EsimOrder"
                    },
                    {
                      "$ref": "#/components/schemas/EsimTopUpOrder"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "object"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://api.aletaplanet.com/v1/esim/orders/eord_01HX8ZQ2K4M7P9RSTVWXYZABCD \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const order = await aleta.esim.orders.retrieve('eord_01HX8ZQ2K4M7P9RSTVWXYZABCD');\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "order = aleta.esim.orders.retrieve(\"eord_01HX8ZQ2K4M7P9RSTVWXYZABCD\")\n"
          }
        ]
      }
    },
    "/v1/esims": {
      "get": {
        "operationId": "listEsims",
        "summary": "List allocated eSIMs",
        "description": "Returns a page of the eSIMs allocated to you, newest first.\n\nRows are `EsimSummary`, which leaves out two fields on purpose.\n`packages` is one: an eSIM accumulates them as it is topped up, and\ninlining every one on every row turns a page into a payload nobody\nasked for.\n\n`activation_code` is the other, and for a different reason. Each one\ninstalls a profile, so a page of a hundred would be a hundred secrets\nin a single response — easy to log, easy to proxy into a browser,\nimpossible to take back. Retrieve an eSIM when you need to deliver\nits install instructions.\n\nFilter by `order_reference` to find the eSIM behind one of your\norders, or by `iccid` when a traveller reads theirs off the handset.\n",
        "tags": [
          "eSIM Profiles"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "order_reference",
            "in": "query",
            "description": "Return only the eSIM allocated to the order carrying this\nreference. Unique per organization, so this returns at most one.\n",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          },
          {
            "name": "iccid",
            "in": "query",
            "description": "Return only the eSIM with this ICCID.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{19,20}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of eSIMs.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EsimSummary"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl \"https://api.aletaplanet.com/v1/esims?limit=50\" \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const page = await aleta.esims.list({ limit: 50 });\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "page = aleta.esims.list(limit=50)\n"
          }
        ]
      }
    },
    "/v1/esims/{esim_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EsimId"
        }
      ],
      "get": {
        "operationId": "getEsim",
        "summary": "Retrieve an eSIM",
        "description": "Returns one eSIM and every package on it.\n\n`packages` is the whole history, not just what is live — a package\nthat has expired stays listed. Read `expires_at` to tell them apart\nrather than assuming the array holds only current ones.\n\nData counters come from the network and settle with a delay. Treat\n`volume.remaining` as recent rather than exact, and never as a\nbilling figure.\n",
        "tags": [
          "eSIM Profiles"
        ],
        "responses": {
          "200": {
            "description": "The eSIM.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esim"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://api.aletaplanet.com/v1/esims/esim_01HX8ZQ2K4M7P9RSTVWXYZABCD \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const esim = await aleta.esims.retrieve('esim_01HX8ZQ2K4M7P9RSTVWXYZABCD');\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "esim = aleta.esims.retrieve(\"esim_01HX8ZQ2K4M7P9RSTVWXYZABCD\")\n"
          }
        ]
      }
    },
    "/v1/esims/{esim_id}/top_ups": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EsimId"
        }
      ],
      "get": {
        "operationId": "listEsimTopUps",
        "summary": "List available top-ups",
        "description": "Returns the add-ons this eSIM can take right now, **grouped by the\npackage each one extends**.\n\nAn eSIM can carry several packages at once, and an add-on belongs to\nexactly one of them — a top-up extends the package it was sold\nagainst rather than the profile as a whole. The grouping is what\ntells you which is which, so read `package_id` before you sell.\n\nOnly running packages appear. A package that has been bought but\nnever reached a network cannot be extended yet, and a package sold\nwithout add-ons is left out rather than listed empty — so an empty\n`data` means there is nothing to sell on this eSIM today.\n\nWithin a group, add-ons come smallest first. Not paginated: a package\ncarries a handful of these, not a catalog.\n",
        "tags": [
          "eSIM Top-ups"
        ],
        "responses": {
          "200": {
            "description": "The add-ons available on this eSIM.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "object",
                    "data"
                  ],
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "list"
                    },
                    "url": {
                      "type": "string",
                      "examples": [
                        "/v1/esims/esim_01HX8ZQ2K4M7P9RSTVWXYZABCD/top_ups"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EsimPackageTopUps"
                      }
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://api.aletaplanet.com/v1/esims/esim_01HX8ZQ2K4M7P9RSTVWXYZABCD/top_ups \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\"\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const topUps = await aleta.esims.topUps.list('esim_01HX8ZQ2K4M7P9RSTVWXYZABCD');\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "top_ups = aleta.esims.top_ups.list(\"esim_01HX8ZQ2K4M7P9RSTVWXYZABCD\")\n"
          }
        ]
      },
      "post": {
        "operationId": "createEsimTopUp",
        "summary": "Order a top-up",
        "description": "Adds data to a package already running on this eSIM.\n\nNothing is installed and nothing is replaced: the traveller keeps the\nprofile they have, and the data lands against the package the add-on\nbelongs to. There is no new activation code because there is no new\nprofile.\n\n`top_up_id` must be one currently listed for this eSIM. An add-on\nthat has stopped being sold, or one belonging to a package that is\nnot running, is refused with `esim_top_up_unavailable` — read the\nlist immediately beforehand rather than trusting a cached one.\n\nOne top-up, bought once. A top-up lands on the allowance the\npackage is running **today** — it cannot be bought ahead for a later\nday, and it expires with the package it extends, so one bought late\nin that package's life buys less time than the same one bought\nearly.\n\n**Send an `Idempotency-Key`.** A timed-out top-up retried without one\nis a second purchase.\n",
        "tags": [
          "eSIM Top-ups"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EsimTopUpCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The top-up order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EsimTopUpOrder"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/esims/esim_01HX8ZQ2K4M7P9RSTVWXYZABCD/top_ups \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"top_up_id\": \"etop_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    \"reference\": \"order-4472\"\n  }'\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const order = await aleta.esims.topUps.create(\n  'esim_01HX8ZQ2K4M7P9RSTVWXYZABCD',\n  { top_up_id: 'etop_01HX8ZQ2K4M7P9RSTVWXYZABCD', reference: 'order-4472' },\n  { idempotencyKey: randomUUID() },\n);\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "order = aleta.esims.top_ups.create(\n    \"esim_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    top_up_id=\"etop_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    reference=\"order-4472\",\n    idempotency_key=str(uuid.uuid4()),\n)\n"
          }
        ]
      }
    },
    "/v1/esim/h5_links": {
      "post": {
        "operationId": "createEsimH5Link",
        "summary": "Create an H5 link",
        "description": "Creates a link to the hosted eSIM page for one cardholder.\n\nRequires the `esim:write` scope.\n\n`cardholder_id` names who the page is for, and must be a cardholder\nin your organization. One outside it is refused with `404`, exactly\nas if it did not exist.\n\n**The link lasts 15 minutes.** `expires_at` is always `created_at`\nplus 15 minutes, and the lifetime is not configurable. Until\nthen the link opens as many times as you like; after it, the page\nrefuses it and you create a new one. A link cannot be retrieved,\nlisted, or revoked once created.\n\nCreate it from your backend when the cardholder asks for the page,\nand redirect them straight to `url`. A link sent by email, SMS, or\npush has usually expired by the time it is opened — send a link to\nyour own endpoint instead, and have that endpoint create a fresh H5\nlink when it is followed.\n\n**Treat `url` as a credential.** Anyone holding it acts as that\ncardholder until it expires. Keep it out of logs, analytics, and\nstorage.\n\nA retry with the same `Idempotency-Key` returns the original link,\n`expires_at` included — even after it has expired, since keys are\nkept for 24 hours. Use a fresh key for every new link.\n",
        "tags": [
          "eSIM H5"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EsimH5LinkCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EsimH5Link"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl -X POST https://api.aletaplanet.com/v1/esim/h5_links \\\n  -H \"Authorization: Bearer $ALETA_ACCESS_TOKEN\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"cardholder_id\": \"chd_01HX8ZQ2K4M7P9RSTVWXYZABCD\"\n  }'\n"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "const link = await aleta.esim.h5Links.create(\n  { cardholder_id: 'chd_01HX8ZQ2K4M7P9RSTVWXYZABCD' },\n  { idempotencyKey: randomUUID() },\n);\nres.redirect(link.url);\n"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "link = aleta.esim.h5_links.create(\n    cardholder_id=\"chd_01HX8ZQ2K4M7P9RSTVWXYZABCD\",\n    idempotency_key=str(uuid.uuid4()),\n)\nreturn redirect(link.url)\n"
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "oauth2ClientCredentials": {
        "type": "oauth2",
        "description": "OAuth 2.0 client credentials, authenticated with `private_key_jwt`\n(RFC 7523). There is no client secret anywhere in the flow: you hold a\nprivate key, we hold its public half, and each token request is proved\nby a signature rather than by a shared value.\n\nExchange a signed assertion at the token endpoint for an opaque access\ntoken, then send that token as a bearer credential. Tokens live for\nfifteen minutes and are refused the moment their client is revoked.\n\nThe client id, and therefore the token, determines your organization\nand your environment. Neither is a request parameter.\n\nIt does **not** determine a program. An organization can hold several,\nand which one an account belongs to is decided when the account is\ncreated — see `program_id` on `POST /v1/accounts`.\n\n**A note on this document's shape.** OpenAPI's `clientCredentials`\nflow object has no field for the client authentication method and\nassumes a `client_secret`. This API uses no such secret; the\n`client_assertion` fields on the token request are the credential.\nSee the *Authentication* guide for the assertion's required claims.\n",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "/oauth/token",
            "scopes": {
              "cardholders:read": "Read cardholders and their verification state",
              "cardholders:write": "Create and update cardholders; report KYC outcomes",
              "accounts:read": "Read accounts and balances",
              "accounts:write": "Open accounts; credit and debit them",
              "cards:read": "Read cards, masked",
              "cards:write": "Issue, freeze, replace, and close cards",
              "cards:secrets": "Read card numbers, encrypted to your key, and set PINs; every use is individually audited",
              "transactions:read": "Read authorizations and settled transactions",
              "disputes:read": "Read disputes",
              "disputes:write": "Open and update disputes",
              "esim:read": "Read the eSIM package catalog and allocated profiles",
              "esim:write": "Order eSIM packages and create H5 links",
              "webhooks:write": "Manage webhook endpoints",
              "authentications:read": "Read 3-D Secure authentications",
              "authentications:write": "Approve and decline 3-D Secure authentications",
              "simulate:write": "Drive sandbox simulators (sandbox only)"
            }
          }
        }
      }
    },
    "headers": {
      "AletaRequestId": {
        "description": "Unique identifier for this request. Log it on every response — it is\nrequired in support tickets.\n",
        "schema": {
          "$ref": "#/components/schemas/RequestIdString"
        }
      },
      "IdempotentReplay": {
        "description": "`true` when this response reproduces an earlier one — the same\n`Idempotency-Key`, or the same `client_transaction_id`, seen before.\nAbsent otherwise.\n",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "A client-generated unique key making the request safe to retry. Generate\nit once, before the first attempt, and reuse it for every retry of the\nsame logical operation. Replay window is 24 hours.\n",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 255,
          "examples": [
            "1f3a8c2e-5b7d-4e91-a0c6-2d8f4b6a9e13"
          ]
        }
      },
      "CardholderId": {
        "name": "cardholder_id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/CardholderIdString"
        }
      },
      "AccountId": {
        "name": "account_id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/AccountIdString"
        }
      },
      "CardId": {
        "name": "card_id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/CardIdString"
        }
      },
      "EsimOrderId": {
        "name": "order_id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/EsimOrderIdString"
        }
      },
      "EsimId": {
        "name": "esim_id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/EsimIdString"
        }
      },
      "EsimPackageId": {
        "name": "package_id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/EsimPackageIdString"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Page size. Values above the maximum are rejected, not clamped.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "StartingAfter": {
        "name": "starting_after",
        "in": "query",
        "description": "Opaque cursor for the next page. Valid for 24 hours.",
        "schema": {
          "type": "string",
          "maxLength": 512
        }
      },
      "EndingBefore": {
        "name": "ending_before",
        "in": "query",
        "description": "Opaque cursor for the previous page. Mutually exclusive with `starting_after`.",
        "schema": {
          "type": "string",
          "maxLength": 512
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is malformed or contains an unrecognized parameter.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Credentials are missing, invalid, or expired. Do not retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Credentials are valid but not permitted. Do not retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist, or belongs to another organization. The two\nare deliberately indistinguishable.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Conflict": {
        "description": "Idempotency key reused with a different body, or a concurrent\nmodification.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "MovementAlreadyApplied": {
        "description": "`adjustment_already_applied`: this `client_transaction_id` was already\napplied to the account with a different amount or direction. Or\n`idempotency_request_in_progress`: the movement carrying it is still in\nflight.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "InsufficientFunds": {
        "description": "The account's available balance is too low.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "Well-formed but fails a business rule.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. Honor `Retry-After`.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "schemas": {
      "CardholderIdString": {
        "type": "string",
        "pattern": "^chd_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "chd_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "AccountIdString": {
        "type": "string",
        "pattern": "^acct_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "acct_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "CardIdString": {
        "type": "string",
        "pattern": "^card_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "card_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "AuthenticationIdString": {
        "type": "string",
        "pattern": "^authn_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "authn_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "AuthorizationIdString": {
        "type": "string",
        "pattern": "^auth_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "auth_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "TransactionIdString": {
        "type": "string",
        "pattern": "^txn_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "txn_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "ProgramIdString": {
        "type": "string",
        "pattern": "^prog_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "prog_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "RequestIdString": {
        "type": "string",
        "pattern": "^req_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "req_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "Money": {
        "type": "object",
        "description": "An amount in **integer minor units** with an ISO 4217 currency. Never a\nfloat. The number of minor units varies by currency — JPY has none, BHD\nhas three.\n",
        "required": [
          "amount",
          "currency"
        ],
        "properties": {
          "amount": {
            "type": "integer",
            "format": "int64",
            "description": "Minor units. `1050` with currency `SGD` is S$10.50.",
            "examples": [
              1050
            ]
          },
          "currency": {
            "$ref": "#/components/schemas/CurrencyCode"
          }
        },
        "additionalProperties": false
      },
      "CurrencyCode": {
        "type": "string",
        "description": "Uppercase ISO 4217 alphabetic code.",
        "pattern": "^[A-Z]{3}$",
        "examples": [
          "SGD"
        ]
      },
      "CountryCode": {
        "type": "string",
        "description": "Uppercase ISO 3166-1 alpha-3 code.\n\nOn an address this is checked against the real ISO list rather than the\npattern alone, so a well-formed value that is not a country — `ZZZ`, or\na currency code such as `SGD` — is rejected with `invalid_parameter`.\n",
        "pattern": "^[A-Z]{3}$",
        "examples": [
          "SGP"
        ]
      },
      "Address": {
        "type": "object",
        "required": [
          "line1",
          "city",
          "postal_code",
          "country"
        ],
        "properties": {
          "line1": {
            "type": "string",
            "maxLength": 100
          },
          "line2": {
            "type": "string",
            "maxLength": 100
          },
          "city": {
            "type": "string",
            "maxLength": 60
          },
          "state": {
            "type": "string",
            "maxLength": 60
          },
          "postal_code": {
            "type": "string",
            "maxLength": 20
          },
          "country": {
            "$ref": "#/components/schemas/CountryCode"
          }
        },
        "additionalProperties": false
      },
      "ListEnvelope": {
        "type": "object",
        "required": [
          "object",
          "data",
          "has_more"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "url": {
            "type": "string",
            "examples": [
              "/v1/cards"
            ]
          },
          "data": {
            "type": "array",
            "items": {}
          },
          "has_more": {
            "type": "boolean",
            "description": "Whether further results exist. Loop on this, not on `data.length`."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `starting_after`. Null when `has_more` is false."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorObject"
          }
        },
        "additionalProperties": false
      },
      "ErrorObject": {
        "type": "object",
        "required": [
          "type",
          "code",
          "message",
          "request_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "Broad category. Decide *how* to handle from this.",
            "enum": [
              "invalid_request_error",
              "authentication_error",
              "permission_error",
              "validation_error",
              "rate_limit_error",
              "api_error"
            ]
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable identifier. **Branch on this**, never on\n`message`.\n",
            "examples": [
              "card_invalid_state"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable, for engineers and logs. Not for end users, and the\nwording may change at any time.\n"
          },
          "param": {
            "type": "string",
            "description": "The offending field, where one can be identified."
          },
          "doc_url": {
            "type": "string",
            "format": "uri",
            "description": "Deep link to documentation for this specific error."
          },
          "request_id": {
            "$ref": "#/components/schemas/RequestIdString"
          },
          "errors": {
            "type": "array",
            "description": "Present on `422` when several fields failed at once.",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string"
                },
                "param": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "TokenRequest": {
        "type": "object",
        "required": [
          "grant_type",
          "client_assertion_type",
          "client_assertion"
        ],
        "properties": {
          "grant_type": {
            "type": "string",
            "enum": [
              "client_credentials"
            ]
          },
          "client_assertion_type": {
            "type": "string",
            "const": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
          },
          "client_assertion": {
            "type": "string",
            "description": "The signed JWT. See the operation description for its claims."
          },
          "scope": {
            "type": "string",
            "description": "Space-separated. Omit for every scope the client holds. A scope\nthe client does not hold refuses the whole request.\n",
            "examples": [
              "cards:read cards:write"
            ]
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Opaque. Prefixed `at_test_` in sandbox, `at_live_` in production.",
            "examples": [
              "at_test_3Vq9…"
            ]
          },
          "token_type": {
            "type": "string",
            "const": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds until the token stops working. Always 900.",
            "examples": [
              900
            ]
          },
          "scope": {
            "type": "string",
            "description": "The scopes actually granted, space-separated.",
            "examples": [
              "cards:read cards:write"
            ]
          }
        }
      },
      "OAuthError": {
        "type": "object",
        "description": "RFC 6749 §5.2. Flat, unlike `ErrorObject` — see the token operation for why.\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_client",
              "invalid_grant",
              "invalid_scope",
              "unsupported_grant_type",
              "server_error"
            ]
          },
          "error_description": {
            "type": "string",
            "description": "Human-readable; for logs, not for branching."
          }
        }
      },
      "Program": {
        "type": "object",
        "description": "A card product your organization is attached to.\n\nFour fields, and that is the whole of it. How a program is operated —\nwhich processor backs it, how it is configured, where its balance is\nheld — is ours to run and deliberately not described here: it changes\nwithout your integration changing, which is only true for as long as\nyou cannot read it.\n",
        "required": [
          "id",
          "object",
          "name",
          "environment"
        ],
        "properties": {
          "id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ProgramIdString"
              }
            ],
            "description": "Send this as `program_id` when you open an account. The sandbox and\nproduction rows of the same product have different ids, so keep one\nper environment in your configuration.\n"
          },
          "object": {
            "type": "string",
            "const": "program"
          },
          "name": {
            "type": "string",
            "description": "The product's name, as agreed with your account contact. For\ntelling two of your programs apart in a log or a picker; nothing\nabout a request depends on it, and it can change.\n",
            "examples": [
              "SG Prepaid"
            ]
          },
          "environment": {
            "type": "string",
            "description": "Which environment this program belongs to. Always the environment\nyour access token names — it is here so a configuration file read\nback says which of the two it holds.\n",
            "enum": [
              "sandbox",
              "production"
            ]
          }
        },
        "additionalProperties": false
      },
      "KycStatus": {
        "type": "string",
        "description": "Identity verification state. Only `approved` permits card issuance.\n",
        "enum": [
          "not_started",
          "pending",
          "approved",
          "rejected",
          "review_required"
        ]
      },
      "CardholderType": {
        "type": "string",
        "description": "The kind of cardholder. Only `individual` is supported for now.\n",
        "enum": [
          "individual"
        ]
      },
      "CardholderCreateType": {
        "type": "string",
        "description": "The kind of cardholder to create. Only `individual` is supported for now.\n",
        "enum": [
          "individual"
        ]
      },
      "IdentityDocumentType": {
        "type": "string",
        "description": "The kind of identity document a cardholder was identified by.\n",
        "enum": [
          "passport",
          "drivers_license",
          "national_id",
          "umid",
          "postal_id",
          "voters_id",
          "sss",
          "tin",
          "other"
        ]
      },
      "IdentityDocument": {
        "type": "object",
        "description": "A cardholder's identity document. Only the number and its type are\nheld — no image and no verification result. The number is stored\nencrypted and is never returned; a cardholder's response reports\n`has_identity_document` instead.\n",
        "required": [
          "number"
        ],
        "properties": {
          "type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/IdentityDocumentType"
              }
            ],
            "description": "Optional. When omitted, the program's default document type is\napplied.\n"
          },
          "number": {
            "type": "string",
            "maxLength": 64
          }
        },
        "additionalProperties": false
      },
      "Cardholder": {
        "type": "object",
        "required": [
          "id",
          "object",
          "type",
          "kyc_status",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/CardholderIdString"
          },
          "object": {
            "type": "string",
            "const": "cardholder"
          },
          "type": {
            "$ref": "#/components/schemas/CardholderType"
          },
          "kyc_status": {
            "$ref": "#/components/schemas/KycStatus"
          },
          "first_name": {
            "type": "string",
            "maxLength": 60
          },
          "last_name": {
            "type": "string",
            "maxLength": 60
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "phone": {
            "type": "string",
            "description": "E.164 format.",
            "pattern": "^\\+[1-9]\\d{6,14}$",
            "examples": [
              "+6591234567"
            ]
          },
          "date_of_birth": {
            "type": "string",
            "format": "date",
            "description": "ISO 8601 date. Not a timestamp, so no `_at` suffix."
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "reference": {
            "type": "string",
            "maxLength": 128,
            "description": "Your own identifier. Filterable, and echoed in webhooks."
          },
          "has_identity_document": {
            "type": "boolean",
            "description": "Whether an identity document is on file. A card cannot be issued to\na cardholder without one. The number itself is never returned — this\nboolean is how you tell it is present.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CardholderCreateRequest": {
        "type": "object",
        "required": [
          "type",
          "first_name",
          "last_name"
        ],
        "properties": {
          "type": {
            "$ref": "#/components/schemas/CardholderCreateType"
          },
          "first_name": {
            "type": "string",
            "maxLength": 60
          },
          "last_name": {
            "type": "string",
            "maxLength": 60
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "phone": {
            "type": "string",
            "pattern": "^\\+[1-9]\\d{6,14}$"
          },
          "date_of_birth": {
            "type": "string",
            "format": "date"
          },
          "address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Address"
              }
            ],
            "description": "Optional here, but required before a card can be issued to this\ncardholder — an issuance without one is rejected with\n`address_required`. The card is registered to this address, so it\nis the cardholder's own, not your program's.\n\nCan be set later with `PATCH /v1/cardholders/{cardholder_id}`.\n"
          },
          "reference": {
            "type": "string",
            "maxLength": 128
          },
          "identity_document": {
            "allOf": [
              {
                "$ref": "#/components/schemas/IdentityDocument"
              }
            ],
            "description": "Optional here, but required before a card can be issued to this\ncardholder — an issuance without one is rejected with\n`identity_document_required`. Set it now unless you are creating a\ncardholder you will not yet issue a card to.\n"
          }
        },
        "additionalProperties": false
      },
      "KycAttestationIdString": {
        "type": "string",
        "pattern": "^kyc_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "kyc_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "KycMethod": {
        "type": "string",
        "description": "How the cardholder's identity was established.",
        "enum": [
          "document_and_selfie",
          "document_only",
          "database_check",
          "in_person",
          "other"
        ]
      },
      "KycAttestationRequest": {
        "type": "object",
        "required": [
          "status",
          "verified_at",
          "provider"
        ],
        "properties": {
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/KycStatus"
              }
            ],
            "description": "The outcome you are asserting. `not_started` cannot be reported —\nit is the absence of an attestation.\n"
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "description": "When you completed verification. May be in the past; must not be in\nthe future.\n"
          },
          "provider": {
            "type": "string",
            "maxLength": 120,
            "description": "The identity provider you used, or your own organization's name if\nverification was performed in-house. Required — an attestation with\nno attesting party is not a record.\n"
          },
          "provider_reference": {
            "type": "string",
            "maxLength": 128,
            "description": "The provider's own identifier for this check. Strongly recommended:\nit is what lets an auditor trace our record back to yours.\n"
          },
          "method": {
            "$ref": "#/components/schemas/KycMethod"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this verification lapses and re-verification is due. Omit if\nyour policy has no expiry.\n"
          },
          "notes": {
            "type": "string",
            "maxLength": 500,
            "description": "Free text for context. **Never put identity document contents,\ndocument numbers, or images here** — see the documents guide.\n"
          },
          "reference": {
            "type": "string",
            "maxLength": 128,
            "description": "Your own identifier for this attestation."
          },
          "identity": {
            "allOf": [
              {
                "$ref": "#/components/schemas/KycIdentityRequest"
              }
            ],
            "description": "What the verification established about the person.\n\nOptional in general and **required on programs whose processor runs\nits own identity verification** — that processor will not open an\naccount until it has been shown who the person is. Opening one\nwithout this is refused with `kyc_identity_required` naming\n`cardholder_id`, at account creation rather than here, because the\nattestation itself is a legitimate record either way.\n\nAll five fields are required together when the object is present. A\nhalf-filled identity would pass here and fail upstream on a field\nyou were never told about.\n"
          },
          "documents": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "$ref": "#/components/schemas/KycDocumentRequest"
            },
            "description": "The images supporting the verification, at most one of each `kind`.\n\nSend these **only** on programs whose processor performs its own\nverification and requires them; see `identity` above. Everywhere\nelse there is nothing that reads them and they should not be sent —\nthe guides describe that as the ordinary case, and it still is.\n\nThis is the only request body on the API that carries files, and the\nonly one with a larger size limit. Each document is capped at 20 MiB\ndecoded.\n"
          }
        },
        "additionalProperties": false
      },
      "KycIdentityRequest": {
        "type": "object",
        "description": "Who the verification says this person is. Every field is required.\n",
        "required": [
          "legal_name",
          "gender",
          "nationality",
          "country_of_birth",
          "document_expires_on"
        ],
        "properties": {
          "legal_name": {
            "type": "string",
            "maxLength": 100,
            "description": "The name exactly as it appears on the identity document."
          },
          "gender": {
            "$ref": "#/components/schemas/Gender"
          },
          "nationality": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CountryCode"
              }
            ],
            "description": "ISO 3166-1 alpha-3. Checked against the list, not the shape: a\ncurrency code is shaped identically and is refused.\n"
          },
          "country_of_birth": {
            "$ref": "#/components/schemas/CountryCode"
          },
          "document_expires_on": {
            "type": "string",
            "format": "date",
            "description": "When the identity document expires. A date, not a timestamp.\n"
          }
        },
        "additionalProperties": false
      },
      "KycIdentity": {
        "type": "object",
        "description": "The identity as recorded, echoed back on the attestation.",
        "required": [
          "legal_name",
          "gender",
          "nationality",
          "country_of_birth"
        ],
        "properties": {
          "legal_name": {
            "type": "string",
            "maxLength": 100
          },
          "gender": {
            "$ref": "#/components/schemas/Gender"
          },
          "nationality": {
            "$ref": "#/components/schemas/CountryCode"
          },
          "country_of_birth": {
            "$ref": "#/components/schemas/CountryCode"
          },
          "document_expires_on": {
            "type": "string",
            "format": "date"
          }
        }
      },
      "Gender": {
        "type": "string",
        "description": "As recorded by the verification. `unspecified` is a valid answer and not\na missing one.\n",
        "enum": [
          "male",
          "female",
          "unspecified"
        ]
      },
      "KycDocumentKind": {
        "type": "string",
        "description": "What an image shows. One of each at most per attestation.\n\n`identity` is the document itself — passport, national ID, licence.\n`residence` is a proof of address. `selfie` is the liveness capture the\ndocument is matched against.\n",
        "enum": [
          "identity",
          "residence",
          "selfie"
        ]
      },
      "KycDocumentRequest": {
        "type": "object",
        "required": [
          "kind",
          "file_name",
          "content_type",
          "content"
        ],
        "properties": {
          "kind": {
            "$ref": "#/components/schemas/KycDocumentKind"
          },
          "file_name": {
            "type": "string",
            "maxLength": 200,
            "description": "The original file name. Stored as given, never used as a path."
          },
          "content_type": {
            "type": "string",
            "pattern": "^image/(jpeg|png)$",
            "examples": [
              "image/jpeg",
              "image/png"
            ],
            "description": "`image/jpeg` or `image/png`. An allowlist of two, and anything else\nis refused here rather than by a processor later, in an error that\nwould name nothing.\n"
          },
          "content": {
            "type": "string",
            "format": "byte",
            "description": "The file, base64 encoded, up to 20 MiB decoded.\n\nBase64 rather than multipart because every other body on this API is\nJSON, and the processors that want these want base64 too.\n\n**Write-only.** No endpoint returns it. See `KycDocument` for what\nyou can read back.\n"
          }
        },
        "additionalProperties": false
      },
      "KycDocument": {
        "type": "object",
        "description": "A stored document, as metadata only.\n\nThere is deliberately no URL and no content field. An operation that\nreads a document back would turn any compromised access token into a\nbulk export of identity documents, and a presigned URL would do it\nwithout needing the token twice.\n",
        "required": [
          "id",
          "object",
          "kind",
          "file_name",
          "content_type",
          "byte_size",
          "is_purged",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "kyc_document"
          },
          "kind": {
            "$ref": "#/components/schemas/KycDocumentKind"
          },
          "file_name": {
            "type": "string",
            "maxLength": 200
          },
          "content_type": {
            "type": "string",
            "pattern": "^image/(jpeg|png)$"
          },
          "byte_size": {
            "type": "integer",
            "format": "int64",
            "description": "The decoded size. The one fact about a document that can be\npublished safely, and what lets you confirm that what arrived is\nwhat you sent.\n"
          },
          "is_purged": {
            "type": "boolean",
            "description": "Whether the bytes are gone while the record of them remains. False\non every document today — they are retained — and published anyway,\nso that the day it turns true is not the day clients learn the field\nexists.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "KycAttestation": {
        "type": "object",
        "required": [
          "id",
          "object",
          "cardholder_id",
          "status",
          "verified_at",
          "provider",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/KycAttestationIdString"
          },
          "object": {
            "type": "string",
            "const": "kyc_attestation"
          },
          "cardholder_id": {
            "$ref": "#/components/schemas/CardholderIdString"
          },
          "status": {
            "$ref": "#/components/schemas/KycStatus"
          },
          "verified_at": {
            "type": "string",
            "format": "date-time"
          },
          "provider": {
            "type": "string",
            "maxLength": 120
          },
          "provider_reference": {
            "type": "string",
            "maxLength": 128
          },
          "method": {
            "$ref": "#/components/schemas/KycMethod"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "notes": {
            "type": "string",
            "maxLength": 500
          },
          "reference": {
            "type": "string",
            "maxLength": 128
          },
          "identity": {
            "$ref": "#/components/schemas/KycIdentity"
          },
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/KycDocument"
            },
            "description": "Absent rather than empty on an attestation that carried none, so\n\"none supplied\" and \"an empty list was stored\" stay distinguishable.\n"
          },
          "is_current": {
            "type": "boolean",
            "description": "Whether this is the attestation currently in force. Exactly one\nattestation per cardholder is current.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When we recorded it — distinct from `verified_at`, which is when you\nperformed the check.\n"
          }
        }
      },
      "CardholderUpdateRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "phone": {
            "type": "string",
            "pattern": "^\\+[1-9]\\d{6,14}$"
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "reference": {
            "type": "string",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "AccountStatus": {
        "type": "string",
        "description": "Account state. `blocked` is reversible and suspends all activity on the\naccount and every card it funds.\n\nThere is deliberately no `closed` state. Account closure is not\ncurrently exposed, and documenting a state nothing can produce is worse\nthan documenting a smaller surface — it can be added later without\nbreaking clients.\n",
        "enum": [
          "active",
          "blocked"
        ]
      },
      "Account": {
        "type": "object",
        "required": [
          "id",
          "object",
          "cardholder_id",
          "program_id",
          "currency",
          "status",
          "ledger_balance",
          "held_balance",
          "available_balance",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/AccountIdString"
          },
          "object": {
            "type": "string",
            "const": "account"
          },
          "cardholder_id": {
            "$ref": "#/components/schemas/CardholderIdString"
          },
          "program_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ProgramIdString"
              }
            ],
            "description": "The program this account was opened under. It is fixed at creation\nand there is no call that moves an account to another program.\n\nEverything the account owns belongs to the same program: the cards\ndrawn on it, and the authorizations and transactions those cards\nproduce. That is why this is the only object you have to name a\nprogram on.\n\n`GET /v1/programs` returns every id your organization can use in\nthis environment. They are also shown in the cockpit under\nSettings > Programs, for reading by hand.\n"
          },
          "currency": {
            "$ref": "#/components/schemas/CurrencyCode"
          },
          "status": {
            "$ref": "#/components/schemas/AccountStatus"
          },
          "ledger_balance": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Money that has settled. Reconcile against this."
          },
          "held_balance": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Reserved by approved authorizations that have not cleared."
          },
          "available_balance": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "`ledger_balance − held_balance`. **The spendable figure — show this\nto end users.**\n"
          },
          "reference": {
            "type": "string",
            "maxLength": 128
          },
          "has_upstream_reference": {
            "type": "boolean",
            "description": "Whether the account exists at the processor, and therefore whether\nit can hold a card.\n\nA boolean rather than the identifier itself: the value is the\nprocessor's, and this API never exposes one.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AccountCreateRequest": {
        "type": "object",
        "required": [
          "program_id",
          "cardholder_id",
          "currency"
        ],
        "properties": {
          "program_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ProgramIdString"
              }
            ],
            "description": "Which of your programs to open this account under. This is the one\nplace a program is named — cards, PINs, spend and reporting all\nreach it through the account.\n\nRequired even if you hold a single program. Choosing for you would\nbe right for exactly as long as that stays true, and would start\nopening accounts under the wrong product, silently, on the day a\nsecond one is attached.\n\n`GET /v1/programs` returns the ids you may send. The value differs\nbetween sandbox and production, so keep one per environment rather\nthan hard-coding a single id.\n"
          },
          "cardholder_id": {
            "$ref": "#/components/schemas/CardholderIdString"
          },
          "currency": {
            "$ref": "#/components/schemas/CurrencyCode"
          },
          "reference": {
            "type": "string",
            "maxLength": 128
          },
          "upstream_reference": {
            "type": "string",
            "maxLength": 64,
            "description": "Adopts an account that already exists at the processor.\n\nAlmost nobody needs this. Leave it out and the account is opened for\nyou. Supply it only when migrating an account that exists already,\nor on a program whose processor has no account-opening operation at\nall — in which case the number is provisioned out of band and this\nis the only way to carry it.\n\nNever returned. `has_upstream_reference` reports whether one is on\nfile without disclosing the processor's own identifier.\n"
          }
        },
        "additionalProperties": false
      },
      "AccountBlockRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 255,
            "description": "Free text recorded on the audit entry. Never shown to the\ncardholder.\n"
          }
        },
        "additionalProperties": false
      },
      "AccountAdjustmentRequest": {
        "type": "object",
        "required": [
          "amount"
        ],
        "properties": {
          "amount": {
            "$ref": "#/components/schemas/Money"
          },
          "type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TransactionType"
              }
            ],
            "default": "adjustment",
            "description": "What this movement was for. It becomes the `type` of the\ntransaction the call records, which is how the movement appears in\n`GET /v1/transactions` afterwards.\n\nOnly you know the intent, which is why it is a parameter rather\nthan something we infer. `credit` accepts `load`, `transfer` and\n`adjustment`; `debit` accepts `fee`, `transfer` and `adjustment`.\nA refund is neither: it names the transaction it refunds, so it is\nrecorded against that transaction and not as a free-standing\nmovement.\n\nDefaults to `adjustment` — the value that claims nothing. Send\n`load` for funding rather than relying on the default, or every\ncorrection will read as a top-up.\n"
          },
          "description": {
            "type": "string",
            "maxLength": 255
          },
          "reference": {
            "type": "string",
            "maxLength": 128,
            "description": "Your label for the movement, for reconciliation. Free text with no\nuniqueness constraint — forty movements may share one.\n"
          },
          "client_transaction_id": {
            "type": "string",
            "maxLength": 128,
            "description": "Your identifier for this movement. Unique per account, across\ncredits and debits, and it never expires. Sending the same id with\nthe same amount again returns the account unchanged with\n`Idempotent-Replay: true`; sending it with a different amount is\nrefused with `adjustment_already_applied`. Use the id of the record\nin your own system that this movement settles.\n"
          }
        },
        "additionalProperties": false
      },
      "CardFormFactor": {
        "type": "string",
        "enum": [
          "virtual",
          "physical"
        ]
      },
      "CardStatus": {
        "type": "string",
        "description": "Card lifecycle state.\n\nVirtual and physical cards share one set of statuses and one path:\n`pending` while the processor issues the card, `inactive` once it has\nthe card (`card.issued`), and `active` when you call `POST\n/v1/cards/{card_id}/activate` (`card.activated`). A card that is not\n`active` declines every authorization.\n\nNote that `frozen` sits in the same field as the lifecycle states, so\nunfreezing always returns a card to `active` rather than to whatever it\nwas before. See the card lifecycle guide.\n",
        "enum": [
          "pending",
          "inactive",
          "active",
          "frozen",
          "expired",
          "closed"
        ]
      },
      "CardFulfillmentStatus": {
        "type": "string",
        "description": "How far the plastic has got. Separate from `status`, which is about\nwhether the card can be used: a `physical` card can be `active` and\nspending online while its plastic is still being made.\n\n`not_applicable` is a card with no plastic to make. `pending` is plastic\nordered. `delivered` is the cardholder confirming it arrived, which is\nalso when the card is enabled for use in person.\n\nNot every status is reachable on every program. Manufacturing and\ndelivery happen outside the API, and a processor that reports nothing\nbetween `pending` and `delivered` never produces `personalized`,\n`shipped` or `returned`. See\n[fulfillment and shipping tracking](https://developer.aletaplanet.com/docs/products/card-issuing/cards/fulfillment-and-shipping-tracking).\n",
        "enum": [
          "not_applicable",
          "pending",
          "personalized",
          "shipped",
          "delivered",
          "returned"
        ]
      },
      "CardFulfillment": {
        "type": "object",
        "description": "Where the plastic is. Absent on a card that has never had any ordered.\n",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "$ref": "#/components/schemas/CardFulfillmentStatus"
          },
          "carrier": {
            "type": "string",
            "description": "Who is carrying it, when the processor says."
          },
          "tracking_number": {
            "type": "string",
            "description": "The carrier's own reference, when the processor reports one. Not\nevery program has tracking.\n"
          },
          "shipped_at": {
            "type": "string",
            "format": "date-time"
          },
          "delivered_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "Card": {
        "type": "object",
        "required": [
          "id",
          "object",
          "cardholder_id",
          "account_id",
          "program_id",
          "form_factor",
          "status",
          "currency",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/CardIdString"
          },
          "object": {
            "type": "string",
            "const": "card"
          },
          "cardholder_id": {
            "$ref": "#/components/schemas/CardholderIdString"
          },
          "account_id": {
            "$ref": "#/components/schemas/AccountIdString"
          },
          "program_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ProgramIdString"
              }
            ],
            "description": "Inherited from the account the card was issued against; a card is\nnever issued into a program directly. It is also the reason a card\ncan only be re-linked to an account in the same program.\n"
          },
          "form_factor": {
            "$ref": "#/components/schemas/CardFormFactor"
          },
          "status": {
            "$ref": "#/components/schemas/CardStatus"
          },
          "currency": {
            "$ref": "#/components/schemas/CurrencyCode"
          },
          "last_four": {
            "type": "string",
            "pattern": "^\\d{4}$",
            "description": "The only part of the card number returned here. The full number and\nCVV require a separate, audited operation.\n",
            "examples": [
              "4242"
            ]
          },
          "embossed_name": {
            "type": "string",
            "maxLength": 26,
            "description": "The name printed on a physical card."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "is_default": {
            "type": "boolean",
            "description": "Whether this is the cardholder's default card."
          },
          "fulfillment": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CardFulfillment"
              }
            ],
            "description": "Where the plastic is, on a card that has had any ordered. Absent\notherwise, so a virtual card that was never upgraded does not carry\nit at all.\n"
          },
          "reference": {
            "type": "string",
            "maxLength": 128
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CardCreateRequest": {
        "type": "object",
        "required": [
          "cardholder_id",
          "account_id",
          "form_factor"
        ],
        "properties": {
          "cardholder_id": {
            "$ref": "#/components/schemas/CardholderIdString"
          },
          "account_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AccountIdString"
              }
            ],
            "description": "The account the card spends from. The card takes this account's\ncurrency; there is no `currency` field to send.\n"
          },
          "form_factor": {
            "$ref": "#/components/schemas/CardFormFactor"
          },
          "embossed_name": {
            "type": "string",
            "maxLength": 26,
            "description": "The name printed on the card. Required for a physical card."
          },
          "delivery_address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Address"
              }
            ],
            "description": "Where a physical card is posted, and required for one. A virtual card\nis billed to it. Omit it on a virtual card and the cardholder's own\n`address` is used instead. The card is always **registered** to the\ncardholder's address regardless, so this never replaces it.\n"
          },
          "reference": {
            "type": "string",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "CardReplaceRequest": {
        "type": "object",
        "required": [
          "reason"
        ],
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "lost",
              "stolen",
              "damaged",
              "expired",
              "compromised",
              "other"
            ]
          }
        },
        "additionalProperties": false
      },
      "CardReasonRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 255,
            "description": "Free text passed to the processor with the status change. Never\nshown to the cardholder.\n"
          }
        },
        "additionalProperties": false
      },
      "CardSecretsRequest": {
        "type": "object",
        "required": [
          "public_key"
        ],
        "properties": {
          "public_key": {
            "type": "string",
            "maxLength": 8192,
            "description": "The RSA public key the card's secrets are encrypted to, in PEM form: a\n`PUBLIC KEY`, an `RSA PUBLIC KEY`, or a `CERTIFICATE` that carries one.\nAt least 2048 bits.\n"
          }
        },
        "additionalProperties": false
      },
      "CardSecrets": {
        "type": "object",
        "description": "A card's number and security code, each encrypted to the key in the\nrequest. Neither is readable without the matching private key.\n",
        "required": [
          "object",
          "card_id",
          "encrypted_pan",
          "encrypted_cvv",
          "algorithm",
          "created_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "card_secrets"
          },
          "card_id": {
            "$ref": "#/components/schemas/CardIdString"
          },
          "encrypted_pan": {
            "type": "string",
            "description": "The card number, encrypted to your key and encoded as base64."
          },
          "encrypted_cvv": {
            "type": "string",
            "description": "The security code, encrypted to your key and encoded as base64."
          },
          "algorithm": {
            "type": "string",
            "const": "RSA-OAEP-SHA256",
            "description": "How both values are encrypted. RSA-OAEP with SHA-256 and no label."
          },
          "last_four": {
            "type": "string",
            "pattern": "^\\d{4}$",
            "description": "The last four digits of the number, in the clear."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CardPinKey": {
        "type": "object",
        "description": "A short-lived key to encrypt a PIN to. See\n`POST /v1/cards/{card_id}/pin-key`.\n",
        "required": [
          "object",
          "card_id",
          "key_id",
          "pin_length",
          "public_key",
          "algorithm",
          "expires_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "card_pin_key"
          },
          "card_id": {
            "$ref": "#/components/schemas/CardIdString"
          },
          "key_id": {
            "type": "string",
            "description": "Send this back with the encrypted PIN, so we know which key to use."
          },
          "pin_length": {
            "type": "integer",
            "description": "How many digits the PIN has. Set for the program, and the only rule we apply."
          },
          "public_key": {
            "type": "string",
            "description": "The RSA public key, in PEM form, to encrypt the PIN to."
          },
          "algorithm": {
            "type": "string",
            "const": "RSA-OAEP-SHA256",
            "description": "How to encrypt the PIN. RSA-OAEP with SHA-256 and no label."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the key stops being accepted. A few minutes after it is issued."
          }
        }
      },
      "CardPinRequest": {
        "type": "object",
        "description": "The PIN to set, encrypted to a key from\n`POST /v1/cards/{card_id}/pin-key`.\n",
        "required": [
          "key_id",
          "encrypted_pin"
        ],
        "properties": {
          "key_id": {
            "type": "string",
            "description": "The `key_id` of the key the PIN is encrypted to."
          },
          "encrypted_pin": {
            "type": "string",
            "maxLength": 1024,
            "description": "The PIN as digits, encrypted with RSA-OAEP using SHA-256 to the key's\n`public_key`, encoded as base64.\n"
          }
        },
        "additionalProperties": false
      },
      "AuthenticationStatus": {
        "type": "string",
        "enum": [
          "pending",
          "approved",
          "declined",
          "timed_out"
        ],
        "description": "| Status | Meaning |\n| --- | --- |\n| `pending` | Waiting for an answer. The merchant's checkout is open |\n| `approved` | Answered yes. The authorization follows |\n| `declined` | Answered no, by you or by the cardholder |\n| `timed_out` | Nobody answered before `expires_at`. The purchase failed at the merchant |\n"
      },
      "AuthenticationMethod": {
        "type": "string",
        "enum": [
          "out_of_band"
        ],
        "description": "How the cardholder was asked. Only out-of-band challenges become\nobjects here; a passcode challenge is run end to end by the Access\nControl Server and is never visible through this API.\n"
      },
      "Authentication": {
        "type": "object",
        "required": [
          "id",
          "object",
          "status",
          "method",
          "card_id",
          "cardholder_id",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/AuthenticationIdString"
          },
          "object": {
            "type": "string",
            "const": "authentication"
          },
          "status": {
            "$ref": "#/components/schemas/AuthenticationStatus"
          },
          "method": {
            "$ref": "#/components/schemas/AuthenticationMethod"
          },
          "card_id": {
            "$ref": "#/components/schemas/CardIdString"
          },
          "cardholder_id": {
            "$ref": "#/components/schemas/CardholderIdString"
          },
          "merchant": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Merchant"
              }
            ],
            "description": "Who the cardholder is buying from, as the Access Control Server\ndescribed it. Show it in the prompt; it is what tells an honest\napproval from a fraudulent one.\n"
          },
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "What the cardholder is approving. Show it exactly, neither\nparaphrased nor rounded.\n"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the challenge stops being answerable. Minutes, not hours, and\nthe cardholder has to find their phone inside it.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "Merchant": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "mcc": {
            "type": "string",
            "pattern": "^\\d{4}$",
            "description": "ISO 18245 merchant category code.",
            "examples": [
              "5814"
            ]
          },
          "country": {
            "$ref": "#/components/schemas/CountryCode"
          },
          "city": {
            "type": "string",
            "maxLength": 60
          }
        },
        "additionalProperties": false
      },
      "TransactionType": {
        "type": "string",
        "enum": [
          "purchase",
          "withdrawal",
          "transfer",
          "load",
          "fee",
          "refund",
          "adjustment"
        ],
        "description": "What the transaction was for. It does **not** say which way the money\nmoved — read `direction` for that, because every type occurs in both\ndirections once reversals are counted.\n\n| Value | Meaning |\n| --- | --- |\n| `purchase` | A card purchase at a merchant |\n| `withdrawal` | Cash taken at an ATM or over a counter |\n| `transfer` | Value moved between accounts |\n| `load` | Value added to the account outside a card transaction — funding |\n| `fee` | A charge levied on the account, including taxes |\n| `refund` | A merchant returned value against an earlier purchase |\n| `adjustment` | A correction, and the value used when nothing more specific applies |\n\nTreat this as an open enum. New values are added as products grow, and\na client that switches on it should have a default branch rather than\nfail on one it does not recognise.\n"
      },
      "TransactionDirection": {
        "type": "string",
        "enum": [
          "credit",
          "debit"
        ],
        "description": "Which way the value moved, from the account's point of view. `credit`\nincreases `ledger_balance`, `debit` decreases it.\n\nCarried explicitly rather than inferred from `type`, because the\ninference does not hold: a purchase and its cancellation share a type\nand move opposite ways. `amount` is always positive and this field\nsays which way it points.\n"
      },
      "Transaction": {
        "type": "object",
        "required": [
          "id",
          "object",
          "account_id",
          "type",
          "direction",
          "amount",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/TransactionIdString"
          },
          "object": {
            "type": "string",
            "const": "transaction"
          },
          "account_id": {
            "$ref": "#/components/schemas/AccountIdString"
          },
          "card_id": {
            "$ref": "#/components/schemas/CardIdString"
          },
          "authorization_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AuthorizationIdString"
              }
            ],
            "description": "Absent where no authorization preceded the transaction. That covers\nforce posts — a purchase that cleared with no authorization at all,\nidentified by this field being absent — as well as merchant-\noriginated refunds and anything credited or debited through the\naccount endpoints.\n\nDo not assume a one-to-one relationship with authorizations, and\njoin on this field with a left join rather than an inner one.\n"
          },
          "type": {
            "$ref": "#/components/schemas/TransactionType"
          },
          "direction": {
            "$ref": "#/components/schemas/TransactionDirection"
          },
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "The settled amount, always positive. `direction` says which way it\nmoved. May differ from the authorized amount — a restaurant tip is\nthe everyday case.\n"
          },
          "merchant": {
            "$ref": "#/components/schemas/Merchant"
          },
          "settled_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EsimPackageIdString": {
        "type": "string",
        "pattern": "^epkg_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "epkg_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "EsimCountryCode": {
        "type": "string",
        "description": "Uppercase ISO 3166-1 **alpha-2** code — `SG`, `JP`.\n\nMobile coverage is addressed in alpha-2 throughout this API. Cards\nare not: an address, a spend control and a card's issuing country\nall take alpha-3 (`SGP`). Convert between them rather than passing\none where the other belongs.\n",
        "pattern": "^[A-Z]{2}$",
        "examples": [
          "JP"
        ]
      },
      "EsimCountry": {
        "type": "object",
        "description": "A country an eSIM package works in.",
        "required": [
          "code",
          "name"
        ],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/EsimCountryCode"
          },
          "name": {
            "type": "string",
            "description": "English name, for display only. Match on `code` — the name is\neditorial and can be reworded.\n",
            "examples": [
              "Japan"
            ]
          }
        },
        "additionalProperties": false
      },
      "EsimIdString": {
        "type": "string",
        "pattern": "^esim_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "esim_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "EsimInstalledVolume": {
        "type": "object",
        "description": "The package's allowance and what is left of it, both in\n**megabytes**.\n\n`value` and `type` are the package's own, unchanged from the catalog.\n`remaining` is measured against whichever of the two `type` names: on\na `total` package it is what is left for the rest of the package's\nlife, on a `daily` one it is what is left of **today** and it resets.\n",
        "required": [
          "value",
          "type"
        ],
        "properties": {
          "value": {
            "type": "integer",
            "description": "The allowance, in megabytes.",
            "examples": [
              5120
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "total",
              "daily"
            ]
          },
          "remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Megabytes left, in the terms `type` sets, **rounded down**. A\npackage with a few kilobytes to go reads `0` rather than `1` —\nbetter to under-report data than to offer some that is not there.\n\n**Present while the package is running.** A package that has not\nstarted has spent nothing and one that has ended has nothing\nleft; neither has a live figure to report, and neither carries\nthis field. `activated_at` and `expires_at` say which case you\nare looking at.\n\nRead from the network, and it settles with a delay of minutes,\nso it lags real usage.\n",
            "examples": [
              3072
            ]
          }
        },
        "additionalProperties": false
      },
      "EsimInstalledPackage": {
        "type": "object",
        "description": "A package provisioned onto an eSIM.\n",
        "required": [
          "id",
          "name",
          "volume"
        ],
        "properties": {
          "id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimPackageIdString"
              }
            ],
            "description": "Which package this is. Matches the catalog, so the same id can\nappear twice here if it was bought twice.\n"
          },
          "name": {
            "type": "string",
            "description": "The package's display name, as it was at purchase.",
            "examples": [
              "Asia 11 Countries 5GB 30 Days"
            ]
          },
          "volume": {
            "$ref": "#/components/schemas/EsimInstalledVolume"
          },
          "activated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the package started. **Absent until it does** — a package\nsits dormant from purchase until the eSIM first reaches a\nnetwork, and the clock starts there rather than at checkout.\n"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the package stops working. Absent while `activated_at` is,\nfor the same reason: the validity period is counted from\nactivation, so an unstarted package has no end date yet.\n"
          }
        },
        "additionalProperties": false
      },
      "EsimStatus": {
        "type": "string",
        "description": "`active` — usable.\n\n`frozen` — suspended, and reversible. Packages on it keep their\nremaining validity while it is frozen.\n\n`closed` — deregistered for good. A closed eSIM cannot be revived and\ncannot take another package.\n",
        "enum": [
          "active",
          "frozen",
          "closed"
        ]
      },
      "EsimSummary": {
        "type": "object",
        "description": "An eSIM as a list row: enough to identify it and reconcile it against\nyour own records, and nothing that costs a secret to hand over.\n\nRetrieve one to get its `activation_code` and its `packages`.\n",
        "required": [
          "id",
          "object",
          "status",
          "iccid",
          "order_reference",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/EsimIdString"
          },
          "object": {
            "type": "string",
            "const": "esim"
          },
          "status": {
            "$ref": "#/components/schemas/EsimStatus"
          },
          "order_reference": {
            "type": "string",
            "description": "Your own order id — the `reference` you sent on the order that\nallocated this eSIM. It is how a list of ours reconciles against\na list of yours without you storing our ids.\n\nA top-up does not change it. It stays the reference of the order\nthat first allocated the profile.\n",
            "examples": [
              "order-4471"
            ]
          },
          "iccid": {
            "type": "string",
            "pattern": "^[0-9]{19,20}$",
            "description": "The profile's ICCID. For support and for matching a carrier's\nrecords; not what a traveller installs.\n",
            "examples": [
              "8910390000012345678"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "Esim": {
        "type": "object",
        "description": "One eSIM profile, its install code, and the packages loaded onto it.\n\nEverything in `EsimSummary`, plus the two fields a list will not hand\nout: the code that installs the profile, and the packages on it.\n\nDeclared in full rather than composed with `allOf` over `EsimSummary`.\nComposing over a base that sets `additionalProperties: false` is\nunsatisfiable: every `allOf` branch is validated against the whole\ninstance, so the base would reject the very fields this schema\nrequires. The two are kept in step by review instead, which is the\nsame trade the card-issuing resources make.\n",
        "required": [
          "id",
          "object",
          "status",
          "iccid",
          "order_reference",
          "created_at",
          "activation_code",
          "packages"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/EsimIdString"
          },
          "object": {
            "type": "string",
            "const": "esim"
          },
          "status": {
            "$ref": "#/components/schemas/EsimStatus"
          },
          "order_reference": {
            "type": "string",
            "description": "Your own order id — the `reference` you sent on the order that\nallocated this eSIM. It is how a list of ours reconciles against\na list of yours without you storing our ids.\n\nA top-up does not change it. It stays the reference of the order\nthat first allocated the profile.\n",
            "examples": [
              "order-4471"
            ]
          },
          "iccid": {
            "type": "string",
            "pattern": "^[0-9]{19,20}$",
            "description": "The profile's ICCID. For support and for matching a carrier's\nrecords; not what a traveller installs.\n",
            "examples": [
              "8910390000012345678"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "activation_code": {
            "type": "string",
            "description": "The LPA string — what a traveller installs, and what to\nencode as a QR. Returned here as well as on the order, so you\ncan re-send install instructions without storing the secret\nyourself.\n\n**Treat it as a secret.** Anyone holding it can install the\nprofile, and it installs only once. It is handed out one eSIM\nat a time on purpose; no list returns it.\n",
            "examples": [
              "LPA:1$smdp.aletaplanet.com$K2-1A9F0C-2B77D4"
            ]
          },
          "packages": {
            "type": "array",
            "description": "Every package ever provisioned onto this eSIM, newest first.\nExpired ones stay.\n",
            "items": {
              "$ref": "#/components/schemas/EsimInstalledPackage"
            }
          }
        },
        "additionalProperties": false
      },
      "EsimTopUpIdString": {
        "type": "string",
        "pattern": "^etop_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "etop_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "EsimPackageTopUps": {
        "type": "object",
        "description": "One running package on an eSIM, and the add-ons sold against it.\n",
        "required": [
          "package_id",
          "name",
          "top_ups"
        ],
        "properties": {
          "package_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimPackageIdString"
              }
            ],
            "description": "The package these add-ons extend. Matches an entry in the eSIM's\nown `packages`, so a client that has already retrieved the eSIM\ncan line the two up without another lookup.\n"
          },
          "name": {
            "type": "string",
            "description": "The package's display name, carried here so a picker does not need a second call to label the group.",
            "examples": [
              "Asia 11 Countries 5GB 30 Days"
            ]
          },
          "top_ups": {
            "type": "array",
            "minItems": 1,
            "description": "Add-ons available on this package, smallest first. Never empty —\na package with nothing to sell is left out of the response\ninstead.\n",
            "items": {
              "$ref": "#/components/schemas/EsimTopUp"
            }
          }
        },
        "additionalProperties": false
      },
      "EsimTopUp": {
        "type": "object",
        "description": "Extra data for a package already running on an eSIM.\n\nA top-up has no validity period of its own. It adds to the package it\nbelongs to and expires with it, so one bought late in that package's\nlife is worth less time than the same one bought early.\n",
        "required": [
          "id",
          "object",
          "name",
          "volume",
          "price"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/EsimTopUpIdString"
          },
          "object": {
            "type": "string",
            "const": "esim_top_up"
          },
          "name": {
            "type": "string",
            "description": "Display name, as shown to an end user. Always English.",
            "examples": [
              "1GB Add-on"
            ]
          },
          "volume": {
            "type": "integer",
            "minimum": 1,
            "description": "Megabytes this add-on adds. A plain amount, not the package's\n`{value, type}` — a top-up tops up whatever scheme its package\nalready runs on.\n",
            "examples": [
              1024
            ]
          },
          "price": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "What this add-on costs you."
          }
        },
        "additionalProperties": false
      },
      "EsimTopUpCreateRequest": {
        "type": "object",
        "required": [
          "top_up_id",
          "reference"
        ],
        "properties": {
          "top_up_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimTopUpIdString"
              }
            ],
            "description": "The add-on to order, from this eSIM's own top-up list."
          },
          "reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Your order id for this purchase. Required, and unique within your\norganization across every eSIM purchase — a top-up shares one\nledger with package orders, so a `reference` spent on either is\nspent.\n",
            "examples": [
              "order-4472"
            ]
          }
        },
        "additionalProperties": false
      },
      "EsimTopUpOrder": {
        "type": "object",
        "description": "One purchase of one add-on, against a package already running on an\neSIM.\n\nIt shares the order ledger with package orders and carries the same\n`eord_` id, so\n[list orders](https://developer.aletaplanet.com/api-reference/resources/list-esim-orders)\nfinds it by `reference` the same way. `object` is how you tell the\ntwo apart.\n\n**`package_id` and `price` are absent while an order is still being\nsettled** — the same signal an absent `esim_id` carries on a package\norder, and the mirror image of it. A package order knows its package\nand its price before we place it, and learns which eSIM it got on\ncompletion. A top-up is the other way round: you name the eSIM and\nthe add-on, while which package the add-on landed on — and what it\nactually cost — are known only from the provider's answer.\n\n`esim_id` and `top_up_id` are therefore present either way: you named\nboth when you placed the order.\n",
        "required": [
          "id",
          "object",
          "esim_id",
          "top_up_id",
          "reference",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/EsimOrderIdString"
          },
          "object": {
            "type": "string",
            "const": "esim_top_up_order"
          },
          "esim_id": {
            "$ref": "#/components/schemas/EsimIdString"
          },
          "package_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimPackageIdString"
              }
            ],
            "description": "The package this top-up extended. Absent until the order\nsettles: you did not name it, and only the provider's answer\nsays which package the add-on landed on.\n"
          },
          "top_up_id": {
            "$ref": "#/components/schemas/EsimTopUpIdString"
          },
          "reference": {
            "type": "string",
            "description": "Your order id, as sent on the create request.",
            "examples": [
              "order-4472"
            ]
          },
          "price": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "What was charged. Absent until the order settles — unlike a\npackage order, whose price is known from the catalog before we\nplace it, a top-up is charged what the provider reports.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "EsimOrderIdString": {
        "type": "string",
        "pattern": "^eord_[0-9A-HJKMNP-TV-Z]{26}$",
        "examples": [
          "eord_01HX8ZQ2K4M7P9RSTVWXYZABCD"
        ]
      },
      "EsimOrderCreateRequest": {
        "type": "object",
        "required": [
          "package_id",
          "reference"
        ],
        "properties": {
          "package_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimPackageIdString"
              }
            ],
            "description": "The package to buy. Must be `available`."
          },
          "reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Your order id for this purchase. Required, and **unique within\nyour organization** — a second order carrying a `reference` you\nhave already used is refused with `esim_order_reference_exists`.\n\nThis is stricter than `reference` elsewhere in the API, where it\nis a free-form label that need not be unique. Here it names one\npurchase, so one purchase is all it may name.\n\nThe uniqueness is a backstop, not a retry mechanism. A request\nthat times out has an unknown outcome, and the only safe way to\nrepeat it is the same `Idempotency-Key`.\n",
            "examples": [
              "order-4471"
            ]
          }
        },
        "additionalProperties": false
      },
      "EsimOrderSummary": {
        "type": "object",
        "description": "One purchase of one package, provisioned onto one eSIM — as a list\nrow, without the code that installs the profile.\n\nAn order that returns a `201` is done. There is no queue behind it and\nno status to poll, and a purchase that could not be provisioned returns\nan error instead of an order.\n\nOne case does not fit that, and this schema is shaped around it.\nWhen the network that provisions the profile times out, we cannot tell\nwhether it took the order. Such an order carries **no `esim_id` and no\n`iccid`** — that absence *is* the signal, the same way an absent\n`activated_at` means an eSIM has not started. It is not charged and not\nfailed; it is unsettled, and it settles on its own, usually within\nminutes. Both fields appear once it does.\n\nSo: an order with an `esim_id` went through. An order without one is\nstill being resolved — ask again shortly rather than ordering a second\ntime.\n",
        "required": [
          "id",
          "object",
          "package_id",
          "reference",
          "price",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/EsimOrderIdString"
          },
          "object": {
            "type": "string",
            "const": "esim_order"
          },
          "package_id": {
            "$ref": "#/components/schemas/EsimPackageIdString"
          },
          "esim_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimIdString"
              }
            ],
            "description": "The eSIM this order was provisioned onto, allocated from our\ninventory when the order was placed. You do not choose it.\n"
          },
          "iccid": {
            "type": "string",
            "pattern": "^[0-9]{19,20}$",
            "description": "The allocated eSIM's ICCID. Useful for support and for matching\nagainst a carrier's own records; not what a traveller installs.\n",
            "examples": [
              "8910390000012345678"
            ]
          },
          "reference": {
            "type": "string",
            "description": "Your order id, as sent on the create request.",
            "examples": [
              "order-4471"
            ]
          },
          "price": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "What was charged, captured when the order was placed. A later\nchange to the package's catalog price does not move it.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "EsimOrder": {
        "type": "object",
        "description": "A purchase, with the code that installs the eSIM it bought.\n\nEverything in `EsimOrderSummary`, plus the `activation_code` a list\nwill not hand out.\n\n`activation_code` is absent for exactly as long as `esim_id` is: an\norder still being settled has no profile yet, so there is no code to\ninstall. The three appear together, and a `201` from\n[create](https://developer.aletaplanet.com/api-reference/resources/create-esim-order)\nalways carries all three.\n\nDeclared in full rather than composed with `allOf` over\n`EsimOrderSummary`, for the reason given on `Esim`.\n",
        "required": [
          "id",
          "object",
          "package_id",
          "reference",
          "price",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/EsimOrderIdString"
          },
          "object": {
            "type": "string",
            "const": "esim_order"
          },
          "package_id": {
            "$ref": "#/components/schemas/EsimPackageIdString"
          },
          "esim_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimIdString"
              }
            ],
            "description": "The eSIM this order was provisioned onto, allocated from our\ninventory when the order was placed. You do not choose it.\n"
          },
          "iccid": {
            "type": "string",
            "pattern": "^[0-9]{19,20}$",
            "description": "The allocated eSIM's ICCID. Useful for support and for matching\nagainst a carrier's own records; not what a traveller installs.\n",
            "examples": [
              "8910390000012345678"
            ]
          },
          "reference": {
            "type": "string",
            "description": "Your order id, as sent on the create request.",
            "examples": [
              "order-4471"
            ]
          },
          "price": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "What was charged, captured when the order was placed. A later\nchange to the package's catalog price does not move it.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "activation_code": {
            "type": "string",
            "description": "What the traveller installs — the LPA string, and the exact\ncontent to encode in a QR code. Render it as one, print it,\nor deep-link it; an iOS or Android handset reads either form.\n\n**Treat it as a secret.** Anyone holding this string can\ninstall the profile, and it can only be installed once.\nDeliver it over a channel you trust and do not log it beside\nthe order id.\n",
            "examples": [
              "LPA:1$smdp.aletaplanet.com$K2-1A9F0C-2B77D4"
            ]
          }
        },
        "additionalProperties": false
      },
      "EsimPackageStatus": {
        "type": "string",
        "description": "Whether the package is on sale.\n\n`available` — orderable now.\n\n`frozen` — we have suspended it, and the suspension is reversible. It\nis not orderable while frozen, but everything already sold against it\nkeeps working.\n\n`disabled` — withdrawn for good. Kept readable so an order that\nreferences it still resolves, never orderable again.\n",
        "enum": [
          "available",
          "frozen",
          "disabled"
        ]
      },
      "EsimPackageVolume": {
        "type": "object",
        "description": "Included data, and whether it is the whole package's allowance or a\nper-day one.\n\nBoth halves are needed to read either. A 30-day package with\n`value: 1073741824` is 1 GB when `type` is `total` and 30 GB when it\nis `daily` — the number alone says nothing.\n",
        "required": [
          "value",
          "type"
        ],
        "properties": {
          "value": {
            "type": "integer",
            "minimum": 1,
            "description": "Data volume in **megabytes**. `5120` is 5 GB. Always MB, whatever\nunit the plan is marketed in, so two packages compare without a\nconversion first.\n",
            "examples": [
              5120
            ]
          },
          "type": {
            "type": "string",
            "description": "`total` — `value` is the allowance for the whole validity period,\nspent at whatever rate the user likes.\n\n`daily` — `value` is the allowance per day, and it resets. What\nhappens once a day's allowance runs out is the plan's own\nbusiness: some throttle, some cut off until the next day.\n",
            "enum": [
              "total",
              "daily"
            ]
          }
        },
        "additionalProperties": false
      },
      "EsimPackageValidity": {
        "type": "object",
        "description": "How long the package stays usable. The clock starts when the profile\nfirst attaches to a network, not when the package is ordered.\n",
        "required": [
          "duration",
          "unit"
        ],
        "properties": {
          "duration": {
            "type": "integer",
            "minimum": 1,
            "examples": [
              30
            ]
          },
          "unit": {
            "type": "string",
            "description": "Only `day` is sold, so `duration` reads as a number of days.\n\nIt is an enum rather than a bare `validity_days` integer so a\nfurther unit can be added without a breaking change.\n",
            "enum": [
              "day"
            ]
          }
        },
        "additionalProperties": false
      },
      "EsimPackage": {
        "type": "object",
        "description": "A data plan: a volume, a validity period, and the coverage it is sold\nagainst. `status` says whether it can be ordered right now.\n",
        "required": [
          "id",
          "object",
          "status",
          "name",
          "volume",
          "validity",
          "countries",
          "price",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimPackageIdString"
              }
            ],
            "description": "Send this as `package_id` when ordering. Sandbox and production\nrows of the same plan have different ids.\n"
          },
          "object": {
            "type": "string",
            "const": "esim_package"
          },
          "status": {
            "$ref": "#/components/schemas/EsimPackageStatus"
          },
          "name": {
            "type": "string",
            "description": "Display name, as shown to an end user. **Always English** —\ntranslate it yourself if you serve another language.\n\nIt repeats the volume and duration in prose; the structured\nfields are what to branch on.\n",
            "examples": [
              "Asia 11 Countries 5GB 30 Days"
            ]
          },
          "volume": {
            "$ref": "#/components/schemas/EsimPackageVolume"
          },
          "validity": {
            "$ref": "#/components/schemas/EsimPackageValidity"
          },
          "countries": {
            "type": "array",
            "minItems": 1,
            "description": "Every country the package works in. Never empty.",
            "items": {
              "$ref": "#/components/schemas/EsimCountry"
            }
          },
          "price": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "What this package costs you."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the package last changed — a price, a coverage list, a\nstatus. This is how you sync a cached catalog without diffing\nevery row: keep the newest `updated_at` you have seen and\ncompare against it.\n"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the package stops being sold. Absent when it has no end date\n— most do not.\n\nAn order placed after this point is refused. Re-read the package\nat checkout rather than trusting a cached list.\n"
          }
        },
        "additionalProperties": false
      },
      "EsimH5LinkCreateRequest": {
        "type": "object",
        "required": [
          "cardholder_id"
        ],
        "properties": {
          "cardholder_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CardholderIdString"
              }
            ],
            "description": "The cardholder the page is for. Must be in your organization."
          }
        },
        "additionalProperties": false
      },
      "EsimH5Link": {
        "type": "object",
        "description": "A link to the hosted eSIM page for one cardholder, valid for 15\nminutes from `created_at`.\n",
        "required": [
          "object",
          "url",
          "cardholder_id",
          "expires_at",
          "created_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "esim_h5_link"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The page to send the cardholder to. Opaque: don't parse it or\nbuild one yourself. It opens any number of times until\n`expires_at`.\n\nTreat it as a credential — keep it out of logs, analytics, and\nstorage.\n",
            "examples": [
              "https://h5.aletaplanet.com/esim/2Qm9Xv7Lk4Tz8Rb1"
            ]
          },
          "cardholder_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CardholderIdString"
              }
            ],
            "description": "The cardholder the link is for, as sent on the request."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the link stops working. Always `created_at` plus 15\nminutes.\n",
            "examples": [
              "2026-10-15T09:15:00Z"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "examples": [
              "2026-10-15T09:00:00Z"
            ]
          }
        },
        "additionalProperties": false
      }
    }
  }
}