ChoEsim

ChoEsim Partner API: guide

Contents: Authentication | Idempotency | Errors | Rate limits | Pagination | Versioning | Wallet | Order lifecycle | Cancellations | Webhooks | Data protection | Security

The OpenAPI file is the reference for every field. This guide explains how the pieces work together.

Basics

Authentication

Send Authorization: Bearer <api key> on every request.

Key types. cesim_live_... keys work on live data and your prepaid wallet. cesim_test_... keys work on the sandbox only.

Scopes. A key has one or more scopes. A call without the needed scope gets 403 forbidden_scope.

ScopeAllows
catalog:readGET /plans, GET /plans/{id}, POST /quotes
orders:readGET /orders, GET /orders/{id}, GET /esims/{id}, GET /esims/{id}/usage, GET /events
orders:writePOST /orders, POST /orders/{id}/cancel, POST /sandbox/wallet/topup (test keys only)
wallet:readGET /wallet, GET /wallet/entries
webhooks:managewebhook endpoints, POST /events/{id}/resend

GET /me needs any valid key.

Key handling.

Idempotency

An idempotency key lets you repeat a request safely. This matters when a network call times out and you do not know if it worked.

Where it is required. POST /orders, POST /orders/{order_id}/cancel and POST /sandbox/wallet/topup need an Idempotency-Key header. A missing key gives 400 idempotency_key_required.

Format. 1 to 64 characters from A-Za-z0-9_:.-. A UUID is recommended.

Rules.

SituationResult
Same key, same bodyThe current state of the same order, with Idempotent-Replayed: true. The status code follows that state (see below).
Same key, different body, after the first request was accepted422 idempotency_key_reuse.
Same key while the first request is still running409 idempotency_in_progress. Retry shortly. A running cancel is not taken over by a second request with the same key.
Key older than 7 daysKeys are kept for 7 days. After that the key is forgotten.

Status code of a replay. The code reflects the current state of the order, not the first answer:

Order state at replay timePOST /orders replayPOST /orders/{id}/cancel replay
fulfilled201n/a
accepted or processing202n/a
failed or cancelled200200 (when cancelled)
cancellingn/a202

So a first answer of 202 becomes 201 on a later replay once the eSIMs are ready.

A refusal before any money is taken does not use up the key. If an order is refused before the wallet is debited (for example 409 price_changed, a validation error, or a 503), nothing is stored for that key. You may send a corrected request with the same key, or with a new key. Both work.

Your keys are scoped to you. They are separate from other partners' keys, and separate between test and live mode.

The reference

Every order carries a reference that you choose. It is unique forever per mode. A second order with the same reference gets 409 duplicate_reference, and the error body includes the existing order_id.

Use both: an Idempotency-Key for safe retries, and a reference that matches your own order id. The reference protects you after the 7 days are over.

Timeouts are not failures

POST /orders waits up to about 15 seconds. If the order is not complete, you get 202 and status: processing.

If a call times out on your side, or returns 202:

  1. Do not create a second order.
  2. Poll GET /orders/{id}, or repeat the same request with the same Idempotency-Key.

If you lost the order id, use GET /orders?reference=....

Errors

Errors use RFC 9457 application/problem+json. Every error body has these members:

MemberMeaning
typeA URN like urn:choesim:problem:price_changed.
titleA short title.
statusThe HTTP status.
detailA human-readable sentence. Optional.
instanceThe request path. Optional.
codeA stable machine-readable code. Use this in your code.
request_idMatches the Request-Id header.

Some errors add members: retry_after, balance_minor, required_minor, expected_total_minor, current_total_minor, order_id, limit and errors[] (each with a JSON pointer into your body and a detail). Ignore members you do not know.

Match on code, not on title or detail. The text can change.

{
  "type": "urn:choesim:problem:price_changed",
  "title": "Price changed",
  "status": 409,
  "detail": "The total is now different from expected_total_minor. Nothing was debited.",
  "instance": "/partner/v1/orders",
  "code": "price_changed",
  "request_id": "req_7c1e2f94",
  "expected_total_minor": 2520,
  "current_total_minor": 2600
}

