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.
0. Prerequisites
Section titled “0. Prerequisites”- A Trile merchant account and a
nep_test_API key (Authentication). - Node.js 18+.
mkdir trile-integration && cd trile-integrationnpm init -y && npm pkg set type=modulenpm install @trilehq/sdk expressSet your environment (never hard-code keys):
export TRILE_SECRET_KEY="nep_test_your_key_here"# TRILE_WEBHOOK_SECRET comes from step 1 below1. 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.
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 nowexport TRILE_WEBHOOK_SECRET="whsec_..." # from the output aboveexport TRILE_PRICE_ID="price_..."2. The server — checkout + verified webhook + access grant
Section titled “2. The server — checkout + verified webhook + access grant”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:
node server.mjs3. Try the flow
Section titled “3. Try the flow”POST /api/checkout→ geturl→ open it in a browser.- Complete the hosted page (phone OTP → top-up → confirm). In test mode, use test keys and sandbox providers.
- Watch your server log
access granted: cus_…when the verifiedsubscription.createdwebhook 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 })).
What you now have
Section titled “What you now have”- A plan —
product+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.createdevent, 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.
- Webhooks in depth — rotation, deliveries, replay.
- Subscriptions — statuses,
past_duerecovery, cancellation. - Going live — swap
nep_test_fornep_live_.