Демо-режим: референсная витрина для eSIM-операторов. Цены живые, оплата отключена. Запустить своего оператора →
Travel eSIM · demo
Русский

Справочник SDK

Содержание

Всё, что нужно действующему eSIM-оператору, чтобы поставить свои системы на рельсы AIMarket: пакеты, интерфейс поставщика, HTTP-поставщик для систем на любом языке, узел продавца, вебхуки, контракт esim.*@v1 и проверка соответствия.

Другие документы: обзор · онбординг · интеграции · гайд оператора · гайд для агентов

1. Пакеты

ПакетЧто этоУстановка
@aimarket/esim-railsSDK: узел продавца, поставщики, вебхуки, схемы, esim-rails checknpm i @aimarket/esim-rails после публикации; до тех пор npm i ./rails из клона этого репозитория
aimarket-hub (PyPI)aimarket node: узел федерации, который вносит вас в каталоги хабовpip install aimarket-hub==3.15.26 или образ node/ из кита
JSON-схемыКонтракт для любого языкаrails/spec/esim-v1/*.schema.json
Docker-образывитрина, узел продавца, узел федерациисобираются docker compose up --build

Node.js 20 или новее. SDK — ESM.

import {
  createNode, ed25519Signer, buildPaywall, fileStore,          // the seller node
  httpSupplier, esimAccessSupplier, airaloSupplier, esimGoSupplier, createEsimAccessClient,   // suppliers
  createWebhooks, verifyWebhook,                                // your systems
  validate, checkSeller,                                        // the contract
} from "@aimarket/esim-rails";

2. Интерфейс поставщика

Поставщик — это откуда берутся eSIM. Витрина и узел продавца общаются с ним только через пять методов.

МетодВозвращаетПравила
catalog()Plan[]тарифы, которые вы продаёте, по вашей себестоимости
order({ transactionId, planId, costUsd }){ orderRef }идемпотентно по transactionId; отказ, если costUsd уже не ваша себестоимость
profiles({ orderRef }) или profiles({ profileId })Profile[][], пока профиль создаётся
cancel({ profileId })trueтолько неиспользованные профили
balance(){ usd } или nullnull, когда следить за балансом не нужно
Plan     { id, name, countries: ["FR", …], kind?: "country" | "region" | "global", data_bytes, days,
           speed?, networks?, top_up?, unused_valid_days?, cost_usd }
Profile  { profileId, orderRef, activationCode, iccid?, qrCodeUrl?, installUrl?, apn?, expires?,
           state?, installed?, dataUsedBytes?, dataTotalBytes?, activated? }

Бросайте SupplierError(code, message, { retryable }): retryable: true значит «не сейчас» (сбой, лимит запросов); иначе — «нет» (неизвестный тариф, изменившаяся цена, пустой баланс). «Нет» никогда не списывается с покупателя.

Поставщик в вашем собственном коде:

import { SupplierError } from "@aimarket/esim-rails";

export const mySupplier = {
  async catalog() {
    return (await db.plans.active()).map((p) => ({
      id: p.sku, name: p.title, countries: p.countries, kind: p.region ? "region" : "country",
      data_bytes: p.gb * 1024 ** 3, days: p.days, cost_usd: p.cost }));
  },
  async order({ transactionId, planId, costUsd }) {
    const plan = await db.plans.get(planId);
    if (!plan || plan.cost !== costUsd) throw new SupplierError("price_changed", "the plan changed");
    const o = await db.orders.upsertByTransaction(transactionId, planId);   // the same id, the same order
    return { orderRef: o.id };
  },
  async profiles({ orderRef, profileId }) {
    const rows = orderRef ? await smdp.profilesOfOrder(orderRef) : [await smdp.profile(profileId)].filter(Boolean);
    return rows.map((r) => ({ profileId: r.id, orderRef: r.order, activationCode: r.lpa, iccid: r.iccid, state: r.state }));
  },
  async cancel({ profileId }) { await smdp.release(profileId); return true; },
  async balance() { return null; },                                       // your own stock: nothing to watch
};

Встроенные: esimAccessSupplier(createEsimAccessClient({ accessCode, secretKey })), airaloSupplier({ clientId, clientSecret }), esimGoSupplier({ apiKey }) и httpSupplier({ url, secret }) — настройка и оговорки в интеграциях.

3. HTTP-поставщик: ваша система, любой язык

Запустите кит с SUPPLIER=http, SUPPLIER_URL и SUPPLIER_SECRET_FILE и реализуйте пять эндпоинтов в своей системе. Полный контракт — в rails/spec/supplier-http.md.

Метод и путьТелоОтвет
GET /catalog—{"plans": [Plan]}
POST /orders{"transaction_id", "plan_id", "cost_usd"}{"order_ref"}
GET /profiles?order_ref=… или ?profile_id=…—{"profiles": [Profile]} (snake_case)
POST /profiles/{profile_id}/cancel{}{"cancelled": true}
GET /balance—{"usd": 250.0} или 404

Каждый запрос несёт X-Kit-Timestamp и X-Kit-Signature: v1=<hex HMAC-SHA256(secret, timestamp + "." + METHOD + " " + path + "." + body)>. Отклоняйте неверную подпись и всё, что старше пяти минут.

sequenceDiagram
  participant K as Кит
  participant Y as Ваша система
  K->>Y: GET /catalog (с подписью)
  Y-->>K: тарифы с cost_usd
  K->>Y: POST /orders {transaction_id, plan_id, cost_usd}
  Y-->>K: {order_ref}   (тот же transaction_id → тот же order_ref)
  loop пока не готов
    K->>Y: GET /profiles?order_ref=…
    Y-->>K: [] … затем [профиль с activation_code]
  end
  K->>Y: POST /profiles/{id}/cancel   (только если платёж не удалось провести)

Минимальный приёмник на Python (Flask), два самых важных эндпоинта:

import hashlib, hmac, json, time
from flask import Flask, request, jsonify, abort

SECRET = open("/run/secrets/kit_supplier").read().strip().encode()
app = Flask(__name__)

@app.before_request
def signed():
    ts, sig = request.headers.get("X-Kit-Timestamp", "0"), request.headers.get("X-Kit-Signature", "")
    path = request.full_path.rstrip("?")
    msg = f"{ts}.{request.method} {path}.".encode() + request.get_data()
    want = "v1=" + hmac.new(SECRET, msg, hashlib.sha256).hexdigest()
    if abs(time.time() * 1000 - int(ts)) > 300_000 or not hmac.compare_digest(want, sig):
        abort(401)

@app.get("/catalog")
def catalog():
    return jsonify(plans=[{"id": p.sku, "name": p.title, "countries": p.countries, "kind": "country",
                           "data_bytes": p.bytes, "days": p.days, "cost_usd": float(p.cost)} for p in active_plans()])

@app.post("/orders")
def orders():
    body = request.get_json()
    plan = find_plan(body["plan_id"])
    if plan is None or float(plan.cost) != body["cost_usd"]:
        return jsonify(error="price_changed"), 409
    order = upsert_order(body["transaction_id"], plan)          # idempotent by transaction_id
    return jsonify(order_ref=order.ref)

Эталон на JavaScript со всеми пятью эндпоинтами: rails/examples/operator-backend.mjs.

4. Узел продавца

const node = createNode({
  supplier,                         // a supplier (section 2) or an eSIM Access client
  store: fileStore("/data/orders.json", fs),     // or your own {get, put}: order records, never codes
  secret: statusSecret,             // 16+ characters: the key of the buyers' status tokens
  signer: ed25519Signer(pem),       // your node's identity: signs every free answer
  publicUrl: "https://esim.example.com/agents",
  paywall,                          // buildPaywall(...) below, or null to quote without selling
  events,                           // createWebhooks(...) or { emit() {} }
  pricing: { marginPct: 100, minMarginUsd: 1 },
  sellerName: "My eSIM", productId: "esim-mine", demo: false,
});
const paywall = await buildPaywall({
  cdpKeyFile: "/run/secrets/cdp_api_key.json", payTo: "0xYourBaseAddress", publicUrl: "https://esim.example.com/agents",
  priceOf: (offerId) => node.priceOf(offerId), onSettleFailure: (p) => node.settleFailed(p),
});
node.app.listen(9490);

Маршруты: POST /quote и POST /status (а также /quote/invoke, /status/invoke для хабов), POST /order (x402), GET /listing.json, GET /healthz. Бесплатные маршруты отвечают любой веб-странице (CORS); заказ — нет.

5. Вебхуки

Задайте WEBHOOK_URL и WEBHOOK_SECRET_FILE (16+ символов) или передайте createWebhooks({ url, secret }).

СобытиеКогда
order.createdпокупатель начал заказ на витрине
order.deliveredоплачено и выдано
order.refund_dueоплачено, но поставщик отказал: верните деньги плательщику
order.underpaidпришёл платёж меньше цены
order.expiredза 24 часа ничего не пришло
order.awaiting_paymentподписанный платёж не удалось провести; заказ снова открыт
agent_order.deliveredзаказ агента выдан
agent_order.cancelledплатёж агента не удалось провести; eSIM отменена
POST /hooks/esim HTTP/1.1
Content-Type: application/json
X-Kit-Event: order.delivered
X-Kit-Timestamp: 1765432100000
X-Kit-Signature: v1=5d41402abc4b2a76b9719d911017c592…

{"id": "3f1c…", "type": "order.delivered", "created_at": "2026-10-14T10:46:24.000Z",
 "data": {"order_id": "Qh3…", "status": "delivered", "offer_id": "JP_5_30", "offer_name": "Japan 5GB 30Days",
          "price_usd": "6.40", "cost_usd": 3.2, "method": "tron",
          "payment": {"ref": "tron:…", "payer": "T…", "units": "6400042"}, "esim_tran_no": "26101410460022",
          "error": null, "created_at": "2026-10-14T10:45:58.000Z"}}

Подпись покрывает timestamp + "." + body. Доставка — минимум один раз (три попытки примерно за полминуты): убирайте дубли по id. Событие никогда не несёт код активации, ссылку на QR или токен заказа.

import { verifyWebhook } from "@aimarket/esim-rails";
app.post("/hooks/esim", express.text({ type: "*/*" }), (req, res) => {
  const ok = verifyWebhook({ secret, timestamp: req.get("x-kit-timestamp"), signature: req.get("x-kit-signature"), body: req.body });
  if (!ok) return res.sendStatus(401);
  crm.record(JSON.parse(req.body));
  res.sendStatus(204);
});
def kit_event_ok(secret: bytes, timestamp: str, signature: str, body: bytes) -> bool:
    want = "v1=" + hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return abs(time.time() * 1000 - int(timestamp)) < 300_000 and hmac.compare_digest(want, signature)

