Elixir client for the eSIM Access wholesale eSIM reseller API.
Order, query, top up, suspend, and cancel eSIM profiles across 185+ countries. Includes typed structs for all responses, a webhook handler with Plug integration for Phoenix, and comprehensive test coverage.
Add esim_access to your list of dependencies in mix.exs:
def deps do
[
{:esim_access, "~> 0.1.0"}
]
end# 1. Create a config with your access code
config = EsimAccess.new(access_code: "your_access_code")
# 2. Check your balance
{:ok, %{balance: balance}} = EsimAccess.Balance.query(config)
# balance is value * 10,000 -- so 100_000 = $10.00
# 3. Browse available packages
{:ok, packages} = EsimAccess.Packages.list(config, %{location_code: "JP"})
# 4. Order an eSIM
{:ok, order} = EsimAccess.Orders.create(config, %{
transaction_id: "txn_#{System.os_time(:millisecond)}",
package_info_list: [
%{package_code: "JP_1_7", count: 1}
]
})
# 5. Query the allocated profile (may take up to 30s)
{:ok, {profiles, _pager}} = EsimAccess.Profiles.query(config, %{
order_no: order.order_no,
pager: %{page_num: 1, page_size: 50}
})
profile = hd(profiles)
# profile.ac -> LPA activation code
# profile.qr_code_url -> QR code image URL
# profile.iccid -> eSIM ICCIDCreate a config struct with EsimAccess.new/1. The struct is passed as the
first argument to every API call -- no global state, no application config.
# Production
config = EsimAccess.new(access_code: "your_access_code")
# Custom base URL (for testing/proxying)
config = EsimAccess.new(
access_code: "your_access_code",
base_url: "https://your-proxy.example.com"
)Get your access code from the eSIM Access developer console.
All functions return {:ok, result} or {:error, %EsimAccess.Error{}}.
# All packages
{:ok, packages} = EsimAccess.Packages.list(config)
# Filter by country (Alpha-2 ISO code)
{:ok, packages} = EsimAccess.Packages.list(config, %{location_code: "US"})
# Regional or global packages
{:ok, packages} = EsimAccess.Packages.list(config, %{location_code: "!RG"})
{:ok, packages} = EsimAccess.Packages.list(config, %{location_code: "!GL"})
# Day Pass plans only
{:ok, packages} = EsimAccess.Packages.list(config, %{data_type: "2"})
# Top-up packages for a specific ICCID
{:ok, packages} = EsimAccess.Packages.list(config, %{
type: "TOPUP",
iccid: "89852..."
})# Single order
{:ok, order} = EsimAccess.Orders.create(config, %{
transaction_id: "unique_txn_id",
package_info_list: [%{package_code: "JP_1_7", count: 1}]
})
# With price verification (fails if price changed)
{:ok, order} = EsimAccess.Orders.create(config, %{
transaction_id: "unique_txn_id",
amount: 15000,
package_info_list: [
%{package_code: "JP_1_7", count: 1, price: 15000}
]
})
# Daily plan with specific number of days
{:ok, order} = EsimAccess.Orders.create(config, %{
transaction_id: "unique_txn_id",
package_info_list: [
%{package_code: "SG_1_Daily", count: 1, period_num: 5}
]
})# Query by order number
{:ok, {profiles, pager}} = EsimAccess.Profiles.query(config, %{
order_no: "B23051616050537",
pager: %{page_num: 1, page_size: 50}
})
# Query by ICCID
{:ok, {profiles, pager}} = EsimAccess.Profiles.query(config, %{
iccid: "89852246280001113119",
pager: %{page_num: 1, page_size: 50}
})
# Cancel unused profile (refundable)
{:ok, _} = EsimAccess.Profiles.cancel(config, %{esim_tran_no: "23120118156818"})
# Suspend / unsuspend data service
{:ok, _} = EsimAccess.Profiles.suspend(config, %{esim_tran_no: "23120118156818"})
{:ok, _} = EsimAccess.Profiles.unsuspend(config, %{esim_tran_no: "23120118156818"})
# Revoke profile (non-refundable)
{:ok, _} = EsimAccess.Profiles.revoke(config, %{esim_tran_no: "23120118156818"}){:ok, result} = EsimAccess.TopUp.create(config, %{
esim_tran_no: "23072017992029",
package_code: "TOPUP_JC172",
transaction_id: "topup_txn_001"
})
# result.expired_time -> new expiry
# result.total_volume -> new total data (bytes){:ok, %{balance: balance}} = EsimAccess.Balance.query(config)
# balance is value * 10,000 (100_000 = $10.00)# Check usage for up to 10 eSIMs (updated every 2-3 hours)
{:ok, usages} = EsimAccess.Usage.check(config, ["25030303480009"])
# usages -> [%{esim_tran_no, data_usage, total_data, last_update_time}]{:ok, _} = EsimAccess.Sms.send(config, %{
esim_tran_no: "23072017992029",
message: "Your verification code is 123456"
}){:ok, locations} = EsimAccess.Locations.list(config)
# type 1 = single country, type 2 = multi-country region# Set webhook URL
{:ok, _} = EsimAccess.Webhook.save(config, "https://your-app.com/webhooks/esim")
# Query current webhook
{:ok, %{"webhook" => url}} = EsimAccess.Webhook.query(config)The library provides typed event structs, a handler behaviour, and a Plug for receiving eSIM Access webhook notifications in Phoenix.
| Event | Description |
|---|---|
OrderStatus |
Order fulfilled -- profiles ready for download |
EsimStatus |
eSIM lifecycle changes (in use, expired, etc.) |
SmdpEvent |
Low-level SM-DP+ profile state transitions |
DataUsage |
Data consumption threshold alerts (50%, 90%) |
ValidityUsage |
Validity period expiry warnings (1 day left) |
CheckHealth |
Connectivity check on initial webhook setup |
defmodule MyApp.EsimWebhookHandler do
@behaviour EsimAccess.Webhooks.Handler
alias EsimAccess.Webhooks.Event
@impl true
def handle_event(%Event.OrderStatus{order_status: "GOT_RESOURCE"} = event) do
# Profiles allocated -- fetch ICCID and QR code
MyApp.Esim.fetch_profiles(event.order_no, event.transaction_id)
:ok
end
@impl true
def handle_event(%Event.DataUsage{remain_threshold: threshold} = event) do
if threshold <= 0.1 do
MyApp.Notifications.send_low_data_warning(event.transaction_id)
end
:ok
end
@impl true
def handle_event(%Event.ValidityUsage{remain: 1} = event) do
MyApp.Notifications.send_expiry_warning(event.transaction_id)
:ok
end
@impl true
def handle_event(_event), do: :ok
end# In your Phoenix router
forward "/webhooks/esim", EsimAccess.Webhooks.Plug,
handler: MyApp.EsimWebhookHandlerOr with IP verification:
forward "/webhooks/esim", EsimAccess.Webhooks.Plug,
handler: MyApp.EsimWebhookHandler,
verify_ip: truedefmodule MyAppWeb.EsimWebhookController do
use MyAppWeb, :controller
def handle(conn, params) do
case EsimAccess.Webhooks.Event.parse(params) do
{:ok, event} ->
MyApp.EsimWebhookHandler.handle_event(event)
send_resp(conn, 200, "ok")
{:error, _reason} ->
send_resp(conn, 400, "invalid event")
end
end
endAll prices are expressed as value * 10,000. For example, 10000 = $1.00 USD.
All data volumes are in bytes. Common conversions:
| Value | Bytes |
|---|---|
| 100 MB | 104,857,600 |
| 1 GB | 1,073,741,824 |
| 5 GB | 5,368,709,120 |
Most profile operations accept either iccid or esim_tran_no. Prefer
esim_tran_no because ICCIDs can be reused across profiles.
The API enforces a limit of 8 requests per second. The client includes automatic retry with backoff for transient errors.
All API functions return {:error, %EsimAccess.Error{}} on failure:
case EsimAccess.Orders.create(config, params) do
{:ok, order} ->
# success
{:error, %EsimAccess.Error{error_code: "200007"}} ->
# insufficient balance
{:error, %EsimAccess.Error{error_code: code, error_message: msg}} ->
Logger.error("eSIM API error [#{code}]: #{msg}")
endSee EsimAccess.Error module docs for the full error code reference.
# Unit tests (no API key needed)
mix test
# Integration tests (requires access code)
ESIM_ACCESS_CODE=your_code mix test --include integrationMIT