Introduction
The Hootmix API lets you create, change and measure short links from your own code. It is a JSON REST API, and every request belongs to one workspace.
https://www.hootmix.com/api/v1The version is part of the path. A version never changes in a breaking way. New behavior ships in a new version, and old versions stay available until they are marked deprecated.
- Authenticate with a workspace API key in the Authorization header.
- Every response is JSON, except 204 responses which have no body.
- Requests and responses use camelCase field names and ISO 8601 dates.
Quickstart
Create a key, then shorten your first link in one request. Replace the key with your own.
curl -X POST "https://www.hootmix.com/api/v1/links" \
-H "Authorization: Bearer $HOOTMIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target":{"type":"REDIRECT","url":"https://example.com/hello"},"slug":"hello"}'The response is the link. Share its url, and read its clicks with Get link analytics.
{
"id": "6650f1c2a1b2c3d4e5f60718",
"slug": "hello",
"url": "https://hoot.mx/hello",
"status": "active"
}Authentication
Create a key in your workspace under API keys. Each key belongs to one workspace and only sees that workspace's data. Pick the scopes the key needs and an expiry date.
Send the key as a bearer token on every request:
Authorization: Bearer hmx_live_ab12cd34_<secret>The full key is shown only once, when you create it. Hootmix keeps a fingerprint, not the key, so a lost key cannot be recovered. Create a new one and revoke the old one.
- links.read: read links.
- links.write: create and change links.
- links.delete: disable and delete links.
- analytics.read: read link statistics.
- events.read: read click events and tags.
- events.write: tag click events.
- folders.read: list folders.
- domains.read: list domains.
A key can only hold scopes that its creator has in the workspace. Keys can have an expiry date, and revoked or expired keys return 401.
Tagging events
Tags label single clicks, not links. Use them to record what happened after a click, such as converted, vip or refund, and then look at those clicks in your analytics.
- A visitor clicks a short link. Hootmix records a click event and, if you use webhooks, sends a link.clicked request to your server.
- The request holds the event id in data.event.id.
- Your server calls PUT /v1/events/{id}/tags with the tags to set.
- The tags appear under the click in the link's recent visits, on the Tags page, and in analytics. Filter by tag with the tag parameter.
{
"id": "9d2f6c1e-...",
"event": "link.clicked",
"createdAt": "2026-10-19T08:31:12.000Z",
"data": {
"link": { "id": "6650f1c2a1b2c3d4e5f60718", "slug": "spring-sale" },
"event": { "id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "countryCode": "ID", "device": "Mobile" }
}
}curl -X PUT "https://www.hootmix.com/api/v1/events/b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d/tags" \
-H "Authorization: Bearer $HOOTMIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tags":["converted"]}'You can also tag a click by hand: open a link, go to Recent visits and use the tag button under the click. Create, rename and delete tag names on the Tags page.
Rate limits
Each API key can make a limited number of requests in a time window. The limit is set by the Hootmix administrator and applies to every key equally. Every response tells you where you stand:
- X-RateLimit-Limit: requests allowed per window.
- X-RateLimit-Remaining: requests left in this window.
- X-RateLimit-Reset: seconds until the window resets.
When you run out, the API answers 429 with a Retry-After header holding the seconds to wait. Back off and retry after that time.
Errors
Errors use the HTTP status code and a JSON body with a stable code and a readable message. Match on the code, not on the message.
{
"error": {
"code": "not_found",
"message": "Link not found"
}
}- invalid_request: a field is missing or malformed.
- folder_ambiguous: you sent both folderId and folderName.
- invalid_api_key: the key is missing, wrong, expired or revoked.
- insufficient_scope: the key lacks the scope this endpoint needs.
- not_found / folder_not_found: the link or folder does not exist in this workspace.
- folder_name_ambiguous / slug_taken: the request clashes with existing data.
- unprocessable: a plan limit or rule blocked the request.
- rate_limited: slow down and retry.
Pagination
List endpoints return the newest items first. Use limit (1 to 100, default 20) and pass the cursor.next from one response as cursor in the next request. When cursor.next is null there are no more items.
{
"data": [ ... ],
"cursor": { "next": "6650f1c2a1b2c3d4e5f60718" }
}/v1/linksCreate a link
Creates a short link. Leave out the domain to use the first active domain and the slug to get a random one.
Request body
targetobjectRequiredWhere the link goes. Required. Pick a type and fill its properties.
typestringRequiredHow visitors are routed. Pick one of the redirect types below.
One ofREDIRECTDEVICESMARTSPLIT
When type is REDIRECT. Sends every visitor to one URL.
urlstringURLRequiredThe destination. An HTTP or HTTPS URL.
statusCodeintegerOptionalThe HTTP status used to redirect. 301 and 308 are permanent, 302 and 307 are temporary.
One of301302307308Default302domainIdstringIDOptionalThe domain to create the link on. See List domains. Defaults to the first active domain.
slugstringSlugOptionalThe path of the short link. Lowercase letters, numbers and dashes. A random one is generated when left out.
Max64folderIdstringIDOptionalThe id of a folder. Send either folderId or folderName, never both.
folderobjectOptionalnamestringRequiredMax60
Response
201The response body:
idstringIDUnique id of the link.
workspaceIdstringIDThe workspace that owns the link.
domainIdstringIDThe domain the link lives on.
domainobjectThe domain the link lives on.
hostnamestringThe hostname, for example hoot.mx.
userIdstringIDoptionalThe user who created the link, when known.
folderIdstringIDoptionalnullableThe folder the link is in, or null.
slugstringSlugThe path after the domain.
statusstringactive links redirect. disabled links stop redirecting but keep their analytics.
One ofactivedisabledcreatedAtstringISO 8601When the link was created.
modestringThe redirect type. It is the same as target.type and cannot change.
One ofREDIRECTDEVICESMARTSPLITROTATORtargetobjectWhere the link sends visitors. It is an object whose type decides the other properties.
typestringHow visitors are routed. Pick one of the redirect types below.
One ofREDIRECTDEVICESMARTSPLIT
When type is REDIRECT. Sends every visitor to one URL.
urlstringURLThe destination. An HTTP or HTTPS URL.
statusCodeintegerThe HTTP status used to redirect.
One of301302307308configurationobjectExtra settings of the link. You can read them here; they are changed in the dashboard.
forwardingobjectPassing query parameters of the short link on to the destination.
isActivebooleanWhether forwarding is on.
modestringALL forwards every parameter. ALLOWLIST forwards only the keys below.
One ofALLALLOWLISTkeysarray<string>The parameters forwarded in ALLOWLIST mode.
overwritebooleanWhether forwarded parameters replace the ones already on the destination.
rulesobjectRedirect rules.
timezonestringlistarray<object>idstringUnique id of the link.
destinationstringURL
protectionobjectAccess protection.
passwordbooleanWhether the link asks for a password. The password itself is never returned.
cloakingobjectoptionalLink preview settings, present when configured.
isActivebooleanWhether the custom preview is on.
urlstringURLThe full short link, ready to share.
Notes
- Send either folderId or folderName, never both. An unknown folder name returns 404, and a name shared by several folders returns 409.
- The redirect type and the number of links must fit your plan, otherwise the API returns 422.
Errors
- 400The request is malformed, or both folderId and folderName were sent.
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 404The link or folder was not found in this workspace.
- 409The slug is taken, or a folder name matches more than one folder.
- 422A plan limit or rule blocked the request.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
POST /v1/linkscurl -X POST "https://www.hootmix.com/api/v1/links" \
-H "Authorization: Bearer $HOOTMIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target":{"type":"REDIRECT","url":"https://example.com/spring-sale"},"slug":"spring-sale","folder":{"name":"Campaigns"}}'201 Created
{
"id": "6650f1c2a1b2c3d4e5f60718",
"workspaceId": "6650f0aaa1b2c3d4e5f60001",
"domainId": "6650f0bbb1b2c3d4e5f60002",
"domain": {
"hostname": "hoot.mx"
},
"folderId": "6650f0ccc1b2c3d4e5f60003",
"slug": "spring-sale",
"status": "active",
"createdAt": "2026-10-19T08:30:00.000Z",
"mode": "REDIRECT",
"target": {
"type": "REDIRECT",
"url": "https://example.com/spring-sale"
},
"statusCode": 302,
"configuration": {
"forwarding": {
"isActive": false,
"mode": "ALL",
"keys": [],
"overwrite": false
},
"rules": {
"list": [],
"timezone": "Asia/Jakarta"
},
"protection": {
"password": false
}
},
"url": "https://hoot.mx/spring-sale"
}Query parameters
limitintegerOptionalHow many links to return.
Default20Min1Max100cursorstringIDOptionalThe cursor.next from the previous page.
folderIdstringIDOptionalOnly links in this folder. Send either folderId or folderName, never both.
folderNamestringOptionalOnly links in the folder with this name. Send either folderId or folderName, never both.
Max60
Response
200The response body:
dataarray<object>idstringIDUnique id of the link.
workspaceIdstringIDThe workspace that owns the link.
domainIdstringIDThe domain the link lives on.
domainobjectThe domain the link lives on.
hostnamestringThe hostname, for example hoot.mx.
userIdstringIDoptionalThe user who created the link, when known.
folderIdstringIDoptionalnullableThe folder the link is in, or null.
slugstringSlugThe path after the domain.
statusstringactive links redirect. disabled links stop redirecting but keep their analytics.
One ofactivedisabledcreatedAtstringISO 8601When the link was created.
modestringThe redirect type. It is the same as target.type and cannot change.
One ofREDIRECTDEVICESMARTSPLITROTATORtargetobjectWhere the link sends visitors. It is an object whose type decides the other properties.
typestringHow visitors are routed. Pick one of the redirect types below.
One ofREDIRECTDEVICESMARTSPLIT
When type is REDIRECT. Sends every visitor to one URL.
urlstringURLThe destination. An HTTP or HTTPS URL.
statusCodeintegerThe HTTP status used to redirect.
One of301302307308configurationobjectExtra settings of the link. You can read them here; they are changed in the dashboard.
forwardingobjectPassing query parameters of the short link on to the destination.
isActivebooleanWhether forwarding is on.
modestringALL forwards every parameter. ALLOWLIST forwards only the keys below.
One ofALLALLOWLISTkeysarray<string>The parameters forwarded in ALLOWLIST mode.
overwritebooleanWhether forwarded parameters replace the ones already on the destination.
rulesobjectRedirect rules.
timezonestringlistarray<object>idstringUnique id of the link.
destinationstringURL
protectionobjectAccess protection.
passwordbooleanWhether the link asks for a password. The password itself is never returned.
cloakingobjectoptionalLink preview settings, present when configured.
isActivebooleanWhether the custom preview is on.
urlstringURLThe full short link, ready to share.
nextCursorstringnullable
Notes
- Pass cursor.next as cursor to get the next page. It is null on the last page.
Errors
- 400The request is malformed, or both folderId and folderName were sent.
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 404The link or folder was not found in this workspace.
- 409The slug is taken, or a folder name matches more than one folder.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
GET /v1/linkscurl -X GET "https://www.hootmix.com/api/v1/links?limit=20" \
-H "Authorization: Bearer $HOOTMIX_API_KEY"200 OK
{
"data": [
{
"id": "6650f1c2a1b2c3d4e5f60718",
"workspaceId": "6650f0aaa1b2c3d4e5f60001",
"domainId": "6650f0bbb1b2c3d4e5f60002",
"domain": {
"hostname": "hoot.mx"
},
"folderId": "6650f0ccc1b2c3d4e5f60003",
"slug": "spring-sale",
"status": "active",
"createdAt": "2026-10-19T08:30:00.000Z",
"mode": "REDIRECT",
"target": {
"type": "REDIRECT",
"url": "https://example.com/spring-sale"
},
"statusCode": 302,
"configuration": {
"forwarding": {
"isActive": false,
"mode": "ALL",
"keys": [],
"overwrite": false
},
"rules": {
"list": [],
"timezone": "Asia/Jakarta"
},
"protection": {
"password": false
}
},
"url": "https://hoot.mx/spring-sale"
}
],
"cursor": {
"next": null
}
}Path parameters
idstringIDRequiredThe id of the link.
Response
200The response body:
idstringIDUnique id of the link.
workspaceIdstringIDThe workspace that owns the link.
domainIdstringIDThe domain the link lives on.
domainobjectThe domain the link lives on.
hostnamestringThe hostname, for example hoot.mx.
userIdstringIDoptionalThe user who created the link, when known.
folderIdstringIDoptionalnullableThe folder the link is in, or null.
slugstringSlugThe path after the domain.
statusstringactive links redirect. disabled links stop redirecting but keep their analytics.
One ofactivedisabledcreatedAtstringISO 8601When the link was created.
modestringThe redirect type. It is the same as target.type and cannot change.
One ofREDIRECTDEVICESMARTSPLITROTATORtargetobjectWhere the link sends visitors. It is an object whose type decides the other properties.
typestringHow visitors are routed. Pick one of the redirect types below.
One ofREDIRECTDEVICESMARTSPLIT
When type is REDIRECT. Sends every visitor to one URL.
urlstringURLThe destination. An HTTP or HTTPS URL.
statusCodeintegerThe HTTP status used to redirect.
One of301302307308configurationobjectExtra settings of the link. You can read them here; they are changed in the dashboard.
forwardingobjectPassing query parameters of the short link on to the destination.
isActivebooleanWhether forwarding is on.
modestringALL forwards every parameter. ALLOWLIST forwards only the keys below.
One ofALLALLOWLISTkeysarray<string>The parameters forwarded in ALLOWLIST mode.
overwritebooleanWhether forwarded parameters replace the ones already on the destination.
rulesobjectRedirect rules.
timezonestringlistarray<object>idstringUnique id of the link.
destinationstringURL
protectionobjectAccess protection.
passwordbooleanWhether the link asks for a password. The password itself is never returned.
cloakingobjectoptionalLink preview settings, present when configured.
isActivebooleanWhether the custom preview is on.
urlstringURLThe full short link, ready to share.
Errors
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 404The link or folder was not found in this workspace.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
GET /v1/links/{id}curl -X GET "https://www.hootmix.com/api/v1/links/6650f1c2a1b2c3d4e5f60718" \
-H "Authorization: Bearer $HOOTMIX_API_KEY"200 OK
{
"id": "6650f1c2a1b2c3d4e5f60718",
"workspaceId": "6650f0aaa1b2c3d4e5f60001",
"domainId": "6650f0bbb1b2c3d4e5f60002",
"domain": {
"hostname": "hoot.mx"
},
"folderId": "6650f0ccc1b2c3d4e5f60003",
"slug": "spring-sale",
"status": "active",
"createdAt": "2026-10-19T08:30:00.000Z",
"mode": "REDIRECT",
"target": {
"type": "REDIRECT",
"url": "https://example.com/spring-sale"
},
"statusCode": 302,
"configuration": {
"forwarding": {
"isActive": false,
"mode": "ALL",
"keys": [],
"overwrite": false
},
"rules": {
"list": [],
"timezone": "Asia/Jakarta"
},
"protection": {
"password": false
}
},
"url": "https://hoot.mx/spring-sale"
}/v1/links/{id}Update a link
Changes the destination or settings of a link. Send only what you want to change.
Path parameters
idstringIDRequiredThe id of the link.
Request body
targetobjectOptionalThe new destination. Its type must be the link's current type.
typestringRequiredHow visitors are routed. Pick one of the redirect types below.
One ofREDIRECTDEVICESMARTSPLIT
When type is REDIRECT. Sends every visitor to one URL.
urlstringURLRequiredThe destination. An HTTP or HTTPS URL.
statusCodeintegerOptionalThe new redirect status.
One of301302307308statusstringOptionalUse disabled to stop the link from redirecting, and active to turn it back on.
One ofactivedisabledfolderIdstringIDOptionalnullableMove the link to this folder, or send null to take it out of its folder. Send either folderId or folderName, never both.
folderobjectOptionalnamestringRequiredMax60
Response
200The response body:
idstringIDUnique id of the link.
workspaceIdstringIDThe workspace that owns the link.
domainIdstringIDThe domain the link lives on.
domainobjectThe domain the link lives on.
hostnamestringThe hostname, for example hoot.mx.
userIdstringIDoptionalThe user who created the link, when known.
folderIdstringIDoptionalnullableThe folder the link is in, or null.
slugstringSlugThe path after the domain.
statusstringactive links redirect. disabled links stop redirecting but keep their analytics.
One ofactivedisabledcreatedAtstringISO 8601When the link was created.
modestringThe redirect type. It is the same as target.type and cannot change.
One ofREDIRECTDEVICESMARTSPLITROTATORtargetobjectWhere the link sends visitors. It is an object whose type decides the other properties.
typestringHow visitors are routed. Pick one of the redirect types below.
One ofREDIRECTDEVICESMARTSPLIT
When type is REDIRECT. Sends every visitor to one URL.
urlstringURLThe destination. An HTTP or HTTPS URL.
statusCodeintegerThe HTTP status used to redirect.
One of301302307308configurationobjectExtra settings of the link. You can read them here; they are changed in the dashboard.
forwardingobjectPassing query parameters of the short link on to the destination.
isActivebooleanWhether forwarding is on.
modestringALL forwards every parameter. ALLOWLIST forwards only the keys below.
One ofALLALLOWLISTkeysarray<string>The parameters forwarded in ALLOWLIST mode.
overwritebooleanWhether forwarded parameters replace the ones already on the destination.
rulesobjectRedirect rules.
timezonestringlistarray<object>idstringUnique id of the link.
destinationstringURL
protectionobjectAccess protection.
passwordbooleanWhether the link asks for a password. The password itself is never returned.
cloakingobjectoptionalLink preview settings, present when configured.
isActivebooleanWhether the custom preview is on.
urlstringURLThe full short link, ready to share.
Notes
- The redirect type of a link cannot change. Create a new link instead.
- Changing the destination does not change the short link or its analytics.
Errors
- 400The request is malformed, or both folderId and folderName were sent.
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 404The link or folder was not found in this workspace.
- 409The slug is taken, or a folder name matches more than one folder.
- 422A plan limit or rule blocked the request.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
PATCH /v1/links/{id}curl -X PATCH "https://www.hootmix.com/api/v1/links/6650f1c2a1b2c3d4e5f60718" \
-H "Authorization: Bearer $HOOTMIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target":{"type":"REDIRECT","url":"https://example.com/autumn-sale"},"statusCode":301}'200 OK
{
"id": "6650f1c2a1b2c3d4e5f60718",
"workspaceId": "6650f0aaa1b2c3d4e5f60001",
"domainId": "6650f0bbb1b2c3d4e5f60002",
"domain": {
"hostname": "hoot.mx"
},
"folderId": "6650f0ccc1b2c3d4e5f60003",
"slug": "spring-sale",
"status": "active",
"createdAt": "2026-10-19T08:30:00.000Z",
"mode": "REDIRECT",
"target": {
"type": "REDIRECT",
"url": "https://example.com/autumn-sale"
},
"statusCode": 301,
"configuration": {
"forwarding": {
"isActive": false,
"mode": "ALL",
"keys": [],
"overwrite": false
},
"rules": {
"list": [],
"timezone": "Asia/Jakarta"
},
"protection": {
"password": false
}
},
"url": "https://hoot.mx/spring-sale"
}/v1/links/{id}Delete a link
Disables the link, so it stops redirecting but keeps its analytics. Add hard=true to remove it for good.
Path parameters
idstringIDRequiredThe id of the link.
Query parameters
hardbooleanOptionalSet to true to delete the link together with its analytics. This cannot be undone.
Defaultfalse
Response
204Returns an empty response.
Notes
- Both forms answer 204 with no body.
- A disabled link can be turned on again with Update a link.
Errors
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 404The link or folder was not found in this workspace.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
DELETE /v1/links/{id}curl -X DELETE "https://www.hootmix.com/api/v1/links/6650f1c2a1b2c3d4e5f60718" \
-H "Authorization: Bearer $HOOTMIX_API_KEY"204 No Content
No response body/v1/links/{id}/statsGet link analytics
Returns clicks, visitors, a time series and breakdowns for one link.
Path parameters
idstringIDRequiredThe id of the link.
Query parameters
daysintegerOptionalThe range to report.
One of173090Default7tagstringOptionalOnly count events tagged with this tag.
Max60
Response
200The response body:
visitsintegerTotal clicks in the period.
visitorsobjectVisitor counts.
uniqueintegerDistinct visitors in the period.
tagsarray<object>Clicks grouped by the tags they carry. A click with two tags counts under both.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
seriesarray<object>Clicks over time. One point per hour for a 1 day range, otherwise one per day.
timestringISO 8601Start of the period.
availablebooleanFalse for periods before analytics were recorded.
visitsintegerClicks in that period.
breakdownsobjectClicks grouped by common dimensions. Each list is sorted by count.
sourcesarray<object>Referring site, or Direct / unknown.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
devicesarray<object>Desktop, Mobile or Tablet.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
browsersarray<object>Browser name.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
outcomesarray<object>For smart links, how visitors were sent on.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
attemptsarray<object>For smart links, the destinations that were tried.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
openingsarray<object>For smart links, whether the app seemed to open.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
detailsobjectMore dimensions, each list limited to the top 10.
geoobjectWhere visitors are.
countryCodesarray<object>Clicks per country code.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
regionsarray<object>Clicks per country and region.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
citiesarray<object>Clicks per country and city.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
agentobjectWhat visitors use.
osarray<object>Operating system name.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
referrerobjectWhere visitors came from.
urlsarray<object>Referring pages without query strings.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
utmobjectCampaign parameters on the short link.
sourcearray<object>Values of utm_source.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
mediumarray<object>Values of utm_medium.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
campaignarray<object>Values of utm_campaign.
_idstringThe value being counted, for example a browser name.
countintegerNumber of clicks.
cutoffstringISO 8601The end of the period, which is when the stats were made.
historyobjectWhether the period is fully recorded.
sincestringISO 8601nullableWhen analytics recording began, or null when the whole period is covered.
isPartialbooleanTrue when the period starts before analytics were recorded.
Notes
- How many days you can ask for depends on your plan. Longer ranges return 422.
- Tags belong to click events, so tagging an event later changes these numbers.
Errors
- 400The request is malformed, or both folderId and folderName were sent.
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 404The link or folder was not found in this workspace.
- 422A plan limit or rule blocked the request.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
GET /v1/links/{id}/statscurl -X GET "https://www.hootmix.com/api/v1/links/6650f1c2a1b2c3d4e5f60718/stats?days=7" \
-H "Authorization: Bearer $HOOTMIX_API_KEY"200 OK
{
"visits": 128,
"visitors": {
"unique": 97
},
"tags": [
{
"_id": "launch",
"count": 128
}
],
"series": [
{
"time": "2026-10-12T08:30:00.000Z",
"available": true,
"visits": 14
},
{
"time": "2026-10-13T08:30:00.000Z",
"available": true,
"visits": 21
}
],
"breakdowns": {
"sources": [
{
"_id": "Direct / unknown",
"count": 80
}
],
"devices": [
{
"_id": "Mobile",
"count": 90
}
],
"browsers": [
{
"_id": "Chrome",
"count": 70
}
],
"outcomes": [],
"attempts": [],
"openings": []
},
"details": {
"geo": {
"countryCodes": [
{
"_id": "ID",
"count": 90
}
],
"regions": [],
"cities": []
},
"agent": {
"os": [
{
"_id": "Android",
"count": 60
}
]
},
"referrer": {
"urls": []
},
"utm": {
"source": [
{
"_id": "newsletter",
"count": 40
}
],
"medium": [],
"campaign": []
}
},
"cutoff": "2026-10-19T08:30:00.000Z",
"history": {
"since": null,
"isPartial": false
}
}/v1/eventsList events
Returns click events, newest first. Bots are not included.
Query parameters
limitintegerOptionalHow many events to return.
Default20Min1Max100cursorstringOptionalThe cursor.next from the previous page.
linkIdstringIDOptionalOnly events of this link.
tagstringOptionalOnly events with this tag.
Max60
Response
200The response body:
dataarray<object>idstringUnique id of the event. Use it to read the event and to tag it.
linkIdstringIDThe link that was clicked.
createdAtstringISO 8601When the click happened.
tagsarray<string>Tags on this event. Empty until you tag it.
Max10devicestringnullableDesktop, Mobile or Tablet.
browserstringnullableBrowser name.
osstringnullableOperating system name.
referrerstringURLnullableThe page the visitor came from, without a query string.
geoobjectWhere the visitor is.
countryCodestringnullableISO 3166-1 alpha-2 country code.
regionstringnullableRegion.
citystringnullableCity.
visitorobjectThe visitor behind the click.
idstringnullableAnonymous id of the visitor, kept in a first-party cookie.
isUniquebooleannullableTrue the first time this visitor clicked this link.
utmobjectCampaign parameters on the short link.
sourcestringnullableValue of utm_source.
mediumstringnullableValue of utm_medium.
campaignstringnullableValue of utm_campaign.
nextCursorstringnullable
Notes
- The id of each event is the id the click webhook sends.
- Pass cursor.next as cursor to get the next page. It is null on the last page.
Errors
- 400The request is malformed, or both folderId and folderName were sent.
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
GET /v1/eventscurl -X GET "https://www.hootmix.com/api/v1/events?limit=20" \
-H "Authorization: Bearer $HOOTMIX_API_KEY"200 OK
{
"data": [
{
"id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"linkId": "6650f1c2a1b2c3d4e5f60718",
"createdAt": "2026-10-19T08:31:12.000Z",
"tags": [
"converted"
],
"device": "Mobile",
"browser": "Chrome",
"os": "Android",
"referrer": "https://newsletter.example.com/issue-42",
"geo": {
"countryCode": "ID",
"region": "JK",
"city": "Jakarta"
},
"visitor": {
"id": "5a0e9d1c-3b7f-4c52-9a1e-2f6d8b4c7e10",
"isUnique": true
},
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "spring"
}
}
],
"cursor": {
"next": null
}
}/v1/events/{id}Get an event
Returns one click event by the id the click webhook sends.
Path parameters
idstringRequiredThe event id. It is data.event.id in the click webhook.
Response
200The response body:
idstringUnique id of the event. Use it to read the event and to tag it.
linkIdstringIDThe link that was clicked.
createdAtstringISO 8601When the click happened.
tagsarray<string>Tags on this event. Empty until you tag it.
Max10devicestringnullableDesktop, Mobile or Tablet.
browserstringnullableBrowser name.
osstringnullableOperating system name.
referrerstringURLnullableThe page the visitor came from, without a query string.
geoobjectWhere the visitor is.
countryCodestringnullableISO 3166-1 alpha-2 country code.
regionstringnullableRegion.
citystringnullableCity.
visitorobjectThe visitor behind the click.
idstringnullableAnonymous id of the visitor, kept in a first-party cookie.
isUniquebooleannullableTrue the first time this visitor clicked this link.
utmobjectCampaign parameters on the short link.
sourcestringnullableValue of utm_source.
mediumstringnullableValue of utm_medium.
campaignstringnullableValue of utm_campaign.
Errors
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 404The link or folder was not found in this workspace.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
GET /v1/events/{id}curl -X GET "https://www.hootmix.com/api/v1/events/b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" \
-H "Authorization: Bearer $HOOTMIX_API_KEY"200 OK
{
"id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"linkId": "6650f1c2a1b2c3d4e5f60718",
"createdAt": "2026-10-19T08:31:12.000Z",
"tags": [
"converted"
],
"device": "Mobile",
"browser": "Chrome",
"os": "Android",
"referrer": "https://newsletter.example.com/issue-42",
"geo": {
"countryCode": "ID",
"region": "JK",
"city": "Jakarta"
},
"visitor": {
"id": "5a0e9d1c-3b7f-4c52-9a1e-2f6d8b4c7e10",
"isUnique": true
},
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "spring"
}
}/v1/foldersList folders
Returns every folder in the workspace. Use parentId to rebuild the tree.
Response
200The response body:
dataarray<object>idstringIDUnique id of the folder.
parentIdstringIDnullableThe parent folder, or null at the top level.
namestringThe folder name.
Errors
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
GET /v1/folderscurl -X GET "https://www.hootmix.com/api/v1/folders" \
-H "Authorization: Bearer $HOOTMIX_API_KEY"200 OK
{
"data": [
{
"id": "6650f0ccc1b2c3d4e5f60003",
"parentId": null,
"name": "Campaigns"
},
{
"id": "6650f0ddd1b2c3d4e5f60004",
"parentId": "6650f0ccc1b2c3d4e5f60003",
"name": "Autumn"
}
]
}Response
200The response body:
dataarray<object>idstringIDUnique id of the domain.
hostnamestringThe hostname.
kindstringGLOBAL domains are shared and managed by Hootmix. WORKSPACE is reserved for domains owned by a workspace.
One ofGLOBALWORKSPACE
Notes
- Domains are managed by the Hootmix administrator for now. Adding your own domain through the API is planned.
Errors
- 401The API key is missing, invalid, expired or revoked.
- 403The API key does not have the scope this endpoint needs.
- 429The key exceeded its rate limit. Wait for Retry-After seconds.
GET /v1/domainscurl -X GET "https://www.hootmix.com/api/v1/domains" \
-H "Authorization: Bearer $HOOTMIX_API_KEY"200 OK
{
"data": [
{
"id": "6650f0bbb1b2c3d4e5f60002",
"hostname": "hoot.mx",
"kind": "GLOBAL"
}
]
}Webhooks
Webhooks send an HTTP POST to your server when something happens in your workspace, so you do not have to poll the API. Add an endpoint under Webhooks in your workspace, pick the events you want, and optionally limit it to some folders. A folder filter also matches the subfolders.
Every request has a JSON body with the same envelope. What is inside data depends on the event.
{
"id": "9d2f6c1e-7a4b-4e0d-8f53-2c1b6a9e4d70",
"event": "link.clicked",
"createdAt": "2026-10-19T08:31:12.000Z",
"data": { }
}Each request also carries these headers:
- X-Hootmix-Event: the event name.
- X-Hootmix-Delivery: the id of this delivery attempt.
- X-Hootmix-Signature: sha256= followed by the HMAC SHA-256 of the raw request body, made with your signing secret.
- User-Agent: Hootmix-Webhook/1.0
Check the signature before you trust a request. The signing secret is shown once, when you create the endpoint or rotate its secret. Compute the HMAC over the raw body, not over parsed JSON.
import { createHmac, timingSafeEqual } from "node:crypto";
const verify = (rawBody, header, secret) => {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
};import hashlib
import hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(header, expected)function verify(string $rawBody, string $header, string $secret): bool {
$expected = "sha256=" . hash_hmac("sha256", $rawBody, $secret);
return hash_equals($expected, $header);
}Delivery:
- Answer with any 2xx status within 5 seconds. Any other status, or no answer, counts as a failure.
- A failed delivery is retried after 30 seconds, 2 minutes, 8 minutes and 32 minutes. After the fifth failed attempt it is marked failed. The recent deliveries of each endpoint are listed in your workspace.
- The id in the body stays the same across retries and across your endpoints, so use it to ignore duplicates.
- Redirects are not followed, and the endpoint must be a public HTTP or HTTPS address.
- Use Send test on an endpoint to send a webhook.test notification.
Payload
idstringUnique id of this notification. It stays the same across retries and across your endpoints, so use it to ignore duplicates.
eventstringThe click.
One oflink.clickedcreatedAtstringISO 8601When the notification was created.
dataobjectWhat happened. The properties depend on the event.
linkobjectThe link that was clicked.
idstringIDThe link id.
urlstringURLThe short link.
slugstringThe path after the domain.
eventobjectThe click.
idstringThe id of the click event. Use it with the API to read or tag the event.
visitorIdstringAnonymous id of the visitor, kept in a first-party cookie.
isUniquebooleanTrue the first time this visitor clicked this link.
countryCodestringnullableISO 3166-1 alpha-2 country code, when known.
regionstringnullableRegion, when known.
citystringnullableCity, when known.
devicestringnullableDesktop, Mobile or Tablet.
browserstringnullableBrowser name.
osstringnullableOperating system name.
referrerstringURLnullableThe page the visitor came from, without a query string.
utmobjectCampaign parameters on the short link.
sourcestringnullableValue of utm_source.
mediumstringnullableValue of utm_medium.
campaignstringnullableValue of utm_campaign.
Notes
- data.event.id is the id of the click event. Use it with PUT /v1/events/{id}/tags to tag the click, or GET /v1/events/{id} to read it.
- Create the endpoint with a folder filter to receive clicks of links in those folders only.
POST your endpoint{
"id": "9d2f6c1e-7a4b-4e0d-8f53-2c1b6a9e4d70",
"event": "link.clicked",
"createdAt": "2026-10-19T08:31:12.000Z",
"data": {
"link": {
"id": "6650f1c2a1b2c3d4e5f60718",
"url": "https://hoot.mx/spring-sale",
"slug": "spring-sale"
},
"event": {
"id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"visitorId": "5a0e9d1c-3b7f-4c52-9a1e-2f6d8b4c7e10",
"isUnique": true,
"countryCode": "ID",
"region": "JK",
"city": "Jakarta",
"device": "Mobile",
"browser": "Chrome",
"os": "Android",
"referrer": "https://newsletter.example.com/issue-42",
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "spring"
}
}
}
}Payload
idstringUnique id of this notification. It stays the same across retries and across your endpoints, so use it to ignore duplicates.
eventstringThe event name. It is also in the X-Hootmix-Event header.
One oflink.createdcreatedAtstringISO 8601When the notification was created.
dataobjectWhat happened. The properties depend on the event.
linkobjectThe link this is about.
idstringIDThe link id.
urlstringURLThe short link.
slugstringThe path after the domain.
modestringThe redirect type.
One ofREDIRECTDEVICESMARTSPLITstatusstringactive or disabled.
One ofactivedisabledfolderIdstringIDnullableThe folder the link is in, or null.
Notes
- Importing links sends one notification per link.
POST your endpoint{
"id": "9d2f6c1e-7a4b-4e0d-8f53-2c1b6a9e4d70",
"event": "link.created",
"createdAt": "2026-10-19T08:31:12.000Z",
"data": {
"link": {
"id": "6650f1c2a1b2c3d4e5f60718",
"url": "https://hoot.mx/spring-sale",
"slug": "spring-sale",
"mode": "REDIRECT",
"status": "active",
"folderId": "6650f0ccc1b2c3d4e5f60003"
}
}
}Payload
idstringUnique id of this notification. It stays the same across retries and across your endpoints, so use it to ignore duplicates.
eventstringThe event name. It is also in the X-Hootmix-Event header.
One oflink.updatedcreatedAtstringISO 8601When the notification was created.
dataobjectWhat happened. The properties depend on the event.
linkobjectThe link this is about.
idstringIDThe link id.
urlstringURLThe short link.
slugstringThe path after the domain.
modestringThe redirect type.
One ofREDIRECTDEVICESMARTSPLITstatusstringactive or disabled.
One ofactivedisabledfolderIdstringIDnullableThe folder the link is in, or null.
POST your endpoint{
"id": "9d2f6c1e-7a4b-4e0d-8f53-2c1b6a9e4d70",
"event": "link.updated",
"createdAt": "2026-10-19T08:31:12.000Z",
"data": {
"link": {
"id": "6650f1c2a1b2c3d4e5f60718",
"url": "https://hoot.mx/spring-sale",
"slug": "spring-sale",
"mode": "REDIRECT",
"status": "disabled",
"folderId": "6650f0ccc1b2c3d4e5f60003"
}
}
}link.deleted
A link was deleted. Only its id and slug are sent, because the link no longer exists.
Payload
idstringUnique id of this notification. It stays the same across retries and across your endpoints, so use it to ignore duplicates.
eventstringThe event name. It is also in the X-Hootmix-Event header.
One oflink.deletedcreatedAtstringISO 8601When the notification was created.
dataobjectWhat happened. The properties depend on the event.
linkobjectThe link this is about.
idstringIDThe link id.
slugstringThe path after the domain.
POST your endpoint{
"id": "9d2f6c1e-7a4b-4e0d-8f53-2c1b6a9e4d70",
"event": "link.deleted",
"createdAt": "2026-10-19T08:31:12.000Z",
"data": {
"link": {
"id": "6650f1c2a1b2c3d4e5f60718",
"slug": "spring-sale"
}
}
}Payload
idstringUnique id of this notification. It stays the same across retries and across your endpoints, so use it to ignore duplicates.
eventstringThe event name. It is also in the X-Hootmix-Event header.
One ofwebhook.testcreatedAtstringISO 8601When the notification was created.
dataobjectEmpty for a test notification.
POST your endpoint{
"id": "9d2f6c1e-7a4b-4e0d-8f53-2c1b6a9e4d70",
"event": "webhook.test",
"createdAt": "2026-10-19T08:31:12.000Z",
"data": {}
}