const IEC_SHORT_URL = process.env.IEC_SHORT_URL // injected — see Setup
const res = await fetch(`${IEC_SHORT_URL}/api/v1/links`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${serviceToken}`, // your own service JWT — see Auth
},
body: JSON.stringify({
destinationUrl: 'https://policypay.com/portal/pay?loan=L-2024-67890&token=abc',
source: { service: 'my-service', channel: 'sms', templateName: 'payment-reminder' },
entity: { iecHash: '0x9f3a2b08721d4c5e8a1b3d7f6e0c2a49b5d81e37', type: 'insured', name: 'Cedar Creek Cattle Co' },
}),
})
const { shortUrl, slug } = await res.json()
// shortUrl === "https://insre.co/pay-0x9f3a2b08721d4c5e8a1b3d7f6e0c2a49b5d81e37"
iec-short is the platform's shortlink provider — branded insre.co URLs with
click analytics (device, browser, geo) and a hook back into Septor for chain
continuity ("message sent" → "link clicked"). It was built for iec-relay (SMS
character limits) but any service can use it for any outbound link.
Declare it under spec.dependencies. The Koko-registered name is iec-short
(unlike iec-relay → relay or iec-docman → docman, this one keeps its
iec- prefix — verified via tawa services list):
spec:
dependencies:
- service: iec-short # injects IEC_SHORT_URL (+ IEC_SHORT_CHARGE_MODE)
# transport omitted → defaults to janus (gas-metered, JWT-verified)
No scopes: needed — iec-short does not publish Bio-ID scopes (see Auth below),
so a bare dependency entry is all that's required.
iec-short's routes are auth: required, but its requireAuth middleware does
not check scopes — it accepts any Bio-ID JWT that verifies against the JWKS
endpoint (a user token, or any service's own client-credentials token). That
means the calling service just needs its own Bio-ID credentials, which requires
declaring spec.auth (either mode gets you BIO_CLIENT_ID/BIO_CLIENT_SECRET):
spec:
auth:
mode: service-only # or 'sso' if you also have user login
import { BioAuth } from '@insureco/bio'
const bio = BioAuth.fromEnv()
const { access_token: serviceToken } = await bio.getClientCredentialsToken([])
await fetch(`${process.env.IEC_SHORT_URL}/api/v1/links`, {
headers: { Authorization: `Bearer ${serviceToken}` },
// ...
})
If your service already calls Bio-ID for something else, reuse that token —
there's nothing iec-short-specific about it.
POST /api/v1/links
| Field | Required | Notes |
|---|---|---|
destinationUrl | yes | the real URL to redirect to |
source.service | yes | your service name — shows up in GET /api/v1/links?service= |
source.messageId / source.channel / source.templateName | no | free-form context, useful for correlating back to iec-relay sends |
slug | no | custom slug (3–60 chars, [a-z0-9-]); omit to auto-generate |
entity.iecHash / entity.type / entity.name | no | InsureBio entity link. iecHash must be 0x + 40 lowercase hex (iec-short rejects anything else with 400, e.g. a slug or a Mongo ObjectId); omit it if your record has none. When present with an inferred action (pay/quote/bind/cancel/renew/sign/doc), the slug auto-derives as {action}-{iecHash} instead of a random one |
septorChainId | no | ties click events back into an existing Septor chain (e.g. the messageId of the relay send that generated this link) |
expiresAt / maxClicks | no | link auto-deactivates (410 Gone) past either limit |
tags | no | array of strings, filterable later |
// 201 response
{
"id": "...",
"shortUrl": "https://insre.co/pay-0x9f3a2b08721d4c5e8a1b3d7f6e0c2a49b5d81e37",
"slug": "pay-0x9f3a2b08721d4c5e8a1b3d7f6e0c2a49b5d81e37",
"slugType": "auto",
"destinationUrl": "...",
"expiresAt": null,
"createdAt": "2026-09-14T..."
}
POST /api/v1/links/bulk
Same shape, { "links": [ {...}, {...} ] }, max 100 per request. Per-item
failures don't fail the batch — response is { created, failed, links, errors }
with errors[].index pointing back into your input array.
GET /api/v1/links?iecHash=&service=&messageId=&tag=&isActive=&limit=&offset=
GET /api/v1/links/:id
PATCH /api/v1/links/:id { destinationUrl?, isActive?, expiresAt?, maxClicks?, tags? }
PATCH invalidates the Redis redirect cache for that slug immediately — a
deactivated or redirected link takes effect on the very next click, not after a
TTL.
GET /api/v1/stats/:linkId # single link — device/browser/geo breakdown + daily timeline
GET /api/v1/stats?slug=|iecHash=|service=&days=30 # aggregate across a filter
stats/:linkId returns devices, browsers, topLocations (top 10 by click
count), and clickTimeline (daily counts) alongside totalClicks /
uniqueClicks. Uniqueness is tracked by a first-party iec_short_cid cookie on
the redirect hop, not by IP — a user without cookies enabled counts as unique on
every click.
GET https://insre.co/:slug — this is what actually fires when someone clicks
the link. No auth, 302 to destinationUrl. It's cache-first (Redis, with a
MongoDB fallback and re-cache on miss), so the extra hop is fast even under
load. Click tracking (trackClick) runs setImmediate-deferred and fire-and-
forget — a tracking failure never delays or breaks the redirect.
Failure responses on the redirect itself:
| Status | Cause |
|---|---|
404 | slug not found, or a reserved path (api*, admin, favicon.ico, robots.txt) |
410 | link deactivated, expired, or hit maxClicks |
// ❌ WRONG: hardcoding insre.co or a K8s DNS URL
await fetch('https://insre.co/api/v1/links', ...)
await fetch('http://iec-short.iec-short-prod.svc.cluster.local:3000/api/v1/links', ...)
// ✅ CORRECT: use the injected dependency URL
await fetch(`${process.env.IEC_SHORT_URL}/api/v1/links`, ...)
// ❌ WRONG: assuming a scope grant gates this — it doesn't
spec:
dependencies:
- service: iec-short
scopes: [short:create] # inert — iec-short publishes no scopes; harmless but does nothing
// ❌ WRONG: awaiting stats/creation on the hot path of an SMS/email send
await relay.sendEmail({ html: `<a href="${await shorten(url)}">Pay now</a>` })
// If iec-short is slow, your send is slow too — shorten opportunistically/async
// where the flow allows it, and fall back to the full URL on error.
iec-short, not short — this one didn't get the prefix
stripped like relay/docman did. Double-check with tawa services list if
unsure before assuming a pattern.iec-short
itself to gate who can create links pointing anywhere.fetch. If you build a wrapper, consider publishing @insureco/short
so this convention can be updated to reference it.302, never 301 — links are meant to be
re-pointable (PATCH destinationUrl) without browsers caching the redirect.slug auto-generation prefers {action}-{iecHash} (e.g.
pay-0x9f3a2b08721d4c5e8a1b3d7f6e0c2a49b5d81e37) over random, and falls back to an 8-char random slug
only on conflict or when no entity.iecHash is given.uniqueClickCount is cookie-based (iec_short_cid), not
IP-based — treat it as directional, not exact.databases: [mongodb, redis] — MongoDB is the record of truth, Redis is a
redirect-speed cache invalidated on every PATCH.Last updated: October 6, 2026