6. Контракт: esim.*@v1

ВозможностьСхема входаСхема результата
esim.quote@v1quote-inputquote-result (с offer)
esim.order@v1{offer_id} + x402delivery
esim.status@v1status-inputstatus-result

Схемы: rails/spec/esim-v1. Бесплатные ответы — {"success": true, "result": …} с X-Provider-Signature: Ed25519, base64, над каноническим JSON {capability_id, product_id, input_sha256, result} (ключи отсортированы, без пробелов, не-ASCII как есть; input_sha256 — SHA-256 в hex от канонического входа). Заказ отвечает 402 с заголовком x402 v2 PAYMENT-REQUIRED: схема exact, сеть eip155:8453, USDC, цена предложения с 6 знаками, ваш адрес.

Проверяйте в своих тестах: import { validate } from "@aimarket/esim-rails"; validate("offer", o) возвращает [] или список проблем.

7. Проверка соответствия

Направьте её на любой узел продавца — из кита или написанный вами:

npx esim-rails check https://esim.example.com/agents --country JP

Что она напечатала для демо-узла:

✓ listing.json — esim.quote@v1, esim.status@v1; order at https://modelmarket.dev/providers/esim/order
✓ esim.quote@v1 answer — 5 offers for JP, all valid
✓ cheapest first
✓ signature — Ed25519 over the request-bound canonical, key from listing.json
✓ bad input is a 400
✓ unknown eSIM is a 404 not_found
✓ unknown offer is a 400 before any price
⚠ x402 terms — a demo node: it sells nothing (demo_mode)
✓ CORS on the free routes

