Skip to main content

Short URLs

Short URLs keep distribution links compact — particularly for SMS, where a full survey URL with ?agent= and cd.* query data would be unwieldy. Unlike email/SMS/IVR, this is not a provider port: it is a tenant database service plus a root-mounted public redirect.

Minting (management API)

ShortUrlService (api/src/domains/short-url.ts) is a thin wrapper over the tenant shortUrls repository. The management routes are under /v1 (api/src/routes/short-urls.ts); minting requires surveys.distribute.

MethodPathPermission
GET/v1/short-urlstenant member
POST/v1/short-urlssurveys.distribute
DELETE/v1/short-urls/:idsurveys.distribute
shortUrlRoutes.post('/short-urls', async (c) => {
if (!can(c.var.tenant, 'surveys.distribute')) return c.json(forbidden('surveys.distribute'), 403);
const parsed = shortUrlBody.safeParse(await c.req.json().catch(() => null));
if (!parsed.success) return c.json(bad('invalid short-url body', parsed.error.issues), 400);
const { id, slug } = await new ShortUrlService(c.var.ports.db).create(c.var.tenant, parsed.data.targetUrl);
return c.json({ id, slug }, 201);
});

create(targetUrl) mints a slug bound to the target and returns { id, slug }.

Resolution & click counting (/r/:slug)

Resolution is a single root-mounted public route in api/src/app.ts, kept at the root so the public link is short (e.g. https://host/r/Ab3xK9pQ). It is rate-limited per client IP (60/min):

app.use('/r/:slug', rateLimit({ limit: 60, windowMs: 60_000, key: clientKey }));
app.get('/r/:slug', async (c) => {
const target = await c.var.ports.db.resolveShortUrl(c.req.param('slug')).catch(() => null);
return c.redirect(target ?? '/', 302);
});

resolveShortUrl(slug) looks up the target, counts the click, and returns the destination; the handler 302-redirects to it. An unknown slug (or any lookup failure) redirects to home (/) rather than erroring.

How channels use it

  • SMS link fallback — when a survey has no SMS-answerable questions, DispatchService.invite() shortens ${base}/s/${surveyId}${agentQuery} and texts ${base}/r/${slug}. The agentQuery preserves ?agent= and cd.<key>= through the redirect.
  • ADEPT web modeAdeptService.initiate() with channel: 'web' builds the survey URL with the agent and custom data, shortens it, and returns ${baseUrl}/r/${slug} as the link for the agent to read out to the customer.

Because the slug points at a full survey-runtime URL, all attribution and custom-data query parameters survive the redirect and are captured by the runtime — see the completion runtime.