const BASE_URL = 'https://kakaesim.com/api/v1'
const API_KEY = process.env.KAKA_API_KEY
class ApiError extends Error {
constructor(code, message, status) {
super(message)
this.code = code
this.status = status
}
}
async function call(path, { method = 'GET', body } = {}) {
const res = await fetch(BASE_URL + path, {
method,
headers: {
Authorization: `Bearer ${API_KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
})
let payload
try {
payload = await res.json()
} catch {
throw new ApiError(-1, `Malformed response (HTTP ${res.status})`, res.status)
}
// code === 0 is the only success signal; the HTTP status is just a category
if (payload.code !== 0) {
const err = new ApiError(payload.code, payload.message, res.status)
err.retryAfter = payload.retry_after ?? null
throw err
}
return payload.data
}
async function placeOrder(outTradeNo, packageSlug, count = 1) {
try {
const { order } = await call('/orders', {
method: 'POST',
body: { out_trade_no: outTradeNo, package_slug: packageSlug, count },
})
return order
} catch (err) {
if (!(err instanceof ApiError)) {
// Network failure: the order may or may not exist. Retrying with the
// same out_trade_no is safe and returns the original order if there is one.
return call(`/orders/${encodeURIComponent(outTradeNo)}`)
.then((d) => d.order)
.catch(() => { throw err })
}
switch (err.code) {
case 3001: // insufficient balance, nothing was charged
throw new Error('Top up your balance and retry the same order number.')
case 3002: // provisioning failed, fully refunded — use a NEW order number
throw new Error('Provisioning failed and was refunded. Retry with a new order number.')
case 1004: // rate limited
throw new Error(`Rate limited, retry in ${err.retryAfter ?? 60}s.`)
default:
throw err
}
}
}
async function main() {
const { balance } = await call('/balance')
console.log('Balance:', balance, 'USD')
const { packages } = await call('/packages?destination=japan')
const pick = packages[0]
console.log('Buying:', pick.name, pick.price, 'USD')
const outTradeNo = `SHOP-${Date.now()}`
const order = await placeOrder(outTradeNo, pick.slug, 1)
console.log('Order:', order.out_trade_no, order.status, order.amount)
console.log('eSIM:', order.esim_id)
console.log('Activation:', order.esim?.ac ?? order.esim?.qr_code_url ?? 'pending')
}
main().catch((err) => {
console.error('Failed:', err.code ?? '', err.message)
process.exitCode = 1
})