Shortifies API

v1.0.0 · base URL https://dashboard.shortifies.com/api

Create and manage short links, read their analytics, report conversions from your server and receive events as they happen.

Authentication

Create a secret key in Developers in the dashboard and send it with every request. Keys belong to one workspace and carry scopes; each endpoint below lists the scope it needs.

curl https://dashboard.shortifies.com/api/v1/links \
  -H "Authorization: Bearer sfy_sk_…"

Keys start with sfy_sk_. Keep them on servers only. Revoked and expired keys get 401 at once.

Errors

Errors use HTTP status codes and a body with a stable, machine-readable code. Branch on the code; the message is for people.

{
  "error": {
    "code": "validation_failed",
    "message": "The request is invalid.",
    "details": [{ "path": "destinationUrl", "message": "Enter a full URL.", "code": "url_invalid" }]
  }
}

Rate limits

Each key may make a fixed number of requests per minute. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window resets). Over the limit you get 429 with Retry-After.

Retries and idempotency

Network errors happen. Writes marked idempotent accept an Idempotency-Key header: send a unique value (a UUID works) and retry with the same key and body; you get the first response back instead of a second link. Keys are remembered for 24 hours per API key.

For conversions, pass your own id (an order id): reporting the same conversion twice counts it once, even without the header.

Pagination

Lists return { items, nextCursor }. Pass nextCursor as cursor to get the next page; it’s null on the last one.

Bulk jobs

Bulk changes, CSV imports and exports run in the background. They return a job right away (202); poll it until status is SUCCEEDED or FAILED. Each item succeeds or fails on its own, and /v1/jobs/{jobId}/items tells you which.

Get a link’s QR code

get/v1/qr-codes/{linkId}

Scope: links:read (Read links).

Parameters

  • linkIdstring (uuid)required

    Path.

  • format"png" | "svg"default "svg"

    Query.

  • sizeintegerdefault 512 · 128–2048

    Query.

  • download"0" | "1"default "0"

    Query.

Response

200 The QR code. image/svg+xml or image/png

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 404

    Not found in this workspace.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Bulk

Bulk changes, CSV import and export. All run in the background as jobs.

Get a job

get/v1/jobs/{jobId}

Scope: links:read (Read links).

Parameters

  • jobIdstring (uuid)required

    Path.

Response

200 Success. Returns Job.

Errors
  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 404

    Not found in this workspace.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

List a job’s item results

get/v1/jobs/{jobId}/items

In item order. Page with after (the last index you saw).

Scope: links:read (Read links).

Parameters

  • jobIdstring (uuid)required

    Path.

  • status"ok" | "error"

    Query.

  • afterintegerdefault -1 · ≥ -1

    Query.

  • limitintegerdefault 100 · 1–1000

    Query.

Response

200 Success. Returns JobItemPage.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 404

    Not found in this workspace.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Organize

Tags and campaigns.

List tags

get/v1/tags

Scope: links:read (Read links).

Response

200 Success. Returns TagPage.

Errors
  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Create a tag

post/v1/tags

Scope: links:write (Create and edit links).

Parameters

  • Idempotency-Keystringmax 255 chars

    Header. Any unique string (a UUID works). Retries with the same key and body return the original response.

Request body (application/json)

  • namestringrequiredmax 32 chars
  • color"gray" | "blue" | "green" | "amber" | "red" | "purple" | "pink" | "teal"default "gray"

Response

201 Success. Returns Tag.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 409

    Conflict, e.g. alias_taken, or idempotency_in_progress.

  • 422

    Understood but not possible, e.g. domain_unavailable, idempotency_key_reused.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

List campaigns

get/v1/campaigns

Scope: links:read (Read links).

Response

200 Success. Returns CampaignPage.

Errors
  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Create a campaign

post/v1/campaigns

Scope: links:write (Create and edit links).

Parameters

  • Idempotency-Keystringmax 255 chars

    Header. Any unique string (a UUID works). Retries with the same key and body return the original response.

Request body (application/json)

  • namestringrequiredmax 80 chars
  • descriptionstring | nullmax 500 chars
  • startsAtstring (date-time) | null
  • endsAtstring (date-time) | null