Conforms to esim.*@v1.

Она вызывает только бесплатные маршруты и спрашивает у заказа его цену; она никогда не платит.

8. Окружение

ПеременнаяСервис
SUPPLIERstore, railsesimaccess (по умолчанию), airalo, esimgo или http
AIRALO_CLIENT_ID_FILE, AIRALO_CLIENT_SECRET_FILE, AIRALO_BASE_URLstore, railsAiralo Partner API
ESIMGO_API_KEY_FILE, ESIMGO_BASE_URLstore, railseSIM Go
ESIMACCESS_ACCESS_CODE_FILE, ESIMACCESS_SECRET_KEY_FILEstore, railsключи eSIM Access
SUPPLIER_URL, SUPPLIER_SECRET_FILEstore, railsваша система, для SUPPLIER=http
WEBHOOK_URL, WEBHOOK_SECRET_FILEstore, railsсобытия для ваших систем; выключены, если не заданы
MARGIN_PCT, MIN_MARGIN_USDstore, railsцена = себестоимость + наценка
PAY_TO_EVM, CDP_KEY_FILErailsкуда платят агенты, и ключ фасилитатора
PAY_TO_EVM, PAY_TO_TON, PAY_TO_TRON, PAY_TO_SOLANA, EVM_CHAINSstoreкуда платят люди
DEMO_MODEstore, rails1: предложения и страницы, без продаж
PUBLIC_URL, PRODUCT_ID, SELLER_NAME / STORE_NAMEобаваша идентичность

9. Версии

esim.*@v1 только растёт: могут появиться новые необязательные поля, существующие сохраняют смысл. Изменение, которое ломает клиента, становится esim.*@v2 и публикуется рядом с v1. SDK следует semver; контракт HTTP-поставщика — версия 1.

Кит на GitHub →