Branded Short Links — iec-short

The Short Version

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.

Setup

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.

Auth — bring your own service token

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.

Creating Links

Single link

POST /api/v1/links
FieldRequiredNotes
destinationUrlyesthe real URL to redirect to
source.serviceyesyour service name — shows up in GET /api/v1/links?service=
source.messageId / source.channel / source.templateNamenofree-form context, useful for correlating back to iec-relay sends
slugnocustom slug (3–60 chars, [a-z0-9-]); omit to auto-generate
entity.iecHash / entity.type / entity.namenoInsureBio 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
septorChainIdnoties click events back into an existing Septor chain (e.g. the messageId of the relay send that generated this link)
expiresAt / maxClicksnolink auto-deactivates (410 Gone) past either limit
tagsnoarray 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..."
}

Bulk create

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.

Querying & Managing Links

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.

Click Analytics

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.

The Redirect Itself

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:

StatusCause
404slug not found, or a reserved path (api*, admin, favicon.ico, robots.txt)
410link deactivated, expired, or hit maxClicks

What NOT to Do

// ❌ 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.

Key Facts

  • Koko name is 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.
  • No Bio-ID scopes published — any verified Bio-ID token (yours or a user's) is accepted. There's no per-caller allow-list today; don't rely on iec-short itself to gate who can create links pointing anywhere.
  • No SDK package yet — this doc is the only interface contract; calls are plain fetch. If you build a wrapper, consider publishing @insureco/short so this convention can be updated to reference it.
  • Redirect responses are always 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.
  • Uniqueness for 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