@sprid/cli 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  sprid follows semver: a breaking change to a command's arguments, its output
4
4
  shape or its exit codes is a major release.
5
5
 
6
+ ## 1.2.0 - 2026-10-07
7
+
8
+ - Pinterest account-wide analytics and top Pins, board descriptions and section management.
9
+ - Connect Pinterest Ads separately; prepare reviewed paused Pin promotions, open app approval to start, and pause, end or sync campaign results.
10
+ - Pinterest metadata and metrics commands accept UUID identifiers.
11
+
12
+ ## 1.1.4 - 2026-10-07
13
+
14
+ - `sprid release` carries @sprid/release 1.2.0: `playScreenshots.tablet` uploads a tablet
15
+ set with `sprid release screenshots --play`, filling Play's 7" and 10" slots from one
16
+ capture.
17
+
6
18
  ## 1.1.3 - 2026-10-06
7
19
 
8
20
  - The bundled chat guide (`sprid docs`): if you change a TikTok post after approving it,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprid/cli",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@sprid/shots": "0.1.3",
42
- "@sprid/release": "1.1.0"
42
+ "@sprid/release": "1.2.0"
43
43
  },
44
44
  "publishConfig": {
45
45
  "access": "public"
@@ -10,6 +10,8 @@ import { confirm } from "./reviews.mjs";
10
10
 
11
11
  const USAGE = [
12
12
  "sprid ads [overview] --account <slug>",
13
+ "sprid ads promotions|promote --account <slug> [--file <promotion.json>]",
14
+ "sprid ads promotion start|pause|end|sync <id> --account <slug>",
13
15
  "sprid ads status|council|metrics <adSetId> --account <slug> [--days N]",
14
16
  "sprid ads create campaign|set|creative|ad|batch --file <args.json> --account <slug>",
15
17
  "sprid ads launch <adSetId> --account <slug>",
@@ -36,6 +38,24 @@ export async function ads(ctx) {
36
38
  show(ctx, await callTool(ctx, "ads_overview", { account }));
37
39
  return 0;
38
40
  }
41
+ if (sub === "promotions") {
42
+ show(ctx, await callTool(ctx, "list_promotions", { account, days: Number(ctx.flags.days ?? 30) }));
43
+ return 0;
44
+ }
45
+ if (sub === "promote") {
46
+ if (typeof ctx.flags.file !== "string") throw new UsageError("sprid ads promote --account <slug> --file <promotion.json>");
47
+ show(ctx, await callTool(ctx, "create_promotion", { account, promotion: await readJson(ctx, ctx.flags.file) }));
48
+ return 0;
49
+ }
50
+ if (sub === "promotion") {
51
+ const id = needId(ctx.positionals[2], "sprid ads promotion start|pause|end|sync <id> --account <slug>");
52
+ const verbs = { start: "start_promotion", pause: "pause_promotion", end: "end_promotion", sync: "sync_promotion" };
53
+ if (!verbs[arg]) throw new UsageError("Choose start, pause, end or sync");
54
+ const result = await callTool(ctx, verbs[arg], { account, id });
55
+ if (arg === "start") return handOff(ctx, result);
56
+ show(ctx, result);
57
+ return 0;
58
+ }
39
59
  if (READS[sub]) {
40
60
  const adSetId = needId(arg, `sprid ads ${sub} <adSetId> --account <slug>`);
41
61
  const days = ctx.flags.days && sub !== "status" ? { days: Number(ctx.flags.days) } : {};
@@ -30,6 +30,8 @@ const AD_CHANNELS = {
30
30
  meta_ads: "meta_ads",
31
31
  "google-ads": "google_ads",
32
32
  google_ads: "google_ads",
33
+ "pinterest-ads": "pinterest_ads",
34
+ pinterest_ads: "pinterest_ads",
33
35
  "tiktok-ads": "tiktok_ads",
34
36
  tiktok_ads: "tiktok_ads",
35
37
  };
@@ -242,7 +244,7 @@ async function connectChannel(ctx, platform) {
242
244
  const adAccountId = ctx.flags["ad-account"];
243
245
  const pixelId = ctx.flags.dataset;
244
246
  if (pageId && platform !== "facebook" && platform !== "meta_ads") throw new UsageError("--page is only supported for Facebook and Meta Ads connections.");
245
- if (adAccountId && !["meta_ads", "google_ads", "tiktok_ads"].includes(platform)) throw new UsageError("--ad-account is only supported for Meta Ads, Google Ads and TikTok Ads connections.");
247
+ if (adAccountId && !["meta_ads", "google_ads", "tiktok_ads", "pinterest_ads"].includes(platform)) throw new UsageError("--ad-account is only supported for Meta Ads, Google Ads, TikTok Ads and Pinterest Ads connections.");
246
248
  if (pixelId && platform !== "meta_ads") throw new UsageError("--dataset is only supported for Meta Ads connections.");
247
249
  const selection = {
248
250
  ...(pageId ? { pageId: String(pageId) } : {}),
@@ -278,7 +280,7 @@ async function connectChannel(ctx, platform) {
278
280
  if (s === "connected") {
279
281
  pending.clear();
280
282
  const handle = !last.channel?.displayName ? label(platform)
281
- : ["meta_ads", "google_ads", "tiktok_ads"].includes(platform) ? last.channel.displayName : `@${last.channel.displayName}`;
283
+ : ["meta_ads", "google_ads", "tiktok_ads", "pinterest_ads"].includes(platform) ? last.channel.displayName : `@${last.channel.displayName}`;
282
284
  if (ctx.json) ctx.out({ ok: true, status: s, channel: last.channel, account });
283
285
  else ctx.print(` ${C.ok(OK)} ${handle} connected to ${account}`);
284
286
  return 0;
@@ -298,7 +300,7 @@ async function connectChannel(ctx, platform) {
298
300
  }
299
301
 
300
302
  function label(platform) {
301
- return { instagram: "Instagram", tiktok: "TikTok", youtube: "YouTube", linkedin: "LinkedIn", facebook: "Facebook", pinterest: "Pinterest", x: "X", meta_ads: "Meta Ads", google_ads: "Google Ads", tiktok_ads: "TikTok Ads", tiktok_accounts: "TikTok comments" }[platform] ?? platform;
303
+ return { instagram: "Instagram", tiktok: "TikTok", youtube: "YouTube", linkedin: "LinkedIn", facebook: "Facebook", pinterest: "Pinterest", x: "X", meta_ads: "Meta Ads", google_ads: "Google Ads", tiktok_ads: "TikTok Ads", pinterest_ads: "Pinterest Ads", tiktok_accounts: "TikTok comments" }[platform] ?? platform;
302
304
  }
303
305
 
304
306
  /** The server names the fields to re-run with; here they are flags. */
@@ -20,7 +20,43 @@ async function channel(ctx) {
20
20
  }
21
21
 
22
22
  export async function pinterest(ctx) {
23
- const [verb, ref] = ctx.positionals;
23
+ const [verb, ref, sectionId] = ctx.positionals;
24
+ if (verb === "overview") {
25
+ const selected = await channel(ctx);
26
+ const days = Number(ctx.flags.days ?? 30);
27
+ if (!Number.isInteger(days) || days < 1 || days > 90) throw new UsageError("--days must be 1 to 90");
28
+ const query = new URLSearchParams({ days: String(days) });
29
+ if (ctx.flags.end) query.set("end", String(ctx.flags.end));
30
+ const result = await ctx.api().get(`/api/channels/${selected.channel.id}/pinterest/analytics?${query}`);
31
+ if (ctx.json) ctx.out(result);
32
+ else {
33
+ ctx.print(` Pinterest account results · ${result.from} to ${result.to} UTC`);
34
+ ctx.print(` Outbound clicks ${result.outboundClicks ?? "unavailable"} · saves ${result.saves ?? "unavailable"} · impressions ${result.impressions ?? "unavailable"}`);
35
+ ctx.print(` Coverage ${result.observedDays}/${result.expectedDays} days`);
36
+ for (const pin of result.topPins ?? []) ctx.print(` ${pin.id} · ${pin.outboundClicks ?? "unavailable"} outbound clicks · ${pin.url}`);
37
+ for (const note of result.notes ?? []) ctx.print(C.dim(` ${note}`));
38
+ }
39
+ return 0;
40
+ }
41
+ if (["update", "section-create", "section-update"].includes(verb)) {
42
+ if (!ref || !/^\d+$/.test(ref)) throw new UsageError("Choose the numeric board id from sprid pinterest boards");
43
+ if (verb === "section-update" && (!sectionId || !/^\d+$/.test(sectionId))) throw new UsageError("Choose a numeric section id to rename");
44
+ const selected = await channel(ctx);
45
+ const path = `/api/channels/${selected.channel.id}/pinterest/boards/${ref}`;
46
+ let body;
47
+ if (verb === "update") {
48
+ body = {};
49
+ if (ctx.flags.name !== undefined) body.name = required(ctx, "name").trim();
50
+ if (ctx.flags.description !== undefined) body.description = String(ctx.flags.description);
51
+ if (!Object.keys(body).length) throw new UsageError("Pass --name or --description for the board");
52
+ } else body = { name: required(ctx, "name").trim() };
53
+ const result = verb === "section-create" ? await ctx.api().post(`${path}/sections`, body)
54
+ : await ctx.api().patch(verb === "update" ? path : `${path}/sections/${sectionId}`, body);
55
+ if (ctx.json) ctx.out(result);
56
+ else ctx.print(` ${C.ok("✓")} Saved ${result.name} (${result.id}).`);
57
+ return 0;
58
+ }
59
+
24
60
  if (verb === "boards") {
25
61
  const selected = await channel(ctx);
26
62
  const bookmark = ctx.flags.bookmark ? `?bookmark=${encodeURIComponent(String(ctx.flags.bookmark))}` : "";
@@ -56,7 +92,7 @@ export async function pinterest(ctx) {
56
92
  }
57
93
 
58
94
  if (verb === "set") {
59
- if (!ref || !/^\d+$/.test(ref)) throw new UsageError("sprid pinterest set <postId> --board <id> --link <url> [metadata]");
95
+ if (!ref || !/^(?:\d+|[0-9a-f]{8}-[0-9a-f-]{27}|[A-Za-z][A-Za-z0-9]{1,4}-[Rr]?\d+)$/i.test(ref)) throw new UsageError("sprid pinterest set <postId> --board <id> --link <url> [metadata]");
60
96
  const post = await ctx.api().get(`/api/posts/${ref}`);
61
97
  const previous = post.pinterestOptions ?? {};
62
98
  const hasFlag = (name) => Object.prototype.hasOwnProperty.call(ctx.flags, name);
@@ -98,7 +134,7 @@ export async function pinterest(ctx) {
98
134
  }
99
135
 
100
136
  if (verb === "metrics") {
101
- if (!ref || !/^\d+$/.test(ref)) throw new UsageError("sprid pinterest metrics <publishId> --account <slug> [--days 30] [--refresh]");
137
+ if (!ref || !/^(?:\d+|[0-9a-f]{8}-[0-9a-f-]{27})$/i.test(ref)) throw new UsageError("sprid pinterest metrics <publishId> --account <slug> [--days 30] [--refresh]");
102
138
  const account = required(ctx, "account");
103
139
  const params = new URLSearchParams({ account, publishId: ref, days: String(ctx.flags.days ?? 30) });
104
140
  if (ctx.flags.refresh) params.set("refresh", "true");
@@ -116,5 +152,5 @@ export async function pinterest(ctx) {
116
152
  return 0;
117
153
  }
118
154
 
119
- throw new UsageError("sprid pinterest boards --account <slug> [--channel <id>] [--bookmark <token>] | create --account <slug> --name <name> [--description <text>] [--privacy public|secret] | set <postId> --board <id> --link <url> | metrics <publishId> --account <slug>");
155
+ throw new UsageError("sprid pinterest boards --account <slug> [--channel <id>] [--bookmark <token>] | overview --account <slug> [--days 30] [--end YYYY-MM-DD] | update <boardId> --name <name> | section-create <boardId> --name <name> | section-update <boardId> <sectionId> --name <name> | create --account <slug> --name <name> [--description <text>] [--privacy public|secret] | set <postId> --board <id> --link <url> | metrics <publishId> --account <slug>");
120
156
  }
@@ -77,6 +77,10 @@ export const COMMAND_GROUPS = [
77
77
  ['connect', 'sprid connect tiktok-comments --account <slug>', 'Let the Inbox read and answer comments on your TikTok posts. Sign in with the TikTok account that publishes them; this is separate from TikTok publishing and from TikTok Ads.'],
78
78
  ['connect', 'sprid connect github --app <slug> --key-from-clipboard --repo <owner/repo>[,<owner/repo>]', 'Read a repository’s views, clones, referrers and stars. GitHub keeps 14 days of traffic, so Sprid copies it daily and the history starts the day you connect. --repo replaces the list, so name every repo.'],
79
79
  ['connect', 'sprid connect ga4|plausible|umami --app <slug> --key <file> … [--use]', 'PostHog, Google Analytics, Plausible and Umami all count the same website visits, so Sprid reads one of them and never adds them together. --use makes this one the source the traffic card reports from.'],
80
+ ['pinterest', 'sprid pinterest overview --account <slug> [--channel <id>] [--days N] [--end YYYY-MM-DD]', 'Read live organic account analytics and top Pins, including Pins outside Sprid. End is inclusive and must be a completed UTC day.'],
81
+ ['pinterest', 'sprid pinterest update <boardId> --account <slug> [--name <name>] [--description <text>]', 'Rename a board or change its description on the exact selected Pinterest channel.'],
82
+ ['pinterest', 'sprid pinterest section-create <boardId> --account <slug> --name <name>', 'Create a board section.'],
83
+ ['pinterest', 'sprid pinterest section-update <boardId> <sectionId> --account <slug> --name <name>', 'Rename a section on its board.'],
80
84
  ['pinterest', 'sprid pinterest boards --account <slug> [--channel <id>] [--bookmark <token>]', 'List boards and sections for the exact connected Pinterest channel; a returned bookmark reads the next page.'],
81
85
  ['pinterest', 'sprid pinterest create --account <slug> --name <name> [--description <text>] [--privacy public|secret]', 'Create a board in the exact connected Pinterest account. Older connections must reconnect once to grant board creation permission.'],
82
86
  ['pinterest', 'sprid pinterest set <postId> --board <id> --link <url> [--section <id>] [--title "…"] [--description "…"] [--alt-text "…"] [--ai-disclosures AI_MODIFIED,SYNTHETIC_PERFORMER]', 'Save the reviewed Pin destination and searchable metadata on a draft.'],
@@ -115,6 +119,9 @@ export const COMMAND_GROUPS = [
115
119
  ['reviews', 'sprid reviews reply <id> [--text "…"] [--yes]', 'Review and send a public reply. --yes skips the confirmation.'],
116
120
  ] },
117
121
  { title: 'Paid ads', entries: [
122
+ ['ads', 'sprid ads promotions --account <slug> [--days N]', 'List promotions across connected advertising networks.'],
123
+ ['ads', 'sprid ads promote --account <slug> --file <promotion.json>', 'Prepare a paused promotion. File carries requestKey, publishId, name, currency, dailyBudgetCents, durationDays, countries and specialAdCategories. Pinterest also requires maxBidCents.'],
124
+ ['ads', 'sprid ads promotion start|pause|end|sync <id> --account <slug>', 'Start opens the in-app spending approval. Pause and end stop delivery; sync reads campaign results.'],
118
125
  ['ads', 'sprid ads [overview] --account <slug>', 'Spend against the cap, what is live and what the connected Meta Ads channel can do.'],
119
126
  ['ads', 'sprid ads status|council|metrics <adSetId> --account <slug> [--days N]', 'Delivery and rejections from Meta, the weekly judgement, or the numbers for one ad set.'],
120
127
  ['ads', 'sprid ads create campaign|set|creative|ad|batch --file <args.json> --account <slug>', 'Create paused objects. The file holds the tool’s arguments; sprid tool describe create_ad_set shows them.'],
@@ -43,7 +43,8 @@ export const GUIDE_GROUPS = [
43
43
  "slugs": [
44
44
  "meta-ads",
45
45
  "google-ads",
46
- "tiktok-ads"
46
+ "tiktok-ads",
47
+ "pinterest-ads"
47
48
  ]
48
49
  }
49
50
  ];
@@ -54,6 +55,7 @@ export const GUIDE_ALIASES = {
54
55
  "ga4": "google-analytics",
55
56
  "ga": "google-analytics",
56
57
  "google-analytics-4": "google-analytics",
58
+ "pinterest_ads": "pinterest-ads",
57
59
  "meta_ads": "meta-ads",
58
60
  "meta": "meta-ads",
59
61
  "google_ads": "google-ads",
@@ -1376,7 +1378,7 @@ export const GUIDES = [
1376
1378
  {
1377
1379
  "id": "pinterest",
1378
1380
  "title": "Pinterest",
1379
- "summary": "Publish the image and video Pins you select to the right boards, keep their destination links intact and read their daily results.",
1381
+ "summary": "Publish the image and video Pins you select to the right boards, keep their destination links intact and read their daily and account-wide results. Rename boards, edit descriptions and create or rename sections.",
1380
1382
  "command": "sprid connect pinterest --account <slug>",
1381
1383
  "url": "https://sprid.studio/docs/connect/pinterest",
1382
1384
  "sections": [
@@ -1427,10 +1429,16 @@ export const GUIDES = [
1427
1429
  "title": "Sources",
1428
1430
  "kind": "sources",
1429
1431
  "markdown": "- [Pinterest developer guidelines](https://policy.pinterest.com/en/developer-guidelines)\n- [Claim your website](https://help.pinterest.com/en/business/article/claim-your-website)\n- [Pinterest Analytics](https://help.pinterest.com/en/business/article/pinterest-analytics)"
1432
+ },
1433
+ {
1434
+ "id": "account-wide-results-and-board-management",
1435
+ "title": "Account-wide results and board management",
1436
+ "kind": "detail",
1437
+ "markdown": "Open **Insights → From social → Pinterest** to read organic results for the entire connected Pinterest account, including Pins created outside Sprid. The window uses completed UTC days and exposes missing coverage. Do not add account totals to per-post totals. Account analytics are read live without server retention.\n\n```sh\nsprid pinterest overview --account <slug> --days 30\nsprid pinterest update <boardId> --account <slug> --name \"Useful guides\" --description \"Searchable guides\"\nsprid pinterest section-create <boardId> --account <slug> --name \"Getting started\"\nsprid pinterest section-update <boardId> <sectionId> --account <slug> --name \"First steps\"\n```\n\nIn a Pin’s board picker, **Manage board and sections** opens the same operations. Creating a section selects it as that Pin’s destination. Public and secret boards require their respective write scopes; reconnect if the grant predates them.\n\nPinterest ads use a separate [Pinterest Ads connection](https://sprid.studio/docs/connect/pinterest-ads). Connecting ordinary Pinterest does not request advertising permissions."
1430
1438
  }
1431
1439
  ],
1432
- "markdown": "# Pinterest\n\n**What Sprid does with this:** Publish the image and video Pins you select to the right boards, keep their destination links intact and read their daily results.\n\n## You need\n\n- A Pinterest account. A free business account is recommended, because Pinterest Analytics requires one.\n- Access to the Sprid account that will own this channel.\n- A board topic in mind. Sprid can create the board during publishing.\n\nYou do **not** create a Pinterest developer app, request API access or copy a token. You only authorize your own Pinterest account.\n\n## Then run\n\n```sh\nsprid connect pinterest --account <slug>\n```\n\nReplace `<slug>` with your Sprid account slug, or use **Settings → Accounts → [account] → Pinterest → Continue with Pinterest** in Sprid.\n\n## Click path (the connect)\n\n1. Check the Pinterest identity in the browser that opens. Sign out first if it is the wrong account.\n2. Review the requested access and authorize Sprid.\n3. Back in Sprid, confirm the connected Pinterest name.\n4. Open a post, select **Publish → Pinterest** and pick the destination in the **Board** picker. If none fits, select **Create board**, name it and choose public or secret; Sprid selects it automatically.\n\nEach Sprid account needs its own connect, even when several Pinterest accounts are signed in to the same browser. Check the identity every time.\n\n## Check the connection\n\nRun `sprid status` and confirm Pinterest shows the intended account. Open a draft Pin, select **Publish → Pinterest** and confirm the intended board appears before approving any schedule.\n\nAfter the first Pin publishes, open Pinterest signed out or from another account and check the Pin, board, visual, title and destination. A successful API response does not prove public visibility.\n\n## Prepare the account\n\nCreate a few specific boards around topics people search for, such as “Small apartment viewing checklist” rather than “Inspiration”. Board names, descriptions and saved Pins give Pinterest context.\n\nClaim your website in Pinterest where possible: it links Pins from that site to the profile and expands website analytics. A website can be claimed by only one Pinterest account, so decide which market or brand owns it before connecting several.\n\nRead [Pinterest content and publishing](https://sprid.studio/docs/pinterest) before preparing the first batch.\n\n## If it fails\n\n- **Sprid says Pinterest is unavailable:** Pinterest is not enabled on the Sprid server you are using. Contact [Sprid support](mailto:hello@sprid.studio); you do not supply credentials.\n- **Wrong account connected:** disconnect it in Sprid, sign out of Pinterest in that browser, then reconnect and check the identity before authorizing.\n- **“No Pinterest boards yet”:** create one from the board picker. If Sprid asks for board permission, reconnect once; older connections lack the board write scope.\n- **Existing boards missing:** reconnect once, then contact [Sprid support](mailto:hello@sprid.studio) with the account name and error.\n- **Access denied:** confirm the Pinterest account is active and you finished the consent screen. Retry once, then contact Sprid support.\n- **“Reconnect Pinterest to publish public Pins”:** the saved connection cannot publish. Reconnect once with the same Pinterest account.\n- **Analytics unavailable:** use a business account and check the Pin is public. Unavailable data is unavailable, not zero.\n\n## Investigate with this connection\n\nOperation `posts`, read from Sprid’s stored Pin publishes and collected daily Pin metrics: impressions, saves, Pin clicks, outbound clicks and video views, summed up to the exclusive end with the days actually observed. There is no `comments` operation: Sprid does not collect Pinterest comments. Compare Pins at equal ages, such as 30, 60 and 90 days. An outbound click is someone leaving Pinterest, not a website session or an install. Live results for one Pin come from `get_pinterest_metrics` (MCP) or `GET /api/metrics/pinterest`.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source pinterest --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources\n\n- [Pinterest developer guidelines](https://policy.pinterest.com/en/developer-guidelines)\n- [Claim your website](https://help.pinterest.com/en/business/article/claim-your-website)\n- [Pinterest Analytics](https://help.pinterest.com/en/business/article/pinterest-analytics)\n",
1433
- "revision": "7adfb46a92aa6042"
1440
+ "markdown": "# Pinterest\n\n**What Sprid does with this:** Publish the image and video Pins you select to the right boards, keep their destination links intact and read their daily and account-wide results. Rename boards, edit descriptions and create or rename sections.\n\n## You need\n\n- A Pinterest account. A free business account is recommended, because Pinterest Analytics requires one.\n- Access to the Sprid account that will own this channel.\n- A board topic in mind. Sprid can create the board during publishing.\n\nYou do **not** create a Pinterest developer app, request API access or copy a token. You only authorize your own Pinterest account.\n\n## Then run\n\n```sh\nsprid connect pinterest --account <slug>\n```\n\nReplace `<slug>` with your Sprid account slug, or use **Settings → Accounts → [account] → Pinterest → Continue with Pinterest** in Sprid.\n\n## Click path (the connect)\n\n1. Check the Pinterest identity in the browser that opens. Sign out first if it is the wrong account.\n2. Review the requested access and authorize Sprid.\n3. Back in Sprid, confirm the connected Pinterest name.\n4. Open a post, select **Publish → Pinterest** and pick the destination in the **Board** picker. If none fits, select **Create board**, name it and choose public or secret; Sprid selects it automatically.\n\nEach Sprid account needs its own connect, even when several Pinterest accounts are signed in to the same browser. Check the identity every time.\n\n## Check the connection\n\nRun `sprid status` and confirm Pinterest shows the intended account. Open a draft Pin, select **Publish → Pinterest** and confirm the intended board appears before approving any schedule.\n\nAfter the first Pin publishes, open Pinterest signed out or from another account and check the Pin, board, visual, title and destination. A successful API response does not prove public visibility.\n\n## Prepare the account\n\nCreate a few specific boards around topics people search for, such as “Small apartment viewing checklist” rather than “Inspiration”. Board names, descriptions and saved Pins give Pinterest context.\n\nClaim your website in Pinterest where possible: it links Pins from that site to the profile and expands website analytics. A website can be claimed by only one Pinterest account, so decide which market or brand owns it before connecting several.\n\nRead [Pinterest content and publishing](https://sprid.studio/docs/pinterest) before preparing the first batch.\n\n## If it fails\n\n- **Sprid says Pinterest is unavailable:** Pinterest is not enabled on the Sprid server you are using. Contact [Sprid support](mailto:hello@sprid.studio); you do not supply credentials.\n- **Wrong account connected:** disconnect it in Sprid, sign out of Pinterest in that browser, then reconnect and check the identity before authorizing.\n- **“No Pinterest boards yet”:** create one from the board picker. If Sprid asks for board permission, reconnect once; older connections lack the board write scope.\n- **Existing boards missing:** reconnect once, then contact [Sprid support](mailto:hello@sprid.studio) with the account name and error.\n- **Access denied:** confirm the Pinterest account is active and you finished the consent screen. Retry once, then contact Sprid support.\n- **“Reconnect Pinterest to publish public Pins”:** the saved connection cannot publish. Reconnect once with the same Pinterest account.\n- **Analytics unavailable:** use a business account and check the Pin is public. Unavailable data is unavailable, not zero.\n\n## Investigate with this connection\n\nOperation `posts`, read from Sprid’s stored Pin publishes and collected daily Pin metrics: impressions, saves, Pin clicks, outbound clicks and video views, summed up to the exclusive end with the days actually observed. There is no `comments` operation: Sprid does not collect Pinterest comments. Compare Pins at equal ages, such as 30, 60 and 90 days. An outbound click is someone leaving Pinterest, not a website session or an install. Live results for one Pin come from `get_pinterest_metrics` (MCP) or `GET /api/metrics/pinterest`.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source pinterest --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources\n\n- [Pinterest developer guidelines](https://policy.pinterest.com/en/developer-guidelines)\n- [Claim your website](https://help.pinterest.com/en/business/article/claim-your-website)\n- [Pinterest Analytics](https://help.pinterest.com/en/business/article/pinterest-analytics)\n\n## Account-wide results and board management\n\nOpen **Insights → From social → Pinterest** to read organic results for the entire connected Pinterest account, including Pins created outside Sprid. The window uses completed UTC days and exposes missing coverage. Do not add account totals to per-post totals. Account analytics are read live without server retention.\n\n```sh\nsprid pinterest overview --account <slug> --days 30\nsprid pinterest update <boardId> --account <slug> --name \"Useful guides\" --description \"Searchable guides\"\nsprid pinterest section-create <boardId> --account <slug> --name \"Getting started\"\nsprid pinterest section-update <boardId> <sectionId> --account <slug> --name \"First steps\"\n```\n\nIn a Pin’s board picker, **Manage board and sections** opens the same operations. Creating a section selects it as that Pin’s destination. Public and secret boards require their respective write scopes; reconnect if the grant predates them.\n\nPinterest ads use a separate [Pinterest Ads connection](https://sprid.studio/docs/connect/pinterest-ads). Connecting ordinary Pinterest does not request advertising permissions.\n",
1441
+ "revision": "8ec74ac68439d71a"
1434
1442
  },
1435
1443
  {
1436
1444
  "id": "meta-ads",
@@ -1602,6 +1610,53 @@ export const GUIDES = [
1602
1610
  ],
1603
1611
  "markdown": "# TikTok Ads\n\n**What Sprid does with this:** Promote a post that is already live on TikTok as a Spark Ad from your own TikTok ad account, start it only when you confirm, and report what it spent and reached.\n\n## You need\n\n- A **TikTok for Business** login at [ads.tiktok.com](https://ads.tiktok.com), with an **ad account** you manage. This is a different login from your TikTok app account, and a different connection from TikTok publishing in Sprid.\n- A **payment method** or a prepaid balance on that ad account.\n- For each post you promote, its **ad authorization code**, generated in the TikTok app by whoever owns the post. See **Get a post's ad authorization code** below. Once a code is applied, Sprid remembers the post until the code expires.\n\nYou do **not** create a TikTok developer app or copy a token. You only sign in to TikTok for Business and pick the ad account.\n\n## Set up TikTok for Business (once, before you connect)\n\nSkip any step you have already done.\n\n1. **Ad account.** Sign in at [ads.tiktok.com](https://ads.tiktok.com) and create an ad account. Pick the **currency** and **time zone** carefully: neither can be changed later. Sprid’s budget controls support currencies with two decimals (USD, EUR, SEK and most others).\n2. **Payment.** In TikTok Ads Manager open **Account → Payment** and add a card or funds. Sprid can connect an account without one, but nothing can deliver until it can pay.\n3. **Account review.** A new ad account is reviewed by TikTok before it can run anything. Sprid tells you when the account is still under review.\n\n## Then run\n\n```sh\nsprid connect tiktok-ads --account <slug>\n```\n\nReplace `<slug>` with your Sprid account slug. You can also use **Settings → Accounts → [account] → TikTok Ads** in Sprid.\n\n## Click path (the connect)\n\n1. Sign in with the TikTok for Business login that manages the ad account.\n2. Choose the ad account, leave every requested permission switched on and click **Confirm**.\n3. Back in Sprid, pick the **ad account** if the login manages more than one. The list only appears when there is a choice.\n\nWith several brands, connect each Sprid account separately and pick that brand’s own ad account every time.\n\n## Get a post's ad authorization code\n\nA Spark Ad runs the post as itself, under the creator’s account, so the creator has to allow it. In the TikTok app, signed in as the account that published the post:\n\n1. Open the post, tap **…** (or the share arrow), then **Ad settings**.\n2. Turn on **Ad authorization**. The first time, TikTok may ask you to turn it on for the account in **Settings and privacy → Creator tools → Ad settings**.\n3. Tap **Generate code**, pick how long the authorization should last and copy the code.\n4. In Sprid, open **Promote** on the post and paste the code when it asks for one.\n\nPick a duration longer than the promotion. Sprid refuses a code that ends before the promotion would, because the ad would stop when it expires. TikTok offers 7, 30, 60, 180 or 365 days *(from TikTok’s help pages; the exact labels in the app may differ)*.\n\n## Check the connection\n\nRun `sprid status` and check that TikTok Ads shows the intended ad account name, followed by “(TikTok Ads)”.\n\nA promotion Sprid prepares starts **paused**. Nothing is spent until you confirm the start. It runs on TikTok only, to adults (18+), in the countries you choose.\n\n## If it fails\n\n- **“TikTok promotion is not switched on in Sprid yet”:** Sprid has not enabled TikTok ads on this server. Contact [Sprid support](mailto:hello@sprid.studio).\n- **“This post needs its ad authorization code”:** generate one in the TikTok app (see above) and paste it in the Promote sheet.\n- **“That authorization code belongs to a different TikTok post”:** the code was generated on another post. Generate it on the post you are promoting.\n- **“The post’s ad authorization ends … before the promotion would”:** generate a new code with a longer duration, or shorten the promotion.\n- **“This TikTok ad account is not active” or still under review:** check the account’s status in TikTok Ads Manager.\n- **“TikTok cannot show ads in …”:** that country is not available to this ad account. Remove it from the promotion.\n- **The currency is not supported:** Sprid’s budgets need a two-decimal currency. Create an ad account in a supported currency.\n- **Political content:** TikTok does not allow political ads. Sprid refuses them.\n- **Housing, credit or employment:** declare the category in the Promote sheet. TikTok then limits targeting. Outside the US and Canada TikTok may refuse the category for your account; the message says so.\n- **The ad stopped with “creator authorization revoked”:** the creator turned ad authorization off or the code expired. Generate a new code and promote the post again.\n\n## Sources\n\n- [TikTok API for Business: authorization](https://business-api.tiktok.com/portal/docs?id=1738373164380162)\n- [About Spark Ads](https://ads.tiktok.com/help/article/spark-ads)\n- [How to create Spark Ads in TikTok Ads Manager](https://ads.tiktok.com/help/article/spark-ads-creation-guide)\n- [Special ad categories on TikTok](https://ads.tiktok.com/help/article/special-ad-categories)\n",
1604
1612
  "revision": "41a9c611fbe927bd"
1613
+ },
1614
+ {
1615
+ "id": "pinterest-ads",
1616
+ "title": "Pinterest Ads",
1617
+ "summary": "Promote a public image or video Pin through your Pinterest ad account, prepare it paused, and read its campaign spend and attributed checkouts. Only your confirmation in the Sprid app starts spending.",
1618
+ "command": "sprid connect pinterest-ads --account <slug>",
1619
+ "url": "https://sprid.studio/docs/connect/pinterest-ads",
1620
+ "sections": [
1621
+ {
1622
+ "id": "you-need",
1623
+ "title": "You need",
1624
+ "kind": "requirements",
1625
+ "markdown": "- A Pinterest business account with an advertising account you can manage, set up in Pinterest Ads Manager.\n- A public Pin published through the ordinary Pinterest connection on the same Sprid account. Its owner must match the advertising account’s owner.\n- A reviewed daily budget, maximum cost per click, run length and audience countries. Sprid supports advertising currencies with two decimal places."
1626
+ },
1627
+ {
1628
+ "id": "then-run",
1629
+ "title": "Then run",
1630
+ "kind": "command",
1631
+ "markdown": "```sh\nsprid connect pinterest-ads --account <slug>\n```\n\nOr open **Settings → Accounts → [account] → Ads → Pinterest Ads**."
1632
+ },
1633
+ {
1634
+ "id": "click-path",
1635
+ "title": "Click path",
1636
+ "kind": "steps",
1637
+ "markdown": "1. Sign in to the Pinterest login that manages your advertising account.\n2. Authorize reading your profile, Pins and boards, and reading and managing ads. Ordinary publishing remains a separate connection.\n3. If Pinterest returns several manageable ad accounts, select the one that pays for this brand. Sprid never chooses one for you.\n4. In Sprid, open a published Pin and choose **Promote**. Set audience countries, daily budget, maximum cost per click and run length. Declare any regulated category; these campaigns must be run in Pinterest Ads Manager instead.\n5. Save a paused draft, review the amounts, then confirm Start in the app. Agents and tokens can prepare or pause, but cannot start or increase spending.\n\nSprid uses consideration campaigns with a fixed daily campaign budget, manual maximum-CPC bidding, adults-only targeting and a campaign end date. Run length counts UTC days including the creation day and ends at midnight UTC, so the reviewed total cannot span an extra daily budget. Pause stops the parent campaign before Sprid records it as paused. Changing budget or end date also requires app approval."
1638
+ },
1639
+ {
1640
+ "id": "check-the-connection",
1641
+ "title": "Check the connection",
1642
+ "kind": "verify",
1643
+ "markdown": "Confirm the advertiser name and id in Settings. Check the reviewed Pin is public and owned by that advertiser before preparing it. Sprid checks identity again at creation and rejects historical test Pins.\n\nCampaign reports cover spend, paid impressions, paid Pin clicks, paid outbound clicks and Pinterest-attributed checkouts, with a 30-day click and 1-day view window, reported on conversion date. Earned delivery is excluded. Pin clicks reach content on or off Pinterest; outbound clicks leave Pinterest. Missing metrics stay unavailable. Checkouts are provider-attributed actions, not verified new customers; Pinterest needs your conversion tracking configured to report them. Campaign and child totals overlap and must never be added."
1644
+ },
1645
+ {
1646
+ "id": "if-it-fails",
1647
+ "title": "If it fails",
1648
+ "kind": "troubleshooting",
1649
+ "markdown": "- **No manageable ad accounts:** set up an advertiser in Pinterest Ads Manager or use a login with Owner, Admin or Campaign Manager access.\n- **Choose the account that owns the Pin:** ordinary Pinterest and Pinterest Ads must refer to the same Pin owner. A public Pin on a different profile cannot be promoted by this advertiser.\n- **Reconnect:** the grant expired or lacks `ads:read` / `ads:write`. Connect Pinterest Ads again; ordinary Pinterest consent does not add these permissions.\n- **Setup interrupted:** check Pinterest Ads Manager before retrying. Pinterest creation has no idempotency key; an interrupted request may have created a paused entity. Sprid keeps an uncertain setup for review rather than silently retrying it.\n- **Billing refused:** add or fix payment details in Pinterest Ads Manager. Sprid does not request billing permissions or manage your payment methods."
1650
+ },
1651
+ {
1652
+ "id": "sources",
1653
+ "title": "Sources",
1654
+ "kind": "sources",
1655
+ "markdown": "[Pinterest campaign and ad-group API](https://developer.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) and [Pinterest API description](https://github.com/pinterest/api-description/blob/main/v5/openapi.json)."
1656
+ }
1657
+ ],
1658
+ "markdown": "# Pinterest Ads\n\n**What Sprid does with this:** Promote a public image or video Pin through your Pinterest ad account, prepare it paused, and read its campaign spend and attributed checkouts. Only your confirmation in the Sprid app starts spending.\n\n## You need\n\n- A Pinterest business account with an advertising account you can manage, set up in Pinterest Ads Manager.\n- A public Pin published through the ordinary Pinterest connection on the same Sprid account. Its owner must match the advertising account’s owner.\n- A reviewed daily budget, maximum cost per click, run length and audience countries. Sprid supports advertising currencies with two decimal places.\n\n## Then run\n\n```sh\nsprid connect pinterest-ads --account <slug>\n```\n\nOr open **Settings → Accounts → [account] → Ads → Pinterest Ads**.\n\n## Click path\n\n1. Sign in to the Pinterest login that manages your advertising account.\n2. Authorize reading your profile, Pins and boards, and reading and managing ads. Ordinary publishing remains a separate connection.\n3. If Pinterest returns several manageable ad accounts, select the one that pays for this brand. Sprid never chooses one for you.\n4. In Sprid, open a published Pin and choose **Promote**. Set audience countries, daily budget, maximum cost per click and run length. Declare any regulated category; these campaigns must be run in Pinterest Ads Manager instead.\n5. Save a paused draft, review the amounts, then confirm Start in the app. Agents and tokens can prepare or pause, but cannot start or increase spending.\n\nSprid uses consideration campaigns with a fixed daily campaign budget, manual maximum-CPC bidding, adults-only targeting and a campaign end date. Run length counts UTC days including the creation day and ends at midnight UTC, so the reviewed total cannot span an extra daily budget. Pause stops the parent campaign before Sprid records it as paused. Changing budget or end date also requires app approval.\n\n## Check the connection\n\nConfirm the advertiser name and id in Settings. Check the reviewed Pin is public and owned by that advertiser before preparing it. Sprid checks identity again at creation and rejects historical test Pins.\n\nCampaign reports cover spend, paid impressions, paid Pin clicks, paid outbound clicks and Pinterest-attributed checkouts, with a 30-day click and 1-day view window, reported on conversion date. Earned delivery is excluded. Pin clicks reach content on or off Pinterest; outbound clicks leave Pinterest. Missing metrics stay unavailable. Checkouts are provider-attributed actions, not verified new customers; Pinterest needs your conversion tracking configured to report them. Campaign and child totals overlap and must never be added.\n\n## If it fails\n\n- **No manageable ad accounts:** set up an advertiser in Pinterest Ads Manager or use a login with Owner, Admin or Campaign Manager access.\n- **Choose the account that owns the Pin:** ordinary Pinterest and Pinterest Ads must refer to the same Pin owner. A public Pin on a different profile cannot be promoted by this advertiser.\n- **Reconnect:** the grant expired or lacks `ads:read` / `ads:write`. Connect Pinterest Ads again; ordinary Pinterest consent does not add these permissions.\n- **Setup interrupted:** check Pinterest Ads Manager before retrying. Pinterest creation has no idempotency key; an interrupted request may have created a paused entity. Sprid keeps an uncertain setup for review rather than silently retrying it.\n- **Billing refused:** add or fix payment details in Pinterest Ads Manager. Sprid does not request billing permissions or manage your payment methods.\n\n## Sources\n\n[Pinterest campaign and ad-group API](https://developer.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) and [Pinterest API description](https://github.com/pinterest/api-description/blob/main/v5/openapi.json).\n",
1659
+ "revision": "f49777262b1c62e4"
1605
1660
  }
1606
1661
  ];
1607
1662
  export const CONTENT_GUIDES = [