Response

201 Success. Returns Campaign.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 409

    Conflict, e.g. alias_taken, or idempotency_in_progress.

  • 422

    Understood but not possible, e.g. domain_unavailable, idempotency_key_reused.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Get a campaign

get/v1/campaigns/{campaignId}

Scope: links:read (Read links).

Parameters

  • campaignIdstring (uuid)required

    Path.

Response

200 Success. Returns Campaign.

Errors
  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 404

    Not found in this workspace.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Update or archive a campaign

patch/v1/campaigns/{campaignId}

Scope: links:write (Create and edit links).

Parameters

  • campaignIdstring (uuid)required

    Path.

Request body (application/json)

  • namestringmax 80 chars
  • descriptionstring | nullmax 500 chars
  • startsAtstring (date-time) | null
  • endsAtstring (date-time) | null
  • archivedboolean

Response

200 Success. Returns Campaign.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 404

    Not found in this workspace.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Domains

List domains

get/v1/domains

The platform subdomain and any custom domains, with their setup status.

Scope: domains:read (Read domains).

Response

200 Success. Returns DomainPage.

Errors
  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Analytics

Click and conversion report

get/v1/analytics

Totals, a time series and breakdowns for a time range, optionally for one link, domain or campaign, and narrowed to any breakdown value (country, region, city, device, OS, browser, referrer, source or UTM value). Add compare=true for the previous period of the same length.

Scope: analytics:read (Read analytics).

Parameters

  • fromstring (date-time)required

    Query.

  • tostring (date-time)required

    Query.

  • interval"hour" | "day"default "day"

    Query.

  • timezonestringdefault "UTC" · max 64 chars

    Query.

  • linkIdstring (uuid)

    Query.

  • domainIdstring (uuid)

    Query.

  • campaignIdstring (uuid)

    Query.

  • comparestring

    Query.

  • countrystring

    Query.

  • regionstringmax 200 chars

    Query.

  • citystringmax 200 chars

    Query.

  • devicestringmax 40 chars

    Query.

  • osstringmax 60 chars

    Query.

  • browserstringmax 60 chars

    Query.

  • referrerstringmax 253 chars

    Query.

  • sourcestringmax 40 chars

    Query.

  • utmSourcestringmax 200 chars

    Query.

  • utmMediumstringmax 200 chars

    Query.

  • utmCampaignstringmax 200 chars

    Query.

  • filtersstringmax 20000 chars

    Query. JSON array of conditions, ANDed. Each is {"dim", "op": "is" | "is_not", "values": [...]} and matches any (or, with is_not, none) of its values. Dimensions: link, domain, campaign, tag (ids), continent (AF, AS, EU, NA, OC, SA), country (ISO codes), region, city, device, os, browser, referrer, source (link or qr), utm_source, utm_medium, utm_campaign, weekday (1 = Monday … 7), hour (0–23, in timezone). Region, city, os, browser, UTM, weekday and hour need advanced analytics.

Response

200 Success. Returns Analytics.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Live clicks

get/v1/analytics/live

Counted clicks from the last minutes (5 to 60, default 30) as they arrive, with clicks per minute. Derived fields only.

Scope: analytics:read (Read analytics).

Parameters

  • minutesintegerdefault 30 · 5–60

    Query.

  • linkIdstring (uuid)

    Query.

  • domainIdstring (uuid)

    Query.

  • campaignIdstring (uuid)

    Query.

  • timezonestringdefault "UTC" · max 64 chars

    Query.

  • filtersstringmax 20000 chars

    Query. JSON array of conditions, ANDed. Each is {"dim", "op": "is" | "is_not", "values": [...]} and matches any (or, with is_not, none) of its values. Dimensions: link, domain, campaign, tag (ids), continent (AF, AS, EU, NA, OC, SA), country (ISO codes), region, city, device, os, browser, referrer, source (link or qr), utm_source, utm_medium, utm_campaign, weekday (1 = Monday … 7), hour (0–23, in timezone). Region, city, os, browser, UTM, weekday and hour need advanced analytics.