Error codes

HTTPcodeCauseWhat your client should do
400invalid_requestMalformed JSON or a bad query parameter.Fix the request. Do not retry unchanged.
400idempotency_key_requiredIdempotency-Key is missing.Add the header.
401invalid_api_keyThe key is missing, unknown, revoked or expired.Check the key. Do not retry in a loop.
402insufficient_balanceThe wallet is lower than the total. Nothing was debited. The body has balance_minor and required_minor.Top up, then send again.
403forbidden_scopeThe key lacks the scope, or a test-only endpoint was called with a live key.Ask ChoEsim for the right scope, or use the right key.
403ip_not_allowedThe caller IP is not on the key's allow-list.Call from an allowed server, or ask ChoEsim to update the list.
403partner_suspendedYour partner account is suspended.Contact partner support.
403wallet_frozenYour wallet is frozen.Contact partner support.
404not_foundNo such resource, or it belongs to another partner. The two cases look the same.Check the id and the mode (test or live).
409idempotency_in_progressA request with this key is still running.Wait, then repeat the same request.
409duplicate_referenceAn order with this reference already exists. The body has its order_id.Read that order. Do not create another.
409price_changedThe total differs from expected_total_minor. Nothing was debited. The body has expected_total_minor and current_total_minor.Decide whether to accept the new total, then send again (same key or new key).
409esim_not_cancellableAn eSIM cannot be cancelled (for example it is used, or the supplier refuses).Do not retry.
409cancel_window_expiredThe order is older than cancel_window_days. Nothing was changed.Do not retry. Contact partner support if you think this is wrong.
413payload_too_largeThe body is larger than 10 KB.Send a smaller body.
415unsupported_media_typeContent-Type is not application/json.Fix the header.
422invalid_requestThe body fails validation: unknown field, out-of-range value, or customer_reference looks like personal data. Check errors[].Fix the request.
422idempotency_key_reuseThe key was used with a different body.Use a new key for a new request.
422plan_unavailableThe plan cannot be sold to you now. Nothing was debited.Refresh your plan list.
422order_limit_exceededA safety limit was hit. The limit member says which one: max_order, daily_spend or open_orders. Nothing was debited.Reduce the order, or wait. Check GET /me.
422cancel_limit_exceededThe daily cancellation cap is reached.Try again after 00:00 UTC.
429rate_limitedToo many requests. Retry-After says how long to wait.Wait that many seconds, then retry.
500internal_errorAn unexpected error on the ChoEsim side. Only the request_id is exposed. Nothing is lost silently.For POST /orders, check GET /orders?reference=... before you retry. Repeating the same request with the same Idempotency-Key is always safe. Quote request_id to support.
503orders_disabledOrdering is paused (maintenance or a kill switch). Nothing was debited.Retry after Retry-After.
503supplier_unavailableOrders are paused for a short time because of a supplier problem. Nothing was debited.Retry after Retry-After.
503supplier_capacityChoEsim cannot take the order now. Nothing was debited.Retry after Retry-After. If it repeats, contact support.

After a 4xx or 503, an order is never left half-created.

Never treat 5xx, a timeout or a 202 as proof that no order exists. Check by reference first.

A 429 response looks like this:

{
  "type": "urn:choesim:problem:rate_limited",
  "title": "Too many requests",
  "status": 429,
  "detail": "Request rate exceeded for this key.",
  "instance": "/partner/v1/orders",
  "code": "rate_limited",
  "request_id": "req_7c1e2f9c",
  "retry_after": 12
}

A 500 response looks like this:

{
  "type": "urn:choesim:problem:internal_error",
  "title": "Internal error",
  "status": 500,
  "instance": "/partner/v1/orders",
  "code": "internal_error",
  "request_id": "req_7c1e2fa0"
}

Rate limits

Limits apply per key and per request class. Each response carries:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window.
X-RateLimit-RemainingRequests left in the window.
X-RateLimit-ResetSeconds until the window resets.

A 429 always has Retry-After (seconds).

Default targets. These are default targets and may change. The live limit is always in the response headers.

