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.
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:
| Route | Limit |
|---|---|
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.
| Resource | Base 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:
| Header | Meaning |
|---|---|
X-Shortifi-Event | event type, e.g. link.created |
X-Shortifi-Delivery | unique delivery id (idempotency key) |
X-Shortifi-Timestamp | unix seconds the request was signed at |
X-Shortifi-Signature | v1=<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
| Event | Fires when |
|---|---|
link.created | a new short link is created |
link.archived | a link is archived |
click.recorded | a click on one of your links lands in analytics (~1 min after the redirect) |
conversion.recorded | an attribution goal converts |
domain.verified | a custom domain passes DNS verification |
post.published | a scheduled social post goes live |
post.failed | a scheduled social post fails to publish |
anomaly.detected | the 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.