Response

200 Success. Returns AnalyticsLive.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

List chart notes

get/v1/analytics/annotations

Notes pinned to moments in a time range, oldest first.

Scope: analytics:read (Read analytics).

Parameters

  • fromstring (date-time)required

    Query.

  • tostring (date-time)required

    Query.

Response

200 Success. Returns Annotation[].

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Add a chart note

post/v1/analytics/annotations

Pin a note such as "Newsletter sent" to a moment, so spikes on the chart explain themselves. Up to 500 per workspace.

Scope: links:write (Create and edit links).

Request body (application/json)

  • atstring (date-time)required
  • labelstringrequiredmax 80 chars

Response

201 Success. Returns Annotation.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 422

    Understood but not possible, e.g. domain_unavailable, idempotency_key_reused.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Delete a chart note

delete/v1/analytics/annotations/{annotationId}

Scope: links:write (Create and edit links).

Parameters

  • annotationIdstring (uuid)required

    Path.

Response

204 Done.

Errors
  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 404

    Not found in this workspace.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Link performance

get/v1/analytics/links

Every link clicked in the period, with the previous period beside it, conversions and the last click; plus active links nobody has clicked in 30 days.

Scope: analytics:read (Read analytics).

Parameters

  • fromstring (date-time)required

    Query.

  • tostring (date-time)required

    Query.

  • timezonestringdefault "UTC" · max 64 chars

    Query.

  • domainIdstring (uuid)

    Query.

  • campaignIdstring (uuid)

    Query.

  • countrystring

    Query.

  • regionstringmax 200 chars

    Query.

  • citystringmax 200 chars

    Query.

  • devicestringmax 40 chars

    Query.

  • osstringmax 60 chars

    Query.

  • browserstringmax 60 chars

    Query.

  • referrerstringmax 253 chars

    Query.

  • sourcestringmax 40 chars

    Query.

  • utmSourcestringmax 200 chars

    Query.

  • utmMediumstringmax 200 chars

    Query.

  • utmCampaignstringmax 200 chars

    Query.

  • filtersstringmax 20000 chars

    Query. JSON array of conditions, ANDed. Each is {"dim", "op": "is" | "is_not", "values": [...]} and matches any (or, with is_not, none) of its values. Dimensions: link, domain, campaign, tag (ids), continent (AF, AS, EU, NA, OC, SA), country (ISO codes), region, city, device, os, browser, referrer, source (link or qr), utm_source, utm_medium, utm_campaign, weekday (1 = Monday … 7), hour (0–23, in timezone). Region, city, os, browser, UTM, weekday and hour need advanced analytics.

Response

200 Success. Returns AnalyticsLinks.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Conversions

Report a conversion from your server

post/v1/conversions

Attributes a conversion to the click that brought the visitor: pass the sfy_cid your landing page received. Accepted conversions are matched to their click within seconds and show up in analytics and the conversion.created webhook. Send your own id (an order id) to make retries safe.

Scope: conversions:write (Report conversions).

Parameters

  • Idempotency-Keystringmax 255 chars

    Header. Any unique string (a UUID works). Retries with the same key and body return the original response.

Request body (application/json)

  • clickIdstring (uuid)required
  • eventstringrequired
  • idstring
  • valuenumber0–1000000000000
  • currencystring
  • occurredAtstring (date-time)

Response

202 Accepted: processing in the background. Returns ConversionAccepted.

Errors
  • 400

    The request is invalid (validation_failed; see details).

  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 409

    Conflict, e.g. alias_taken, or idempotency_in_progress.

  • 422

    Understood but not possible, e.g. domain_unavailable, idempotency_key_reused.

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Meta

Inspect the API key

get/v1/api-key

The workspace and scopes of the key making the request.

Any valid API key.

Response

200 Success. Returns ApiKeyInfo.

Errors
  • 401

    Missing, invalid, revoked or expired API key.

  • 403

    The key lacks the scope this endpoint needs (insufficient_scope).

  • 429

    Rate limit reached (rate_limited). Wait Retry-After seconds.

Webhooks

