@mandar1045/resync
v0.1.1
Published
Node.js SDK for Resync — the recovery layer for UPI Autopay. Register your autopay customers, report payment events, and let Resync win back failed charges.
Maintainers
Readme
@mandar1045/resync
Node.js SDK for Resync — the recovery layer for UPI Autopay.
Your billing stays on Razorpay or Cashfree. You register your autopay customers with Resync, it watches every expected charge, and when one fails it executes smart retries on your own gateway account. Pricing is success-based: 2% of recovered revenue.
- Zero runtime dependencies (uses the built-in
fetch) - Node 18+
- Full TypeScript types
Install
npm install @mandar1045/resyncQuick start
Get an API key from the Resync dashboard (Settings → API Keys), then:
import { Resync } from "@mandar1045/resync";
const resync = new Resync(process.env.RESYNC_API_KEY!, {
baseUrl: "http://localhost:8000", // your Resync gateway
});
// 1. Register an existing autopay customer (idempotent on external_ref)
const { autopay, already_existed } = await resync.autopays.register({
external_ref: "sub_1042", // your own id
customer: { name: "Asha", email: "[email protected]" },
gateway: "RAZORPAY",
gateway_mandate_id: "sub_NxA92kD1", // the gateway's mandate id
amount_paise: 49900, // ₹499.00
interval: "MONTHLY",
next_autopay_at: "2026-07-01T10:00:00Z",
last_payment_at: "2026-06-01T10:02:11Z",
});That's it — Resync now watches this autopay. If the expected charge doesn't arrive (or you report a failure), recovery starts automatically.
Backfill all your autopays
const result = await resync.autopays.registerBulk(
customers.map((c) => ({
external_ref: c.subscriptionId,
customer: { email: c.email, name: c.name },
gateway: "RAZORPAY",
gateway_mandate_id: c.razorpaySubscriptionId,
amount_paise: c.amountPaise,
next_autopay_at: c.nextChargeDate,
})),
);
console.log(`${result.imported} imported, ${result.failed} failed`);Import straight from your gateway (no per-customer data)
If you'd rather not assemble the list yourself, connect your Razorpay/Cashfree keys in the Resync dashboard (Settings → Gateways) and let Resync read your existing mandates directly from the gateway:
const { imported, updated, failed } = await resync.autopays.importFromGateway({
gateway: "RAZORPAY", // omit to import from every connected gateway
});
console.log(`${imported} imported, ${updated} updated, ${failed} failed`);Idempotent — re-running refreshes existing autopays instead of duplicating them. Resync only knows the customers it reads here (or that you register), so run this once after connecting a gateway, or enable background sync to keep it current automatically.
Report payment outcomes (recommended)
The fastest recovery starts the moment you see a failure:
// In your gateway webhook handler:
await resync.events.reportPayment({
external_ref: "sub_1042",
status: "FAILED",
failure_code: "U30", // insufficient balance → retried on salary day
amount_paise: 49900,
payment_ref: "pay_88x1", // your payment id — makes the call idempotent
});
// And on success, to keep the schedule in sync:
await resync.events.reportPayment({
external_ref: "sub_1042",
status: "SUCCESS",
amount_paise: 49900,
payment_ref: "pay_88x2",
});Keep autopays in sync
await resync.autopays.update(autopay.id, {
next_autopay_at: "2026-08-01T10:00:00Z",
});
await resync.autopays.cancel(autopay.id); // customer cancelled at your endReceive webhooks
Register an endpoint, then verify every delivery:
import express from "express";
import { constructWebhookEvent } from "@mandar1045/resync";
const app = express();
app.post("/webhooks/resync", express.raw({ type: "*/*" }), (req, res) => {
try {
const event = constructWebhookEvent(
req.body,
req.header("X-SubFlow-Signature")!,
process.env.RESYNC_WEBHOOK_SECRET!,
);
// event types: payment.failed, payment.succeeded
console.log("resync event:", event);
res.status(200).end();
} catch {
res.status(401).end();
}
});Recovery stats
const stats = await resync.dunning.stats();
console.log(
`recovered ₹${Number(stats.recovered_amount_paise ?? 0) / 100}`,
`(${stats.recovery_rate_pct?.toFixed(1)}% of ${stats.total_failed} failures)`,
);Error handling
Every non-2xx response throws a ResyncError:
import { ResyncError } from "@mandar1045/resync";
try {
await resync.autopays.get("does-not-exist");
} catch (err) {
if (err instanceof ResyncError) {
console.error(err.status, err.code, err.message);
}
}API reference
The full REST surface is described in openapi.yaml at
the repository root.