Request classDefault
Read60 requests per minute per key
Quote30 requests per minute per key
Create order10 requests per minute per key, burst of 3
Cancel5 requests per minute per key
Cancellations per day5 per UTC day per partner (daily_cancel_cap in GET /me)

Other safety limits are returned by GET /me: max_order_minor, daily_spend_cap_minor, max_open_orders, daily_cancel_cap, cancel_window_days, max_items_per_order (30) and max_quantity_per_line (10). The first three are set for each partner by ChoEsim. They are checked in the database.

How to behave:

Pagination

List endpoints use cursors: GET /plans, GET /orders, GET /wallet/entries, GET /events.

Order of results: orders and wallet entries come newest first. Events come oldest first, so you can resume from next_cursor.

GET /orders supports the filters status, created_from, created_to and reference. In a list, esims is left out. Fetch one order to get activation data.

Versioning and deprecation

The major version is in the path: /partner/v1.

Not breaking (can happen in v1):

Your client must ignore fields and enum values it does not know.

Breaking (needs v2): removing or renaming a field, changing the meaning of a field, or making validation stricter.

Deprecation. When a version or endpoint is deprecated, responses carry Deprecation and Sunset headers, and the changelog explains the change. ChoEsim gives at least six months notice before it removes or changes the behaviour of an endpoint in a way that breaks existing integrations. For urgent security, legal or abuse reasons the notice can be shorter, and ChoEsim tells you as soon as it can.

Webhook payloads carry api_version: "v1".

Wallet and ledger

You pay from a prepaid wallet in USD. A live wallet and a test wallet are separate.

Top-ups. Live wallets are topped up in the partner portal: you enter the amount and pay by card through OnePay. The card is charged in Vietnamese dong at the system exchange rate with no margin, and ChoEsim bears the gateway fees. Where a bank transfer has been agreed, the portal shows the bank details and the exact transfer reference, and bank fees on your side are yours. There is no top-up API for live wallets. The minimum is USD 50 for the first top-up and USD 25 for each later top-up. The wallet balance cannot exceed three times your daily spend limit.

Ledger. GET /wallet/entries is an append-only list of every change, newest first. Each entry has:

FieldMeaning
seqRunning number of the entry.
kindtopup, debit_order, credit_refund, adjustment or reversal.
amount_minorSigned. Debits are negative.
balance_after_minorBalance after this entry.
order_idThe order, if there is one.
refA reference, for example a bank transfer reference.

Every entry carries the running balance, so you can rebuild and check a statement.

{
  "id": "wle_0f1e2d3c",
  "object": "wallet_entry",
  "seq": 3,
  "kind": "debit_order",
  "amount_minor": -2520,
  "currency": "USD",
  "balance_after_minor": 7480,
  "order_id": "po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10",
  "ref": null,
  "created_at": "2026-11-01T09:30:00Z"
}

Reconcile every day: the sum of amount_minor should equal the balance.

A wallet.low_balance event is sent when the wallet falls below a threshold agreed with ChoEsim. A wallet.credited event is sent when a top-up or refund is credited.

The balance is an advance payment for ChoEsim eSIMs. It earns no interest, cannot be withdrawn as cash or transferred, and an unused balance is refunded only to the bank account that paid it, after bank fees and amounts you owe. The details are in the partner terms.

Order lifecycle

StatusMeaning
acceptedThe wallet is debited. Delivery has not started.
processingIn progress. See status_detail.
fulfilledThe eSIMs are ready.
failedThe order definitely failed. The wallet is credited.
cancellingA cancellation is under way.
cancelledCancelled, and the wallet is credited.

status_detail can be:

POST /orders
   |-- 201 fulfilled
   |-- 202 processing --> fulfilled
   |                  --> processing / under_review --> fulfilled | failed
   |-- 4xx / 503 (nothing debited)

fulfilled --> cancelling --> cancelled

Things to know:

GET /esims/{esim_id}/usage returns the last known data usage. It is built from notifications ChoEsim has received. It can be late or empty. A null field means unknown. It does not mean zero.

Cancellations

POST /orders/{order_id}/cancel cancels a whole order of unused eSIMs and credits your wallet. It needs an Idempotency-Key and the orders:write scope.