Add endpoints in Developers. Each event is a JSON POST; answer with any 2xx within 10 seconds. Anything else is retried with backoff for about two days, and an endpoint that keeps failing is turned off. Delivery is at least once: use webhook-id to skip duplicates.

Verifying signatures

Requests are signed per the Standard Webhooks spec, so its libraries work as is. Or verify by hand in Node.js:

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

// secret: the endpoint's whsec_… signing secret. body: the raw request body (a string, before JSON.parse).
export function verifyShortifiesWebhook(secret, headers, body) {
  const id = headers['webhook-id'];
  const timestamp = Number(headers['webhook-timestamp']);
  // Reject old requests so a captured one can't be replayed later.
  if (!id || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = Buffer.from(createHmac('sha256', key).update(`${id}.${timestamp}.${body}`).digest('base64'));
  // Several signatures are sent while a secret rotation overlaps; any match is enough.
  return String(headers['webhook-signature'] ?? '')
    .split(' ')
    .map((s) => Buffer.from(s.replace(/^v1,/, '')))
    .some((sig) => sig.length === expected.length && timingSafeEqual(sig, expected));
}

Custom headers

An endpoint can also send up to 10 headers of your own with every delivery, such as Authorization or X-Api-Key, for receivers that check their own credentials. Set them when you add or edit the endpoint. Values are stored encrypted and never shown again. They can’t replace webhook-id, webhook-timestamp, webhook-signature, content-type or user-agent, so keep checking the signature as well.

link.created

A link was created (dashboard, API or import). Verify the webhook-signature header before trusting the body.

data

link.updated

A link’s settings changed. Verify the webhook-signature header before trusting the body.

data

  • linkLinkrequired
  • changedFieldsstring[]required

link.deleted

A link was deleted. Verify the webhook-signature header before trusting the body.

data

domain.verified

A custom domain finished setup and is serving links. Verify the webhook-signature header before trusting the body.

data

domain.disabled

A domain stopped serving links (setup failed, disabled or removed). Verify the webhook-signature header before trusting the body.

data

  • domainWebhookDomainrequired
  • reason"failed" | "disabled" | "removed"required

conversion.created

A conversion was attributed to a click. Verify the webhook-signature header before trusting the body.

data

subscription.updated

The workspace’s plan or billing status changed. Verify the webhook-signature header before trusting the body.

data

Objects

ApiKeyInfo

  • idstring (uuid)required
  • workspaceIdstring (uuid)required
  • scopes("links:read" | "links:write" | "links:delete" | "domains:read" | "analytics:read" | "conversions:write")[]required

Tag

  • idstring (uuid)required
  • namestringrequired
  • color"gray" | "blue" | "green" | "amber" | "red" | "purple" | "pink" | "teal"required
  • linkCountinteger≥ -9007199254740991

Routing

  • version1required
  • rulesobject[]requiredup to 20 items

Job

  • idstring (uuid)required
  • typestringrequired

    links_bulk_create, links_bulk_update, links_bulk_delete, link_import, link_export or qr_export.

  • status"QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED"required
  • progressintegerrequired≥ -9007199254740991

    Items processed so far.

  • totalintegerrequired≥ -9007199254740991
  • succeededintegerrequired≥ -9007199254740991
  • failedintegerrequired≥ -9007199254740991
  • filenamestring | nullrequired
  • errorstring | nullrequired

    Why the whole job failed. Per-item errors are under /items.

  • downloadUrlstring | nullrequired

    Signed link (15 minutes) to the result file, for exports.

  • createdAtstring (date-time)required
  • startedAtstring (date-time) | nullrequired
  • finishedAtstring (date-time) | nullrequired

JobItem

  • indexintegerrequired≥ -9007199254740991

    0-based item index. For CSV imports, the file line is index + 2.

  • okbooleanrequired
  • linkIdstring (uuid) | nullrequired
  • errorobject | nullrequired

Campaign

  • idstring (uuid)required
  • namestringrequired
  • descriptionstring | nullrequired
  • startsAtstring (date-time) | nullrequired
  • endsAtstring (date-time) | nullrequired
  • archivedbooleanrequired
  • linkCountintegerrequired≥ -9007199254740991
  • createdAtstring (date-time)required

Domain

  • idstring (uuid)required
  • hostnamestringrequired
  • type"PLATFORM" | "CUSTOM"required
  • status"PENDING" | "VERIFYING" | "VERIFIED" | "SSL_PENDING" | "ACTIVE" | "FAILED" | "DISABLED"required
  • activebooleanrequired
  • apexbooleanrequired
  • rootRedirectUrlstring | nullrequired
  • notFoundRedirectUrlstring | nullrequired
  • dnsRecordsobject[]required
  • dnsHealthybooleanrequired
  • lastCheckedAtstring (date-time) | nullrequired
  • lastCheckMessagestring | nullrequired
  • nextCheckAtstring (date-time) | nullrequired
  • verificationDeadlinestring (date-time) | nullrequired
  • activatedAtstring (date-time) | nullrequired
  • linkCountintegerrequired≥ -9007199254740991
  • createdAtstring (date-time)required

Analytics

  • rangeobjectrequired
  • totalsobjectrequired
  • excludedobjectrequired

    Clicks left out of every number: declared bots, and suspicious clicks (automation, missing browser language, click bursts or floods) by reason.

  • seriesobject[]required
  • countriesobject[]required
  • citiesobject[]required
  • devicesobject[]required
  • osobject[]required
  • browsersobject[]required
  • referrersobject[]required
  • sourcesobject[]required
  • topLinksobject[]required
  • topDomainsobject[]required
  • campaignsobject[]required
  • utmSourcesobject[]required
  • utmMediumsobject[]required
  • utmCampaignsobject[]required
  • conversionsobjectrequired
  • variantsobject[]required
  • regionsobject[]required

    Clicks by state or province; extra is the country code.

  • heatmapobject[]required

    Clicks by weekday (1 = Monday) and hour of day, in the requested time zone. Hours without clicks are left out.

  • revenueobjectrequired

    Conversion value by link, campaign and UTM source, per currency. Only conversions reported with a value count.

  • previousobject | nullrequired

    With compare=true: the period of the same length just before this one, with series aligned to the current buckets. partial is true when part of it is older than your analytics history.

  • locked("cities" | "regions" | "heatmap" | "os" | "browsers" | "utmSources" | "utmMediums" | "utmCampaigns" | "campaigns" | "variants")[]required

    Sections your plan doesn’t include (advanced analytics); they are returned empty.

AnalyticsLive

  • clicksobject[]required

    Counted clicks (bots and suspicious clicks left out), newest first, up to 40. Derived fields only: never an IP address or user agent.

  • perMinuteobject[]required
  • totalintegerrequired≥ -9007199254740991

Annotation

  • idstring (uuid)required
  • atstring (date-time)required
  • labelstringrequired
  • createdAtstring (date-time)required

ConversionAccepted

  • idstring (uuid)required

    The conversion id. With your own id, the same input always maps to the same value.

  • queuedtruerequired

WebhookDomain

  • idstring (uuid)required
  • hostnamestringrequired
  • type"PLATFORM" | "CUSTOM"required
  • statusstringrequired

WebhookConversion

  • idstring (uuid)required
  • eventstringrequired
  • valuenumber | nullrequired
  • currencystring | nullrequired
  • clickIdstring (uuid)required
  • linkIdstring (uuid)required
  • campaignIdstring (uuid) | nullrequired
  • occurredAtstring (date-time)required

WebhookSubscription

  • planstringrequired

    Plan in effect now.

  • subscribedPlanstring | nullrequired

    Plan paid for; differs from plan once a failed payment outlasts the grace period.

  • status"TRIALING" | "ACTIVE" | "PAST_DUE" | "UNPAID" | "CANCELED" | "INCOMPLETE" | "INCOMPLETE_EXPIRED" | "PAUSED" | nullrequired
  • interval"month" | "year" | nullrequired
  • currentPeriodEndstring (date-time) | nullrequired
  • cancelAtPeriodEndbooleanrequired
  • trialEndsAtstring (date-time) | nullrequired