Skip to content

Integrate in 10 minutes

This page is a complete, paste-ready integration: a small Express server that sells one subscription plan through hosted checkout, verifies webhooks, and grants access when a subscription is created. Everything below is real, runnable code — replace the placeholders and go.

  • A Trile merchant account and a nep_test_ API key (Authentication).
  • Node.js 18+.
Terminal window
mkdir trile-integration && cd trile-integration
npm init -y && npm pkg set type=module
npm install @trilehq/sdk express

Set your environment (never hard-code keys):

Terminal window
export TRILE_SECRET_KEY="nep_test_your_key_here"
# TRILE_WEBHOOK_SECRET comes from step 1 below

1. One-time bootstrap — product, price, webhook endpoint

Section titled “1. One-time bootstrap — product, price, webhook endpoint”

Run this once (node bootstrap.mjs) to model your plan and register your webhook endpoint. Save the IDs and the webhook secret it prints.

bootstrap.mjs
import { Configuration, CatalogApi, WebhooksApi } from '@trilehq/sdk'
const config = new Configuration({ apiKey: process.env.TRILE_SECRET_KEY })
const catalog = new CatalogApi(config)
const webhooks = new WebhooksApi(config)
// What you sell…
const { data: product } = await catalog.createProduct({
createProduct: { name: 'Pro plan' },
idempotencyKey: crypto.randomUUID(),
})
// …and how it's billed: NPR 499.00/month (amounts are string paisa).
const { data: price } = await catalog.createPrice({
createPrice: { productId: product.id, amountPaisa: '49900', interval: 'month' },
idempotencyKey: crypto.randomUUID(),
})
// Where Trile should send billing events.
const { data: endpoint } = await webhooks.createWebhookEndpoint({
createWebhookEndpoint: {
url: 'https://yourapp.com/webhooks/trile', // must be publicly reachable
enabled_events: ['subscription.created', 'invoice.paid', 'invoice.payment_failed'],
},
idempotencyKey: crypto.randomUUID(),
})
console.log('PRICE_ID =', price.id)
console.log('WEBHOOK_ID =', endpoint.id)
console.log('TRILE_WEBHOOK_SECRET =', endpoint.secret) // shown exactly once — store it now
Terminal window
export TRILE_WEBHOOK_SECRET="whsec_..." # from the output above
export TRILE_PRICE_ID="price_..."

2. The server — checkout + verified webhook + access grant

Section titled “2. The server — checkout + verified webhook + access grant”
server.mjs
import crypto from 'node:crypto'
import express from 'express'
import { Configuration, CheckoutApi } from '@trilehq/sdk'
const app = express()
const checkout = new CheckoutApi(
new Configuration({ apiKey: process.env.TRILE_SECRET_KEY }),
)
// ── Your "database" (swap for a real one) ────────────────────────────────────
const activeCustomers = new Set()
// ── 1) Start a checkout: your app calls this, then redirects the user ───────
app.post('/api/checkout', express.json(), async (req, res) => {
const { data: session } = await checkout.createCheckoutSession({
createCheckoutSession: {
priceId: process.env.TRILE_PRICE_ID,
successUrl: 'https://yourapp.com/welcome',
cancelUrl: 'https://yourapp.com/pricing',
},
idempotencyKey: crypto.randomUUID(),
})
// Send the customer to Trile's hosted page (phone OTP + wallet top-up + subscribe).
res.json({ url: session.publicUrl, expiresAt: session.expiresAt })
})
// ── 2) Webhook: raw body + Trile-Signature verification ─────────────────────
// The signature covers the exact bytes Trile sent — use express.raw here,
// NOT express.json (a parsed-then-restringified body breaks the HMAC).
function verifyTrile(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
const t = parts['t']
const sig = parts['v1']
if (!t || !sig) return false
// Reject stale deliveries (replay protection).
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex')
const a = Buffer.from(sig)
const b = Buffer.from(expected)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
const seenEvents = new Set() // dedupe — deliveries are at-least-once
app.post(
'/webhooks/trile',
express.raw({ type: 'application/json' }),
(req, res) => {
const raw = req.body.toString('utf8')
const header = req.get('Trile-Signature') ?? ''
if (!verifyTrile(raw, header, process.env.TRILE_WEBHOOK_SECRET)) {
return res.status(400).send('invalid signature')
}
const event = JSON.parse(raw)
if (seenEvents.has(event.id)) return res.sendStatus(200) // repeat delivery
seenEvents.add(event.id)
// ── 3) Grant access on the verified event — never on the redirect ───────
switch (event.type) {
case 'subscription.created':
activeCustomers.add(event.data.customerId)
console.log('access granted:', event.data.customerId)
break
case 'invoice.payment_failed':
console.log('past due:', event.data.subscriptionId) // optionally restrict access
break
case 'invoice.paid':
console.log('cycle paid:', event.data.subscriptionId)
break
}
res.sendStatus(200) // respond fast; do slow work async
},
)
// ── Gate your product on the grant ───────────────────────────────────────────
app.get('/api/me/access', express.json(), (req, res) => {
const customerId = req.query.customerId // however you map your users → cus_…
res.json({ active: activeCustomers.has(customerId) })
})
app.listen(3000, () => console.log('listening on :3000'))

Run it:

Terminal window
node server.mjs
  1. POST /api/checkout → get url → open it in a browser.
  2. Complete the hosted page (phone OTP → top-up → confirm). In test mode, use test keys and sandbox providers.
  3. Watch your server log access granted: cus_… when the verified subscription.created webhook lands.

To exercise the webhook route before going end-to-end, send a test event: POST /v1/webhooks/endpoints/:id/test (webhooks.sendTestWebhook({ id, idempotencyKey })).

  • A planproduct + price (NPR 499.00/month, string paisa).
  • A checkout endpoint — mints a hosted session; the customer handles OTP/KYC/top-up on Trile’s page, not yours.
  • A verified webhook — raw-body HMAC check, replay protection, event dedupe.
  • Server-side fulfilment — access is granted by the subscription.created event, never by the success redirect.

Renewals are automatic: each cycle Trile invoices and debits the wallet, then emits invoice.paid or invoice.payment_failed — your handler above already sees both.