Rules:

Responses: 200 when cancelled and credited (or a replay of an earlier success). 202 if the cancellation is not finished within the wait budget. Then poll the order, or wait for order.cancelled.

Refund rules beyond this (for example technical faults) are in the partner terms.

Webhooks

Webhooks are an optimisation. GET /orders/{id} is the source of truth. Reconcile with GET /events and GET /orders.

Endpoints

An endpoint has a status of active or disabled, plus disabled_reason and last_success_at.

ChoEsim can disable an endpoint that keeps failing. The rule is 5 abandoned events in a row, or no success for 4 days, or a 410 answer from your endpoint. ChoEsim then writes to your contact address. Ask partner support to re-enable it.

Events

EventWhendata members
order.fulfilledEvery eSIM of the order is ready. Fetch the order for activation data.order_id, reference, status
order.failedThe order definitely failed and the wallet was credited. Never sent for an unknown outcome.order_id, reference, status, failure_code
order.cancelledCancellation finished and the wallet was credited.order_id, reference, status
order.delayedThe order has been processing longer than expected. Informational, no action needed.order_id, reference, status
esim.status_changedThe supplier-reported status of an eSIM changed.esim_id, order_id, esim_status
wallet.creditedA top-up or refund was credited.balance_minor, currency, entry_id (optional)
wallet.low_balanceThe wallet fell below the threshold agreed with ChoEsim.balance_minor, currency, entry_id (optional)
pingAn endpoint was created or re-enabled.endpoint_id (optional)

Every event has this envelope. The id equals the webhook-id header.

Payloads are thin. They contain ids and status, never activation data.

Examples of the data member for each event:

{ "id": "evt_1f9c2a7e", "object": "event", "type": "order.fulfilled", "api_version": "v1", "livemode": false, "created": "2026-11-01T09:30:02Z",
  "data": { "order_id": "po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10", "reference": "partner-order-10042", "status": "fulfilled" } }
{ "id": "evt_2a8d3b6f", "object": "event", "type": "order.failed", "api_version": "v1", "livemode": false, "created": "2026-11-01T09:31:02Z",
  "data": { "order_id": "po_test_5c1d8e72-1b2a-4c3d-9e8f-6a7b5c4d3e2f", "reference": "partner-order-10044", "status": "failed", "failure_code": "price_changed" } }
{ "id": "evt_4c0f5d9b", "object": "event", "type": "order.cancelled", "api_version": "v1", "livemode": false, "created": "2026-11-02T10:00:00Z",
  "data": { "order_id": "po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10", "reference": "partner-order-10042", "status": "cancelled" } }
{ "id": "evt_5d1a6eac", "object": "event", "type": "order.delayed", "api_version": "v1", "livemode": false, "created": "2026-11-01T09:41:00Z",
  "data": { "order_id": "po_test_9d2b7a44-0c1d-4e5f-8a12-7b6c5d4e3f21", "reference": "partner-order-10043", "status": "processing" } }
{ "id": "evt_3b9e4c8a", "object": "event", "type": "esim.status_changed", "api_version": "v1", "livemode": true, "created": "2026-11-02T08:00:00Z",
  "data": { "esim_id": "esm_example", "order_id": "po_example", "esim_status": "IN_USE" } }
{ "id": "evt_6e2b7fbd", "object": "event", "type": "wallet.credited", "api_version": "v1", "livemode": false, "created": "2026-11-01T09:00:00Z",
  "data": { "balance_minor": 10000, "currency": "USD", "entry_id": "wle_0f1e2d3b" } }
{ "id": "evt_7f3c80ce", "object": "event", "type": "wallet.low_balance", "api_version": "v1", "livemode": false, "created": "2026-11-01T09:35:00Z",
  "data": { "balance_minor": 900, "currency": "USD" } }
{ "id": "evt_80d491df", "object": "event", "type": "ping", "api_version": "v1", "livemode": false, "created": "2026-11-01T09:00:00Z",
  "data": { "endpoint_id": "whe_8f2a1c9d" } }

Headers

