Skip to content

Commit f61bef4

Browse files
committed
feat: link tags
Tags are live on the API (spoo v2.4.0): a tags collection with color and icon, tag_ids on link create and update, tag filters on the link list, tag slices on aggregate stats and exports, and a bulk tag call. This adds a tags resource plus those fields to the existing methods, and refreshes openapi.json so the generated schema carries the new shapes.
1 parent 691e155 commit f61bef4

11 files changed

Lines changed: 4075 additions & 476 deletions

File tree

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,29 @@ const result = await spoo.links.bulk.setStatus(ids, "INACTIVE");
8686
console.log(result.summary); // { total, succeeded, failed }
8787
```
8888

89+
## Tags
90+
91+
Tags are labels you attach to links, at most 10 per link. Links point at
92+
tags by id, so renaming a tag shows up on every link at once.
93+
94+
```ts
95+
const launch = await spoo.tags.create({ name: "launch", color: "violet", icon: "rocket" });
96+
97+
await spoo.links.create({ long_url: "https://example.com/launch", tag_ids: [launch.id] });
98+
await spoo.links.update(id, { tag_ids: [launch.id] }); // replaces the list; [] or null clears it
99+
100+
const tagged = await spoo.links.list({
101+
filter: { tagNames: ["launch", "q3"], tagsMatch: "all" },
102+
});
103+
104+
await spoo.links.bulk.tags(ids, { add: [launch.id], remove: [oldTag.id] });
105+
```
106+
107+
Every link carries its `tags` (id, name, color, icon). `spoo.tags.list()`
108+
returns every tag with its link count, and `spoo.tags.delete(id)` removes the
109+
tag from every link that had it. Stats calls take `tag` (names) or `tagId`
110+
(ids) to slice clicks to tagged links; see [Analytics](#analytics).
111+
89112
## Pagination
90113

91114
Every list is a `Page`: use it directly, walk it by hand, or iterate items
@@ -105,6 +128,7 @@ const stats = await spoo.stats.get({
105128
startDate: new Date("2026-01-01"),
106129
groupBy: ["time", "country"],
107130
device: ["mobile"],
131+
tag: ["launch"],
108132
timezone: "Asia/Kolkata",
109133
});
110134

@@ -230,7 +254,7 @@ naming the endpoint so it gets a typed method.
230254

231255
The SDK covers the third-party integration surface of the API: identity
232256
read (`auth.me`), Sign in with Spoo, and the full data plane, meaning
233-
shortening, link management, claims, bulk operations, analytics, file
257+
shortening, link management, tags, claims, bulk operations, analytics, file
234258
exports, public link reads and the emoji alias catalogue.
235259

236260
Deliberately out of scope: API key management, service health, the contact
@@ -248,7 +272,9 @@ exist for backward compatibility, not for new integrations.
248272
| `links.update`, `links.setStatus` | `PATCH /api/v1/urls/{id}`, `PATCH /api/v1/urls/{id}/status` |
249273
| `links.delete`, `links.deleteByDomain` | `DELETE /api/v1/urls/{id}`, `DELETE /api/v1/urls?domain=` |
250274
| `links.claim` | `POST /api/v1/urls/claim` |
251-
| `links.bulk.delete`, `links.bulk.setStatus`, `links.bulk.setExpiry`, `links.bulk.setDomain` | `POST /api/v1/urls/bulk/*` |
275+
| `links.bulk.delete`, `links.bulk.setStatus`, `links.bulk.setExpiry`, `links.bulk.setDomain`, `links.bulk.tags` | `POST /api/v1/urls/bulk/*` |
276+
| `tags.list`, `tags.create` | `GET /api/v1/tags`, `POST /api/v1/tags` |
277+
| `tags.update`, `tags.delete` | `PATCH /api/v1/tags/{id}`, `DELETE /api/v1/tags/{id}` |
252278
| `stats.get`, `stats.getForLink` | `GET /api/v1/stats`, `GET /api/v1/stats/links/{id}` |
253279
| `stats.export`, `stats.exportForLink` | `GET /api/v1/export`, `GET /api/v1/export/links/{id}` |
254280
| `public.stats`, `public.statsWithPassword` | `GET or POST /api/v1/public/stats/{code}` |

openapi.json

Lines changed: 2260 additions & 416 deletions
Large diffs are not rendered by default.

src/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type RequestOptions,
77
} from "./core/http.js";
88
import { Links } from "./resources/links.js";
9+
import { Tags } from "./resources/tags.js";
910
import { Stats } from "./resources/stats.js";
1011
import { PublicLinks } from "./resources/public.js";
1112
import { Emoji } from "./resources/emoji.js";
@@ -49,6 +50,7 @@ export interface SpooOptions {
4950

5051
export class Spoo {
5152
readonly links: Links;
53+
readonly tags: Tags;
5254
readonly stats: Stats;
5355
/** Public, unauthenticated per-link endpoints (stats page, preview). */
5456
readonly public: PublicLinks;
@@ -86,6 +88,7 @@ export class Spoo {
8688
});
8789

8890
this.links = new Links(this._transport, baseUrl);
91+
this.tags = new Tags(this._transport);
8992
this.stats = new Stats(this._transport);
9093
this.public = new PublicLinks(this._transport);
9194
this.emoji = new Emoji(this._transport);

0 commit comments

Comments
 (0)