Developers

Quickstart

Three calls: mint a key, create a link, read its clicks back. Everything below runs verbatim against a real Shortifi workspace.

1. Create an API key

In the app: Settings → API keys → New key. The full secret (sk_<prefix>.<remainder>) is shown once — store it in your secrets manager, not in source control.

2. Create a link

# replace $SHORTIFI_API_KEY and $WORKSPACE_ID
curl -X POST https://api.shortifi.me/api/v1/workspaces/$WORKSPACE_ID/links \
  -H "Authorization: Bearer $SHORTIFI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_url": "https://example.com/pricing"}'

Response (201):

{
  "id": "01J...",
  "short_code": "aZ3k9Pp",
  "target_url": "https://example.com/pricing",
  "final_url": "https://example.com/pricing",
  "click_count": 0,
  "status": "active",
  ...
}

3. Fetch its clicks

curl https://api.shortifi.me/api/v1/links/$LINK_ID/analytics \
  -H "Authorization: Bearer $SHORTIFI_API_KEY"
{
  "total_clicks": 0,
  "bot_clicks": 0,
  "unique_days": 0,
  "series": [],
  "top_sources": [],
  "top_mediums": [],
  "top_countries": [],
  "top_devices": []
}

That's the whole loop — everything else in the API (campaigns, vocabularies, custom domains, webhooks, bio pages) follows the same Bearer-auth, JSON-in/JSON-out shape.

Authentication

Every request outside the public redirect path (GET /{code}) is either browser session (httpOnly cookie, used by the app itself) or a programmatic Bearer API key. Third-party integrations — Zapier included — always use the latter.

Header

Authorization: Bearer sk_XXXXXXXX.YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY

A key is scoped to exactly one workspace, with the role of the person who created it (viewer/editor/admin/owner). It does not need — and must never send — a CSRF token; CSRF protection only applies to cookie-borne credentials. Revoke a key any time from Settings → API keys; revocation takes effect on the next request.

Rate limits

Limits are enforced per-route with a Redis sliding window and returned as 429 rate_limited when exceeded. Indicative limits on the write paths integrations care about most:

RouteLimit
Create link (POST /workspaces/:id/links)60 / min / actor
Bulk create (POST /workspaces/:id/links/bulk)5 / min / actor
Public redirect (GET /{code})200 / sec
Public shared dashboard (GET /d/{token})120 / min / IP

A rate-limited response includes no Retry-After header yet — back off and retry with jitter on any 429.

Core endpoints

The customer-facing surface is tagged x-public in the OpenAPI schema (see OpenAPI / schema) — that's the set safe to build integrations against.

ResourceBase path
Links/api/v1/workspaces/:id/links, /api/v1/links/:id
Campaigns/api/v1/workspaces/:id/campaigns
Analytics/api/v1/workspaces/:id/analytics, /api/v1/links/:id/analytics, /api/v1/campaigns/:id/analytics
Webhooks/api/v1/workspaces/:id/webhooks
Workspaces (read)/api/v1/workspaces

Webhooks

Register an endpoint and Shortifi POSTs a signed JSON payload on every matching event — this is the trigger side of the Zapier "New click" / "New link" integrations.

curl -X POST https://api.shortifi.me/api/v1/workspaces/$WORKSPACE_ID/webhooks \
  -H "Authorization: Bearer $SHORTIFI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.zapier.com/hooks/standard/xxxxx/", "event_types": ["link.created"]}'

Response includes the endpoint's signing secret — shown once, same as an API key. Every delivery carries:

HeaderMeaning
X-Shortifi-Eventevent type, e.g. link.created
X-Shortifi-Deliveryunique delivery id (idempotency key)
X-Shortifi-Timestampunix seconds the request was signed at
X-Shortifi-Signaturev1=<hex hmac-sha256>

The signature is HMAC-SHA256 over "{timestamp}.{raw request body}", keyed with the endpoint's secret — verify it before trusting the payload, and reject anything with a timestamp older than a few minutes to block replays.

Verify in Python

import hashlib
import hmac
import time

def verify_shortifi_signature(secret: str, timestamp: str, signature_header: str, raw_body: bytes, tolerance_s: int = 300) -> bool:
    if abs(time.time() - int(timestamp)) > tolerance_s:
        return False  # stale — possible replay
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    given = signature_header.removeprefix("v1=")
    return hmac.compare_digest(expected, given)

Verify in TypeScript (Node)

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyShortifiSignature(secret: string, timestamp: string, signatureHeader: string, rawBody: Buffer, toleranceS = 300): boolean {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceS) return false; // stale
  const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
  const given = signatureHeader.replace(/^v1=/, "");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(given, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Failed deliveries retry with exponential backoff and dead-letter after the final attempt; replay any delivery from Settings → Webhooks → Deliveries once your endpoint is fixed.

Event catalog

EventFires when
link.createda new short link is created
link.archiveda link is archived
click.recordeda click on one of your links lands in analytics (~1 min after the redirect)
conversion.recordedan attribution goal converts
domain.verifieda custom domain passes DNS verification
post.publisheda scheduled social post goes live
post.faileda scheduled social post fails to publish
anomaly.detectedthe click-anomaly detector flags an unusual spike/drop

Zapier's "New click" and "New link" triggers map directly to click.recorded and link.created via a REST Hook subscription (the POST /webhooks call above); "Create link" as a Zapier action maps to POST /workspaces/:id/links.

OpenAPI / schema

The full schema is served at https://api.shortifi.me/openapi.json (interactive docs at https://api.shortifi.me/docs outside production). Public, customer-facing operations are marked "x-public": true on each operation object — filter on that field if you're generating a client and only want the supported surface.

Questions or found a docs bug? Contact us — we read every message.