HeaderMeaning
webhook-idThe event id. Identical on every retry. Use it to de-duplicate.
webhook-timestampUnix time in seconds of this attempt. It changes on each retry.
webhook-signatureOne or two space-separated values like v1,<base64>. Two appear during secret rotation.

Verify the signature

The signing method follows the Standard Webhooks style.

  1. The endpoint secret is whsec_ followed by standard base64 of the key bytes. Strip whsec_ and base64-decode the rest. The decoded bytes are the HMAC key. Do not use the base64 text as the key.
  2. Build the signed string: {webhook-id}.{webhook-timestamp}.{raw body}.
  3. Signature = base64(HMAC-SHA256(key, signed string)), sent as v1,<signature>.
  4. Use the raw request body, byte for byte. Do not parse and re-serialise it.
  5. Reject a timestamp more than 5 minutes old.
  6. Compare in constant time.
  7. Accept the request if any of the signatures in the header matches.

Test vector. Use it to check your code. It is a documentation key, never a real secret.

ItemValue
Secretwhsec_Y2hvZXNpbS1kb2NzLWV4YW1wbGUta2V5LTMyYnl0ZXMh
webhook-idevt_1f9c2a7e
webhook-timestamp1793525402
Bodythe exact text below, on one line
Expected webhook-signaturev1,yOGM2hgcarOxufvwHvPAvpRmv8QfypZQYDforcG5N50=
{"id":"evt_1f9c2a7e","object":"event","type":"order.fulfilled","api_version":"v1","livemode":false,"created":"2026-11-01T09:30:02Z","data":{"order_id":"po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10","reference":"partner-order-10042","status":"fulfilled"}}

The timestamp in the vector is old on purpose. The sample functions take an optional "now" value so that you can test with it.

Node.js (version 18 or later), file verify-webhook.mjs:

import crypto from "node:crypto";

const TOLERANCE_SECONDS = 300; // 5 minutes

// rawBody: Buffer or string, exactly as received.
// headers: object with lower-case header names.
// nowSeconds: only set it in tests.
export function verifyWebhook(rawBody, headers, secret, nowSeconds = Date.now() / 1000) {
  if (!secret) throw new Error("webhook secret is not configured");

  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatureHeader = headers["webhook-signature"];
  if (!id || !timestamp || !signatureHeader) return false;

  const ts = Number(timestamp);
  if (!Number.isInteger(ts)) return false;
  if (Math.abs(nowSeconds - ts) > TOLERANCE_SECONDS) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest();

  for (const part of signatureHeader.split(" ")) {
    const [version, signature] = part.split(",");
    if (version !== "v1" || !signature) continue;
    const given = Buffer.from(signature, "base64");
    if (given.length === expected.length && crypto.timingSafeEqual(given, expected)) {
      return true;
    }
  }
  return false;
}

Test with the vector, file verify-webhook.test.mjs. Run node verify-webhook.test.mjs. It prints ok when it passes.

import assert from "node:assert/strict";
import { verifyWebhook } from "./verify-webhook.mjs";

const secret = "whsec_Y2hvZXNpbS1kb2NzLWV4YW1wbGUta2V5LTMyYnl0ZXMh";
const body =
  '{"id":"evt_1f9c2a7e","object":"event","type":"order.fulfilled","api_version":"v1","livemode":false,"created":"2026-11-01T09:30:02Z","data":{"order_id":"po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10","reference":"partner-order-10042","status":"fulfilled"}}';
const headers = {
  "webhook-id": "evt_1f9c2a7e",
  "webhook-timestamp": "1793525402",
  "webhook-signature": "v1,yOGM2hgcarOxufvwHvPAvpRmv8QfypZQYDforcG5N50=",
};
const now = 1793525402;

assert.equal(verifyWebhook(Buffer.from(body), headers, secret, now), true);
assert.equal(verifyWebhook(Buffer.from(body + " "), headers, secret, now), false); // changed body
assert.equal(verifyWebhook(Buffer.from(body), headers, secret, now + 400), false); // too old
console.log("ok");

An Express handler that uses it:

