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.
| Method | Path | Permission |
|---|---|---|
GET | /v1/short-urls | tenant member |
POST | /v1/short-urls | surveys.distribute |
DELETE | /v1/short-urls/:id | surveys.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}. TheagentQuerypreserves?agent=andcd.<key>=through the redirect. - ADEPT web mode —
AdeptService.initiate()withchannel: '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.