@rentaltide/underwriter
v0.1.1
Published
Build a RentalTide coverage underwriter: signature verification, routing, validation and idempotent binds, so you write only underwriting.
Maintainers
Readme
@rentaltide/underwriter
Build a RentalTide coverage underwriter. You write underwriting; the SDK does signature verification, routing, validation and idempotent binds.
npm i @rentaltide/underwriterimport crypto from 'node:crypto';
import { createUnderwriter, decline, NO_QUOTE } from '@rentaltide/underwriter';
const rentaltide = createUnderwriter({ secret: process.env.SIGNING_SECRET });
rentaltide
.quote(({ bookingSubtotal, days, currency }) => {
if (bookingSubtotal < 50) return NO_QUOTE; // no card renders, booking proceeds
return {
quoteId: crypto.randomUUID(),
premium: Math.max(bookingSubtotal * 0.065, 12),
currency,
coverageLimit: 25_000,
depositEffect: 'reduce_to',
depositEffectValue: 500,
questionSchemaVersion: '2026-09-01',
};
})
.schema(() => ({
version: '2026-09-01',
fields: [
{
id: 'date_of_birth',
label: 'Date of birth',
type: 'date',
required: true,
minAgeYears: 18,
},
{ id: 'prior_claims_3y', label: 'Claims in the last 3 years?', type: 'checkbox' },
],
}))
.requote(({ basePremium, answers }) => {
if (tooYoung(answers.date_of_birth)) return decline('Operator must be 18 or over.');
return {
decision: 'accepted',
premium: answers.prior_claims_3y ? basePremium * 1.4 : basePremium,
};
})
.bind(({ quoteId, bookingReference, renter }) => issuePolicy(quoteId, bookingReference, renter))
.reschedule(({ policyId, startsAt, endsAt }) => ({ decision: 'accepted' }))
.cancel(({ policyId }) => voidPolicy(policyId));
rentaltide.listen(8787);What it handles so you don't
The signature. Timing-safe comparison, a five-minute timestamp window, and
the HMAC taken over the raw bytes — JSON.stringify of a parsed body does
not round-trip byte for byte, which is the quiet way a hand-rolled check ends up
rejecting valid requests or accepting invalid ones.
Idempotent binds. RentalTide retries /bind on a timeout or a 5xx. The SDK
remembers the policy against its quoteId and returns the same one, so a retry
cannot write a second policy on the same booking — the most expensive mistake in
this integration. Memory by default; pass store for production:
createUnderwriter({
secret,
store: {
get: (quoteId) => db.policies.findByQuoteId(quoteId),
set: (quoteId, policy) => db.policies.insert({ quoteId, ...policy }),
},
});The rules that fail quietly. A premium returned as "44.50", a reduce_to
with no depositEffectValue (which renders to a renter as a saving and then
changes nothing at hold time), a documentUrl over plain http, more than twelve
questions, a regex with a nested quantifier. Each throws where the handler
returned, with the reason, instead of becoming a card that silently never
renders.
Anywhere you run
http.createServer(rentaltide.handler()); // Node
export default { fetch: rentaltide.fetch }; // Workers, Hono, Next route handlers
rentaltide.listen(8787); // or just thisTesting it
npm i -g @rentaltide/cli
rentaltide coverage test --url http://localhost:8787Fifteen checks, signed exactly as the platform signs them, exit code included.
Contract
https://docs.rentaltide.com/developers/embedded-insurance/
Types for every payload ship with the package.