import express from "express";
import { verifyWebhook } from "./verify-webhook.mjs";

const SECRET = process.env.CHOESIM_WEBHOOK_SECRET; // whsec_REPLACE_ME
const app = express();

// express.raw keeps req.body as the untouched Buffer.
app.post("/hooks/choesim", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyWebhook(req.body, req.headers, SECRET)) {
    return res.status(400).send("invalid signature");
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // Store event.id, reply, then process (see "Write idempotent handlers").
  res.status(200).send("ok");
  queueForProcessing(event); // your own function
});

app.listen(3000);

Python (version 3.9 or later), file verify_webhook.py:

import base64
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300  # 5 minutes


def verify_webhook(raw_body: bytes, headers, secret: str, now=None) -> bool:
    """headers: a mapping with lower-case names (Flask's request.headers also works).
    now: seconds since the epoch; only set it in tests."""
    if not secret:
        raise ValueError("webhook secret is not configured")

    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signature_header = headers.get("webhook-signature")
    if not msg_id or not timestamp or not signature_header:
        return False

    try:
        ts = int(timestamp)
    except ValueError:
        return False
    current = time.time() if now is None else now
    if abs(current - ts) > TOLERANCE_SECONDS:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
    expected = hmac.new(key, signed, hashlib.sha256).digest()

    for part in signature_header.split(" "):
        version, _, signature = part.partition(",")
        if version != "v1" or not signature:
            continue
        try:
            given = base64.b64decode(signature)
        except ValueError:
            continue
        if hmac.compare_digest(given, expected):
            return True
    return False

Test with the vector, file test_verify_webhook.py. Run python3 test_verify_webhook.py. It prints ok when it passes.

from verify_webhook import verify_webhook

SECRET = "whsec_Y2hvZXNpbS1kb2NzLWV4YW1wbGUta2V5LTMyYnl0ZXMh"
BODY = (
    '{"id":"evt_1f9c2a7e","object":"event","type":"order.fulfilled","api_version":"v1",'
    '"livemode":false,"created":"2026-11-01T09:30:02Z","data":{"order_id":'
    '"po_test_3b8e4c1a-6a53-4f0b-9c33-0f3c2f6e1a10","reference":"partner-order-10042",'
    '"status":"fulfilled"}}'
).encode("utf-8")
HEADERS = {
    "webhook-id": "evt_1f9c2a7e",
    "webhook-timestamp": "1793525402",
    "webhook-signature": "v1,yOGM2hgcarOxufvwHvPAvpRmv8QfypZQYDforcG5N50=",
}
NOW = 1793525402

assert verify_webhook(BODY, HEADERS, SECRET, now=NOW) is True
assert verify_webhook(BODY + b" ", HEADERS, SECRET, now=NOW) is False  # changed body
assert verify_webhook(BODY, HEADERS, SECRET, now=NOW + 400) is False  # too old
print("ok")

A Flask handler that uses it:

import os

from flask import Flask, request

from verify_webhook import verify_webhook

SECRET = os.environ.get("CHOESIM_WEBHOOK_SECRET")  # whsec_REPLACE_ME
app = Flask(__name__)


@app.post("/hooks/choesim")
def choesim_hook():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    if not verify_webhook(raw, request.headers, SECRET):
        return "invalid signature", 400
    # Store the event id, reply, then process after the reply.
    return "ok", 200

Retries, order and replays

Write idempotent handlers

  1. Verify the signature first. Reject with 400 if it fails.
  2. Look up webhook-id in your own store. If it is already there, reply 200 and stop.
  3. Save the webhook-id, then reply 200.
  4. Process after the reply. Use a queue.
  5. Do not trust the payload alone. Call GET /orders/{id} and act on what it says. A late or repeated event must not undo a newer state.
  6. Make the processing step safe to run twice. For example, deliver an eSIM to your customer only if you have not already done so for that order_id.

Data protection

Only what the API specification says:

Do not put personal data of your customers into customer_reference or any other field. ChoEsim deletes activation codes (LPA) 90 days after the installation window ends and keeps order records and customer_reference for 12 months. Roles and the rest are in the partner terms.

Security recommendations