# Get Current User Source: https://docs.shipstar.ai/api-reference/authentication GET /me Retrieve the profile of the authenticated user ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` API tokens are created in the [Dashboard](https://app.shipstar.ai/dashboard) under **API Keys**. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/me" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/me', { headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' } }); const user = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.shipstar.ai/api/v1/me', headers={'Authorization': 'Bearer YOUR_API_TOKEN'} ) user = response.json() ``` ## Response **id** `string` *required* The user's unique identifier (UUID) **email** `string` *required* The user's email address **first\_name** `string` The user's first name **last\_name** `string` The user's last name **is\_active** `boolean` *required* Whether the user account is active **is\_verified** `boolean` *required* Whether the user's email has been verified ### Example Response ```json 200 theme={null} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe", "is_active": true, "is_verified": true } ``` ## Errors | Status | Description | | - | - | | 401 | Invalid or expired API token | # Get Banner by Slug Source: https://docs.shipstar.ai/api-reference/banner/get-banner GET /banner/{public_slug} Get a project's live announcement banner by its stable public slug Returns the live banner for an embed key. This is what the [embed widget](/guides/website-integration#announcement-banner) fetches: the slug is the project's stable banner key (`-banner`), and it stays the same when a new banner replaces the old one, so a snippet pasted once keeps showing whatever is current. No authentication required. A `404` means nothing is live for that key (never published, replaced, or past its take-down time) — render nothing. ## Path Parameters **public\_slug** `string` *required* The banner's public slug, shown in the dashboard's "Add to your website" sheet after the first publish. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/banner/acme-banner" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/banner/acme-banner'); const banner = response.status === 404 ? null : await response.json(); ``` ```python Python theme={null} import requests response = requests.get('https://api.shipstar.ai/api/v1/banner/acme-banner') banner = None if response.status_code == 404 else response.json() ``` ## Response The same shape as [Get Live Banner](/api-reference/banner/get-live-banner). ```json 200 theme={null} { "slug": "acme-banner", "headline": "Scheduled X threads now post themselves", "body": "Approve once; Shipstar posts the thread at the scheduled time.", "cta_label": "See what's new", "badge": "New", "link_url": "https://acme.com/changelog", "period_start": "2026-08-31", "period_end": "2026-09-07", "published_at": "2026-09-07T09:02:11+00:00", "expires_at": null, "commit_count": 14 } ``` ## Errors | Status | Description | | - | - | | 404 | No live banner for this slug | | 429 | Rate limit exceeded — wait `Retry-After` seconds | # Get Live Banner Source: https://docs.shipstar.ai/api-reference/banner/get-live-banner GET /banner Get the announcement banner currently live for a project Returns the announcement banner currently live for a project — the one-line bar (badge, headline, one-sentence body, call-to-action) that the [embed widget](/guides/website-integration#announcement-banner) shows at the top of a site. **Exactly one banner is live per project**: publishing a new one replaces it. With an API token the response is your project's banner. Without a token it is Shipstar's own (what [shipstar.ai](https://shipstar.ai) renders above its nav). A `404` means no banner is live — never published, replaced, or past its take-down time — and is the normal empty state, not an error: render nothing. Send `X-Shipstar-Page-Url` with the page you render it on so your dashboard's "Published" status links there. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/banner" \ -H "Authorization: Bearer $SHIPSTAR_API_TOKEN" \ -H "X-Shipstar-Page-Url: https://your-site.com/" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/banner', { headers: { Authorization: `Bearer ${process.env.SHIPSTAR_API_TOKEN}`, 'X-Shipstar-Page-Url': 'https://your-site.com/', }, next: { revalidate: 300 }, }); const banner = response.status === 404 ? null : await response.json(); ``` ```python Python theme={null} import os, requests response = requests.get( 'https://api.shipstar.ai/api/v1/banner', headers={ 'Authorization': f"Bearer {os.environ['SHIPSTAR_API_TOKEN']}", 'X-Shipstar-Page-Url': 'https://your-site.com/', }, ) banner = None if response.status_code == 404 else response.json() ``` ## Response **slug** `string` The stable embed key (`data-shipstar-key`). It does not change when a new banner replaces the old one. **headline** `string` At most 80 characters. **body** `string` One sentence on the benefit, at most 160 characters. The widget hides it on narrow screens. **cta\_label** `string` Call-to-action label, at most 24 characters. **badge** `string` Short tag to show before the headline, at most 16 characters — `"New"` unless the banner re-worded it. An empty string means the banner has no badge: render nothing. **link\_url** `string | null` Where the call-to-action points. `null` means the banner has no link — omit the button. **period\_start / period\_end** `string | null` The commit window the banner was generated from (`YYYY-MM-DD`). **published\_at** `string | null` ISO-8601. Changes with every publish; use it as a cache key or a "seen" marker. **expires\_at** `string | null` When the banner stops being served, if a take-down was set on the schedule. The API already stops serving it then; respect it if you cache longer. **commit\_count** `integer | null` Provenance: how many commits the banner was generated from. ### Example Response ```json 200 theme={null} { "slug": "acme-banner", "headline": "Scheduled X threads now post themselves", "body": "Approve once; Shipstar posts the thread at the scheduled time.", "cta_label": "See what's new", "badge": "New", "link_url": "https://acme.com/changelog", "period_start": "2026-08-31", "period_end": "2026-09-07", "published_at": "2026-09-07T09:02:11+00:00", "expires_at": null, "commit_count": 14 } ``` ## Errors | Status | Description | | - | - | | 404 | No banner is live for the project | | 429 | Rate limit exceeded — wait `Retry-After` seconds | # Get All Blogs RSS Feed Source: https://docs.shipstar.ai/api-reference/blog/all-blogs-feed GET /blogs/feed Get an RSS 2.0 feed of all published blog posts Returns an RSS 2.0 XML feed containing all published blog posts. This endpoint does not require authentication. Use this to provide a single RSS subscription URL for all of your blog content. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/blogs/feed" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/blogs/feed'); const rss = await response.text(); ``` ```python Python theme={null} import requests response = requests.get('https://api.shipstar.ai/api/v1/blogs/feed') rss = response.text ``` ## Response Returns an RSS 2.0 XML document with `Content-Type: application/xml` containing all published blog posts. ### Example Response ```xml 200 theme={null} My Product - Blog Latest blog posts and updates What We Shipped This Week This week we focused on improving the dashboard experience... Mon, 31 Mar 2025 12:00:00 GMT Introducing Smart Notifications We're excited to announce our new notification system... Mon, 24 Mar 2025 12:00:00 GMT ``` # Get Blog Post RSS Feed Source: https://docs.shipstar.ai/api-reference/blog/blog-post-feed GET /blog/{public_slug}/feed Get a published blog post as an RSS 2.0 feed item Returns a single published blog post formatted as an RSS 2.0 XML feed. This endpoint does not require authentication. ## Path Parameters **public\_slug** `string` *required* The unique public slug of the blog post. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/blog/weekly-update-march-31-2025/feed" ``` ```javascript JavaScript theme={null} const slug = 'weekly-update-march-31-2025'; const response = await fetch(`https://api.shipstar.ai/api/v1/blog/${slug}/feed`); const rss = await response.text(); ``` ```python Python theme={null} import requests slug = 'weekly-update-march-31-2025' response = requests.get(f'https://api.shipstar.ai/api/v1/blog/{slug}/feed') rss = response.text ``` ## Response Returns an RSS 2.0 XML document with `Content-Type: application/xml`. ## Errors | Status | Description | | - | - | | 404 | Blog post not found or not published | | 422 | Invalid slug format | # Get Public Blog Post Source: https://docs.shipstar.ai/api-reference/blog/get-blog-post GET /blog/{public_slug} Get a published blog post by its public slug Retrieves a single published blog post by its public slug. Returns the blog post markdown content and metadata. This endpoint does not require authentication. ## Path Parameters **public\_slug** `string` *required* The unique public slug of the blog post. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/blog/weekly-update-march-31-2025" ``` ```javascript JavaScript theme={null} const slug = 'weekly-update-march-31-2025'; const response = await fetch(`https://api.shipstar.ai/api/v1/blog/${slug}`); const post = await response.json(); ``` ```python Python theme={null} import requests slug = 'weekly-update-march-31-2025' response = requests.get(f'https://api.shipstar.ai/api/v1/blog/{slug}') post = response.json() ``` ## Response Returns the blog post object with its full markdown content. ### Example Response ```json 200 theme={null} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "slug": "weekly-update-march-31-2025", "content": "# What We Shipped This Week\n\nThis week we focused on improving the dashboard experience...", "period_start": "2025-03-24T00:00:00Z", "period_end": "2025-03-31T23:59:59Z", "published_at": "2025-03-31T12:00:00Z", "created_at": "2025-03-31T10:00:00Z" } ``` ## Errors | Status | Description | | - | - | | 404 | Blog post not found or not published | | 422 | Invalid slug format | # List Published Blog Posts Source: https://docs.shipstar.ai/api-reference/blog/list-blog-posts GET /blogs List all published blog posts, newest first Returns an array of all published blog posts with their content and metadata, ordered by most recent first. This endpoint does not require authentication. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/blogs" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/blogs'); const posts = await response.json(); ``` ```python Python theme={null} import requests response = requests.get('https://api.shipstar.ai/api/v1/blogs') posts = response.json() ``` ## Response Returns an array of blog post objects. Each post includes markdown content and metadata. ### Example Response ```json 200 theme={null} [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "slug": "weekly-update-march-31-2025", "content": "# What We Shipped This Week\n\nThis week we focused on...", "period_start": "2025-03-24T00:00:00Z", "period_end": "2025-03-31T23:59:59Z", "published_at": "2025-03-31T12:00:00Z", "created_at": "2025-03-31T10:00:00Z" } ] ``` # Get Changelog RSS Feed Source: https://docs.shipstar.ai/api-reference/changelog/changelog-feed GET /changelog/{public_slug}/feed Get a published changelog as an RSS 2.0 feed Returns a published changelog formatted as an RSS 2.0 XML feed. This endpoint does not require authentication. Use this to let users subscribe to your changelog updates in their preferred RSS reader. ## Path Parameters **public\_slug** `string` *required* The unique public slug of the changelog. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/changelog/my-product-changelog-march-2025/feed" ``` ```javascript JavaScript theme={null} const slug = 'my-product-changelog-march-2025'; const response = await fetch(`https://api.shipstar.ai/api/v1/changelog/${slug}/feed`); const rss = await response.text(); ``` ```python Python theme={null} import requests slug = 'my-product-changelog-march-2025' response = requests.get(f'https://api.shipstar.ai/api/v1/changelog/{slug}/feed') rss = response.text ``` ## Response Returns an RSS 2.0 XML document with `Content-Type: application/xml`. ### Example Response ```xml 200 theme={null} My Product - Changelog Latest product updates and changes Dashboard redesign Completely rebuilt dashboard with new analytics views. New Features Tue, 01 Apr 2025 10:00:00 GMT ``` ## Errors | Status | Description | | - | - | | 404 | Changelog not found or not published | | 422 | Invalid slug format | # Get Public Changelog Source: https://docs.shipstar.ai/api-reference/changelog/get-changelog GET /changelog/{public_slug} Get a published changelog by its public slug Retrieves a single published changelog by its public slug. This endpoint does not require authentication. ## Path Parameters **public\_slug** `string` *required* The unique public slug of the changelog. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/changelog/my-product-changelog-march-2025" ``` ```javascript JavaScript theme={null} const slug = 'my-product-changelog-march-2025'; const response = await fetch(`https://api.shipstar.ai/api/v1/changelog/${slug}`); const changelog = await response.json(); ``` ```python Python theme={null} import requests slug = 'my-product-changelog-march-2025' response = requests.get(f'https://api.shipstar.ai/api/v1/changelog/{slug}') changelog = response.json() ``` ## Response Returns the changelog object with its full content. ### Example Response ```json 200 theme={null} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "slug": "my-product-changelog-march-2025", "content": "{\"title\": \"March 2025 Changelog\", \"entries\": [{\"category\": \"New Features\", \"title\": \"Dashboard redesign\", \"description\": \"Completely rebuilt dashboard with new analytics views.\"}]}", "period_start": "2025-03-01T00:00:00Z", "period_end": "2025-03-31T23:59:59Z", "published_at": "2025-04-01T10:00:00Z", "created_at": "2025-03-31T15:00:00Z" } ``` ## Errors | Status | Description | | - | - | | 404 | Changelog not found or not published | | 422 | Invalid slug format | # List Published Changelogs Source: https://docs.shipstar.ai/api-reference/changelog/list-changelogs GET /changelogs List all published changelogs, newest first Returns an array of all published changelogs, ordered by most recent first. This endpoint does not require authentication. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/changelogs" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/changelogs'); const changelogs = await response.json(); ``` ```python Python theme={null} import requests response = requests.get('https://api.shipstar.ai/api/v1/changelogs') changelogs = response.json() ``` ## Response Returns an array of changelog objects. Each changelog contains structured entries with categories, titles, and descriptions. ### Example Response ```json 200 theme={null} [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "slug": "my-product-changelog-march-2025", "content": "{\"title\": \"March 2025 Changelog\", \"entries\": [...]}", "period_start": "2025-03-01T00:00:00Z", "period_end": "2025-03-31T23:59:59Z", "published_at": "2025-04-01T10:00:00Z", "created_at": "2025-03-31T15:00:00Z" } ] ``` # Health Check Source: https://docs.shipstar.ai/api-reference/health GET /health Check V1 API availability Returns the health status and version of the V1 API. This endpoint does not require authentication. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/health" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/health'); const status = await response.json(); ``` ```python Python theme={null} import requests response = requests.get('https://api.shipstar.ai/api/v1/health') status = response.json() ``` ## Response **status** `string` *required* API health status. Returns `"healthy"` when operational. **version** `string` *required* The API version string. ### Example Response ```json 200 theme={null} { "status": "healthy", "version": "1.0.0" } ``` # Get Public KB Articles Source: https://docs.shipstar.ai/api-reference/kb/get-kb-articles GET /kb/{public_slug} Get published knowledge base articles by public slug Retrieves a set of published knowledge base articles by its public slug. Returns the articles array and metadata. This endpoint does not require authentication. ## Path Parameters **public\_slug** `string` *required* The unique public slug of the KB article set. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/kb/shipstar-knowledge-base" ``` ```javascript JavaScript theme={null} const slug = 'shipstar-knowledge-base'; const response = await fetch(`https://api.shipstar.ai/api/v1/kb/${slug}`); const kb = await response.json(); ``` ```python Python theme={null} import requests slug = 'shipstar-knowledge-base' response = requests.get(f'https://api.shipstar.ai/api/v1/kb/{slug}') kb = response.json() ``` ## Response Returns the KB article set with its full content, including categorized articles. ### Example Response ```json 200 theme={null} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "slug": "shipstar-knowledge-base", "content": "{\"articles\": [{\"category\": \"Getting Started\", \"title\": \"How to connect your GitHub account\", \"body\": \"Navigate to Sources in your dashboard and click Connect GitHub...\"}, {\"category\": \"Content Generation\", \"title\": \"Generating your first changelog\", \"body\": \"Once your repositories are connected, go to the Content tab...\"}]}", "published_at": "2025-03-31T12:00:00Z", "created_at": "2025-03-31T10:00:00Z" } ``` ## Errors | Status | Description | | - | - | | 404 | KB articles not found or not published | | 422 | Invalid slug format | ## Mirrored help centers When a project mirrors its Featurebase help center into the Knowledge Base, the set carries `source` (`"featurebase"`), `source_url` (the help center's public address), and `synced_at`, and each article has a `source` object with the provider, the Featurebase `article_id`, its `url`, and `updated_at`. The Shipstar-served copy is canonical; the fields are there so a renderer can say where the article is maintained. Sets authored in Shipstar have `source: null`. # List Published KB Articles Source: https://docs.shipstar.ai/api-reference/kb/list-kb-articles GET /kb List all published knowledge base article sets, newest first Returns an array of all published knowledge base article sets, ordered by most recent first. This endpoint does not require authentication. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/kb" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/kb'); const articles = await response.json(); ``` ```python Python theme={null} import requests response = requests.get('https://api.shipstar.ai/api/v1/kb') articles = response.json() ``` ## Response Returns an array of KB article set objects. Each set contains categorized help articles generated from your commits. ### Example Response ```json 200 theme={null} [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "slug": "shipstar-knowledge-base", "content": "{\"articles\": [{\"category\": \"Getting Started\", \"title\": \"How to connect your GitHub account\", \"body\": \"...\"}]}", "published_at": "2025-03-31T12:00:00Z", "created_at": "2025-03-31T10:00:00Z" } ] ``` # Add Recipients Source: https://docs.shipstar.ai/api-reference/mailing-lists/add-recipients POST /email/lists/{list_id}/recipients Bulk-add email addresses to a mailing list Append-only bulk add: up to 1000 entries per request, each a plain email address or an object with `email` plus optional `name`, `external_id` and `attributes`. Each address is validated individually; invalid entries are reported in the `invalid` array rather than failing the batch. Addresses are deduplicated case-insensitively, and addresses already on the list — active, removed or unsubscribed — are counted in `skipped_existing` and left untouched, so re-syncing never resurrects an opt-out. A list holds at most 500 active recipients. > [!NOTE] > > To keep a list in sync with another system (update names, reactivate removed contacts, change an address by `external_id`), use [Upsert Recipients](/api-reference/mailing-lists/upsert-recipients) instead. This endpoint never modifies an existing recipient. ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` API tokens are created in the [Dashboard](https://app.shipstar.ai/dashboard) under **API Keys**. The list must belong to the token's project. ## Path Parameters **list\_id** `string` *required* The mailing list's unique identifier (UUID). List ids come from [List Mailing Lists](/api-reference/mailing-lists/list-mailing-lists). ## Body **emails** `(string | object)[]` *required* Between 1 and 1000 entries. A plain string is an email address; an object carries `email` (required) plus optional `name`, `external_id` and `attributes` — the same shape as [Upsert Recipients](/api-reference/mailing-lists/upsert-recipients). ## Request ```bash cURL theme={null} curl -X POST "https://api.shipstar.ai/api/v1/email/lists/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipients" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"emails": ["one@example.com", {"email": "two@example.com", "name": "Sam", "external_id": "usr_42"}]}' ``` ```javascript JavaScript theme={null} const listId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const response = await fetch(`https://api.shipstar.ai/api/v1/email/lists/${listId}/recipients`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json' }, body: JSON.stringify({ emails: ['one@example.com', 'two@example.com'] }) }); const result = await response.json(); ``` ```python Python theme={null} import requests list_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' response = requests.post( f'https://api.shipstar.ai/api/v1/email/lists/{list_id}/recipients', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={'emails': ['one@example.com', 'two@example.com']} ) result = response.json() ``` ## Response **added** `integer` *required* Number of addresses added to the list **skipped\_existing** `integer` *required* Number of entries already on the list in any status (including unsubscribed ones, which are never re-activated) **invalid** `string[]` *required* Addresses that failed validation and were not added ### Example Response ```json 200 theme={null} { "added": 2, "skipped_existing": 1, "invalid": ["not-an-email"] } ``` ## Errors | Status | Description | | - | - | | 400 | Adding the batch would exceed the 500 active recipients per list limit | | 401 | Invalid or expired API token | | 404 | Mailing list not found in the token's project | | 422 | Invalid request body (e.g. empty `emails` array or more than 1000 items) | ## Rate Limits This endpoint is limited to 30 requests per minute per IP. # Create Mailing List Source: https://docs.shipstar.ai/api-reference/mailing-lists/create-mailing-list POST /email/lists Create a named mailing list in your API token's project Creates a mailing list in the project your API token is scoped to. A project holds at most 20 lists and names are unique within a project. Use the returned `id` with the recipient endpoints. ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` ## Body **name** `string` *required* The list name, 1–100 characters. Shown in the dashboard and the send dialog. ## Request ```bash cURL theme={null} curl -X POST "https://api.shipstar.ai/api/v1/email/lists" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Product updates"}' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/email/lists', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Product updates' }) }); const list = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( 'https://api.shipstar.ai/api/v1/email/lists', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={'name': 'Product updates'}, ) mailing_list = response.json() ``` ## Response Returns the created list in the same shape as [List Mailing Lists](/api-reference/mailing-lists/list-mailing-lists). ```json 201 theme={null} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Product updates", "recipient_count": 0, "active_count": 0, "removed_count": 0, "unsubscribed_count": 0, "created_at": "2026-09-28T10:00:00Z" } ``` ## Errors | Status | Description | | - | - | | 400 | The project already has 20 mailing lists, or the name is blank | | 401 | Invalid or expired API token | | 409 | A list with this name already exists in the project | | 422 | Invalid request body | ## Rate Limits This endpoint is limited to 30 requests per minute per IP. # Export Recipients Source: https://docs.shipstar.ai/api-reference/mailing-lists/export-recipients GET /email/lists/{list_id}/recipients/export.csv Download a mailing list as CSV Returns the list's recipients as a CSV attachment — every status unless you filter — with the columns: ``` email,name,external_id,status,created_at,status_changed_at,attributes ``` `attributes` is compact JSON (empty when the recipient has none). The same file is what the dashboard's **Export CSV** button downloads, and its columns are accepted back by the dashboard's **Import CSV**. ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` ## Path Parameters **list\_id** `string` *required* The mailing list's unique identifier (UUID). ## Query Parameters **status** `string` Only recipients in this status: `active`, `removed` or `unsubscribed`. ## Request ```bash cURL theme={null} curl -o recipients.csv \ "https://api.shipstar.ai/api/v1/email/lists/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipients/export.csv?status=active" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ## Response `200 OK` with `Content-Type: text/csv; charset=utf-8` and a `Content-Disposition: attachment; filename="-recipients.csv"` header. ```csv theme={null} email,name,external_id,status,created_at,status_changed_at,attributes sam@example.com,Sam Lee,usr_42,active,2026-09-01T08:00:00+00:00,,"{""plan"":""pro""}" casey@example.com,,,removed,2026-09-02T08:00:00+00:00,2026-09-20T09:30:00+00:00, ``` ## Errors | Status | Description | | - | - | | 400 | Unknown `status` value | | 401 | Invalid or expired API token | | 404 | Mailing list not found in the token's project | ## Rate Limits This endpoint is limited to 20 requests per minute per IP. # Get Mailing List Source: https://docs.shipstar.ai/api-reference/mailing-lists/get-mailing-list GET /email/lists/{list_id} One mailing list with recipient counts by status Returns a single mailing list with its recipient counts — the quickest way to check that a sync landed (`active_count` went up) without paging through recipients. ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` ## Path Parameters **list\_id** `string` *required* The mailing list's unique identifier (UUID). ## Request ```bash cURL theme={null} curl "https://api.shipstar.ai/api/v1/email/lists/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ## Response **id** `string` *required* The mailing list's unique identifier (UUID) **name** `string` *required* The mailing list's name **recipient\_count** `integer` *required* Total recipients on the list, in every status **active\_count** `integer` *required* Recipients who will receive the next release email (at most 500) **removed\_count** `integer` *required* Recipients you removed. An upsert or add reactivates them. **unsubscribed\_count** `integer` *required* Recipients who unsubscribed themselves. Never reactivated by the API. **created\_at** `string` *required* ISO 8601 creation timestamp ```json 200 theme={null} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Product updates", "recipient_count": 312, "active_count": 298, "removed_count": 9, "unsubscribed_count": 5, "created_at": "2026-07-10T09:12:00Z" } ``` ## Errors | Status | Description | | - | - | | 401 | Invalid or expired API token | | 404 | Mailing list not found in the token's project | ## Rate Limits This endpoint is limited to 100 requests per minute per IP. # List Mailing Lists Source: https://docs.shipstar.ai/api-reference/mailing-lists/list-mailing-lists GET /email/lists List the mailing lists of your API token's project Returns an array of the mailing lists belonging to the project your API token is scoped to, with recipient counts by status. Use the returned `id` values with [Upsert Recipients](/api-reference/mailing-lists/upsert-recipients) and the other recipient endpoints, or create a list with [Create Mailing List](/api-reference/mailing-lists/create-mailing-list). ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` API tokens are created in the [Dashboard](https://app.shipstar.ai/dashboard) under **API Keys**. ## Request ```bash cURL theme={null} curl -X GET "https://api.shipstar.ai/api/v1/email/lists" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.shipstar.ai/api/v1/email/lists', { headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' } }); const lists = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.shipstar.ai/api/v1/email/lists', headers={'Authorization': 'Bearer YOUR_API_TOKEN'} ) lists = response.json() ``` ## Response Returns an array of mailing list objects. **id** `string` *required* The mailing list's unique identifier (UUID) **name** `string` *required* The mailing list's name **recipient\_count** `integer` *required* Total number of recipients on the list in every status **active\_count** `integer` *required* Recipients who will receive the next release email **removed\_count** `integer` *required* Recipients you removed (an upsert or add reactivates them) **unsubscribed\_count** `integer` *required* Recipients who unsubscribed themselves (never reactivated by the API) **created\_at** `string` *required* When the list was created (ISO 8601 timestamp) ### Example Response ```json 200 theme={null} [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Release announcements", "recipient_count": 240, "active_count": 232, "removed_count": 3, "unsubscribed_count": 5, "created_at": "2026-06-15T09:30:00Z" } ] ``` ## Errors | Status | Description | | - | - | | 401 | Invalid or expired API token | ## Rate Limits This endpoint is limited to 100 requests per minute per IP. # List Recipients Source: https://docs.shipstar.ai/api-reference/mailing-lists/list-recipients GET /email/lists/{list_id}/recipients Page through a mailing list's recipients, with status filter and search Returns a mailing list's recipients oldest first, 100 per page by default. Each recipient carries the fields your sync wrote (`name`, `external_id`, `attributes`) plus its lifecycle `status`. Recipient statuses: | Status | Meaning | Reactivated by upsert? | | - | - | - | | `active` | Receives release emails | — | | `removed` | You removed them (API or dashboard) | Yes | | `unsubscribed` | They clicked unsubscribe in an email | Never | ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` ## Path Parameters **list\_id** `string` *required* The mailing list's unique identifier (UUID). ## Query Parameters **status** `string` Only recipients in this status: `active`, `removed` or `unsubscribed`. **q** `string` Case-insensitive search across email, name and external\_id. **limit** `integer` (default: 100) Page size, 1–500. **cursor** `string` The `next_cursor` from the previous page. Omit for the first page. ## Request ```bash cURL theme={null} curl "https://api.shipstar.ai/api/v1/email/lists/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipients?status=active&limit=200" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const listId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const all = []; let cursor = null; do { const url = new URL(`https://api.shipstar.ai/api/v1/email/lists/${listId}/recipients`); url.searchParams.set('limit', '500'); if (cursor) url.searchParams.set('cursor', cursor); const page = await (await fetch(url, { headers: { Authorization: 'Bearer YOUR_API_TOKEN' } })).json(); all.push(...page.items); cursor = page.next_cursor; } while (cursor); ``` ```python Python theme={null} import requests list_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' url = f'https://api.shipstar.ai/api/v1/email/lists/{list_id}/recipients' headers = {'Authorization': 'Bearer YOUR_API_TOKEN'} recipients, cursor = [], None while True: params = {'limit': 500, **({'cursor': cursor} if cursor else {})} page = requests.get(url, headers=headers, params=params).json() recipients += page['items'] cursor = page['next_cursor'] if not cursor: break ``` ## Response **items** `object[]` *required* Recipients in this page. **Recipient** **id** `string` *required* Recipient id (UUID) **email** `string` *required* Lowercased email address **name** `string` Display name, if set **external\_id** `string` Your identifier for this person, if set **attributes** `object` *required* Free-form key/value map (strings, numbers, booleans) **status** `string` *required* `active`, `removed` or `unsubscribed` **status\_changed\_at** `string` When the status last changed; null while never changed **is\_active** `boolean` *required* Shorthand for `status == "active"` **created\_at** `string` *required* ISO 8601 timestamp **updated\_at** `string` *required* ISO 8601 timestamp **next\_cursor** `string` Opaque cursor for the next page; `null` on the last page. ```json 200 theme={null} { "items": [ { "id": "5e0b1c1e-6d2b-4a1f-9a3d-1c2b3a4d5e6f", "email": "sam@example.com", "name": "Sam Lee", "external_id": "usr_42", "attributes": {"plan": "pro"}, "status": "active", "status_changed_at": null, "is_active": true, "created_at": "2026-09-01T08:00:00Z", "updated_at": "2026-09-20T08:00:00Z" } ], "next_cursor": "MjAyNi0wOS0wMVQwODowMDowMCswMDowMHw1ZTBiMWMxZS0..." } ``` ## Errors | Status | Description | | - | - | | 400 | Unknown `status` value or malformed `cursor` | | 401 | Invalid or expired API token | | 404 | Mailing list not found in the token's project | ## Rate Limits This endpoint is limited to 100 requests per minute per IP. # Remove Recipients Source: https://docs.shipstar.ai/api-reference/mailing-lists/remove-recipient DELETE /email/lists/{list_id}/recipients/{identifier} Take one recipient (or a batch) off a list without erasing their opt-out history Removal is a soft state. A removed recipient stops getting release emails but keeps their row, so a later [upsert](/api-reference/mailing-lists/upsert-recipients) or add puts them back, and a contact who unsubscribed themselves can never be removed-then-re-added as a way around their opt-out. Two forms: * **Single** — `DELETE /email/lists/{list_id}/recipients/{identifier}`, where `identifier` is matched as an email address first (case-insensitive), then as an `external_id`. * **Batch** — `POST /email/lists/{list_id}/recipients/remove` with `emails` and/or `external_ids`, up to 1000 identifiers. Both are idempotent. ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` ## Single removal ### Path Parameters **list\_id** `string` *required* The mailing list's unique identifier (UUID). **identifier** `string` *required* An email address or an `external_id`. URL-encode it (`@` is fine as is in most clients). ### Request ```bash cURL theme={null} curl -X DELETE "https://api.shipstar.ai/api/v1/email/lists/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipients/usr_42" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ### Response **status** `string` *required* `removed` (was active), `already_removed`, or `unsubscribed` (left untouched). ```json 200 theme={null} {"status": "removed"} ``` ## Batch removal `POST /email/lists/{list_id}/recipients/remove` ### Body **emails** `string[]` Email addresses to remove **external\_ids** `string[]` External ids to remove At least one of the two, at most 1000 identifiers in total. ### Request ```bash cURL theme={null} curl -X POST "https://api.shipstar.ai/api/v1/email/lists/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipients/remove" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"external_ids": ["usr_42", "usr_43"], "emails": ["old@example.com"]}' ``` ### Response **removed** `integer` *required* Active recipients now removed **already\_removed** `integer` *required* Identifiers that were already removed **skipped\_unsubscribed** `integer` *required* Identifiers matching unsubscribed recipients (untouched) **not\_found** `string[]` *required* Identifiers that matched nothing on this list ```json 200 theme={null} { "removed": 2, "already_removed": 0, "skipped_unsubscribed": 0, "not_found": ["old@example.com"] } ``` ## Errors | Status | Description | | - | - | | 401 | Invalid or expired API token | | 404 | Mailing list not found in the token's project, or (single form) no recipient matches the identifier | | 422 | Batch body has neither `emails` nor `external_ids`, or more than 1000 identifiers | ## Rate Limits Both forms are limited to 30 requests per minute per IP. # Upsert Recipients Source: https://docs.shipstar.ai/api-reference/mailing-lists/upsert-recipients PUT /email/lists/{list_id}/recipients Idempotent add-or-update — the call to sync your user base against Adds or updates up to 1000 recipients per request. This is the endpoint a signup hook, nightly job or Zapier/n8n flow should call: sending the same payload twice is harmless, and sending a changed one updates the contact in place. See the [sync guide](/guides/sync-mailing-list) for end-to-end recipes. How each entry is matched and what happens: | The entry… | Result | | - | - | | has an `external_id` that is already on the list | That recipient is updated — including its email, if it changed | | has an email that is on the list with no `external_id` | That recipient is updated and adopts the `external_id` | | matches nothing | Added as a new active recipient | | matches a `removed` recipient | Reactivated, then updated | | matches an `unsubscribed` recipient | Left completely untouched; counted in `skipped_unsubscribed` | | has an email that belongs to a *different* `external_id` | Reported in `conflicts`, nothing changed | `name` and `external_id` are only written when you send them. `attributes` replaces the stored map when present. Upsert never deletes: remove departed users with [Remove Recipient](/api-reference/mailing-lists/remove-recipient). ## Authentication This endpoint requires an API token passed as a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` ## Path Parameters **list\_id** `string` *required* The mailing list's unique identifier (UUID). ## Body **recipients** `(string | object)[]` *required* 1–1000 entries. A plain string is an email address. An object has: **Recipient object** **email** `string` *required* Email address (case-insensitive) **name** `string` Display name, up to 200 characters. Used for the greeting in release emails. **external\_id** `string` Your own id for this person (user id, customer id), up to 255 characters, unique per list. Key by it so an email change updates the same recipient. **attributes** `object` Up to 20 keys (≤ 64 chars each) with string, number, boolean or null values; at most 2 KB serialized. Replaces the stored map. ## Request ```bash cURL theme={null} curl -X PUT "https://api.shipstar.ai/api/v1/email/lists/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipients" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "recipients": [ {"email": "sam@example.com", "name": "Sam Lee", "external_id": "usr_42", "attributes": {"plan": "pro"}}, "casey@example.com" ] }' ``` ```javascript JavaScript theme={null} const listId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const response = await fetch(`https://api.shipstar.ai/api/v1/email/lists/${listId}/recipients`, { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json' }, body: JSON.stringify({ recipients: [ { email: 'sam@example.com', name: 'Sam Lee', external_id: 'usr_42', attributes: { plan: 'pro' } }, 'casey@example.com' ] }) }); const result = await response.json(); ``` ```python Python theme={null} import requests list_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' response = requests.put( f'https://api.shipstar.ai/api/v1/email/lists/{list_id}/recipients', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={'recipients': [ {'email': 'sam@example.com', 'name': 'Sam Lee', 'external_id': 'usr_42', 'attributes': {'plan': 'pro'}}, 'casey@example.com', ]}, ) result = response.json() ``` ## Response **added** `integer` *required* New recipients created **updated** `integer` *required* Existing active recipients whose fields changed **reactivated** `integer` *required* Removed recipients put back on the list **unchanged** `integer` *required* Entries that matched an active recipient with identical fields **skipped\_unsubscribed** `integer` *required* Entries that matched a recipient who unsubscribed; nothing was changed **invalid** `string[]` *required* Entries whose email failed validation **conflicts** `object[]` *required* Entries that could not be applied, each with `email`, `external_id` and a `reason`. ```json 200 theme={null} { "added": 1, "updated": 1, "reactivated": 0, "unchanged": 0, "skipped_unsubscribed": 0, "invalid": [], "conflicts": [] } ``` ## Errors | Status | Description | | - | - | | 400 | Applying the batch would exceed 500 active recipients (nothing is written; split the batch or remove contacts first) | | 401 | Invalid or expired API token | | 404 | Mailing list not found in the token's project | | 422 | Invalid request body (empty array, more than 1000 entries, oversize attributes) | ## Rate Limits This endpoint is limited to 30 requests per minute per IP. # Shipstar API Overview Source: https://docs.shipstar.ai/api-reference/overview Shipstar V1 REST API reference — endpoints, authentication, rate limits, and versioning ## Base URL All V1 API endpoints are served at: ``` https://api.shipstar.ai/api/v1 ``` ## Authentication Most public content endpoints (changelogs, blogs, KB articles) do **not** require authentication. The `/me` and mailing list endpoints require a Bearer token. ```bash theme={null} Authorization: Bearer YOUR_API_KEY ``` See the [Authentication Guide](/guides/authentication) for details on creating and managing API keys. ## Endpoints ### Public Content (No Auth Required) These endpoints serve published content and are freely accessible: > [card] [Changelogs](/api-reference/changelog/list-changelogs) > > List and retrieve published changelogs with RSS feeds > [card] [Blog Posts](/api-reference/blog/list-blog-posts) > > List and retrieve published blog posts with RSS feeds > [card] [Knowledge Base](/api-reference/kb/list-kb-articles) > > List and retrieve published KB article sets > [card] [Health Check](/api-reference/health) > > Check API availability ### Authenticated > [card] [Get Current User](/api-reference/authentication) > > Verify your API token and retrieve your user profile > [card] [Mailing Lists](/api-reference/mailing-lists/list-mailing-lists) > > Create lists, sync recipients (upsert / remove), page, and export CSV ## Response Format All endpoints return JSON responses. Public content endpoints return the generated content directly. Error responses follow this format: ```json theme={null} { "detail": "Error message describing the issue" } ``` ### Validation Errors (422) Validation errors include location and message details: ```json theme={null} { "detail": [ { "loc": ["path", "public_slug"], "msg": "Field required", "type": "missing" } ] } ``` ## RSS Feeds Published changelogs and blog posts include RSS 2.0 feeds. Append `/feed` to any changelog or blog post URL: * `GET /changelog/{slug}/feed` — RSS feed for a single changelog * `GET /blog/{slug}/feed` — RSS feed for a single blog post * `GET /blogs/feed` — RSS feed of all published blog posts ## Rate Limits | Endpoint Category | Limit | | - | - | | Public content | 100 requests/minute | | Authenticated endpoints | 100 requests/minute | | Mailing list writes (create list, add / upsert / remove recipients) | 30 requests/minute | | Mailing list CSV export | 20 requests/minute | Limits are applied per client IP. Every rate-limited response includes standard headers so clients (and AI agents) can self-throttle in real time: | Header | Meaning | | - | - | | `RateLimit-Limit` | Requests allowed in the current window | | `RateLimit-Remaining` | Requests left in the current window | | `RateLimit-Reset` | Seconds until the window resets | | `RateLimit-Policy` | Quota policy, e.g. `100;w=60` (100 requests per 60-second window) | | `X-RateLimit-Limit` / `X-RateLimit-Remaining` | Legacy equivalents of the above | | `X-RateLimit-Reset` | Window reset as a Unix timestamp | When a limit is exceeded, the API returns `429 Too Many Requests` with a `Retry-After` header (in seconds). Wait at least that long before retrying. ```bash theme={null} curl -sI https://api.shipstar.ai/api/v1/changelogs | grep -i ratelimit # ratelimit-limit: 100 # ratelimit-remaining: 99 # ratelimit-reset: 60 # ratelimit-policy: 100;w=60 ``` See the [Error Handling Guide](/guides/error-handling) for details on handling rate limit responses. ## Versioning The API is versioned in the URL path (`/api/v1`). Breaking changes only ship in a new version, and deprecations are announced ahead of any removal — see the [Versioning & Deprecation Policy](/api-reference/versioning). # Shipstar API Versioning & Deprecation Policy Source: https://docs.shipstar.ai/api-reference/versioning How the Shipstar API is versioned, what counts as a breaking change, and how deprecations are announced ## Versioning scheme The Shipstar REST API is versioned in the URL path: ``` https://api.shipstar.ai/api/v1 ``` `v1` is the current stable version. You never need to send a version header — the path is the contract. The MCP server (`https://mcp.shipstar.ai/mcp`) follows the same policy: tools may gain new optional parameters at any time, but existing tools and parameters are not removed or repurposed without the deprecation process below. ## What we may change without notice Additive, backwards-compatible changes can ship at any time within `v1`: * New endpoints, and new optional query or body parameters on existing endpoints * New fields in response objects (build clients that ignore unknown fields) * New enum values in fields documented as extensible * New response headers ## What counts as a breaking change Breaking changes only ship in a new version path (e.g. `/api/v2`), never silently within `v1`: * Removing or renaming an endpoint, parameter, or response field * Changing a field's type or meaning * Making an optional parameter required * Tightening authentication or narrowing a scope in a way that rejects previously valid requests ## How deprecations are announced When an endpoint or field is scheduled for removal: 1. The [changelog](https://shipstar.ai/changelog) and this documentation announce the deprecation and its replacement. 2. Affected endpoints start returning a `Deprecation` header, and — once a removal date is fixed — a `Sunset` header ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)) with that date: ``` Deprecation: true Sunset: Sat, 01 Jan 2028 00:00:00 GMT Link: ; rel="sunset" ``` 3. Deprecated `v1` surface stays available for **at least 6 months** after the announcement before removal. No endpoint currently carries a `Deprecation` or `Sunset` header — nothing is deprecated today. ## Recommendations for clients and agents * Key your integration to the versioned base URL, not to undocumented behavior. * Ignore response fields and headers you don't recognize. * Watch for `Deprecation`/`Sunset` headers in responses and subscribe to the [changelog feed](https://shipstar.ai/feed.xml) for announcements. # FAQ Source: https://docs.shipstar.ai/faq Frequently asked questions about Shipstar Have a question that isn't answered here? Reach out to us at [support@shipstar.ai](mailto:support@shipstar.ai). *** ## General > [accordion] What is Shipstar? > > Shipstar is automated product marketing for engineers and agents. It connects to your GitHub repositories, analyzes your commits, and generates polished marketing content — changelogs, blog posts, social media threads, knowledge base articles, release notes emails, and more. > [accordion] What content types does Shipstar generate? > > Shipstar generates 10 types of content: > > | Type | Format | > | - | - | > | Daily Product Update | Markdown | > | Weekly Product Update | Markdown | > | Feature Blog | Markdown | > | Blog Post | Markdown | > | LinkedIn Post | Text | > | Twitter Thread | Text | > | Feature Page | JSON | > | KB Articles | JSON | > | Public Changelog | JSON | > | Release Notes Email | JSON | > [accordion] How does the AI decide what to include? > > Shipstar uses Claude to analyze your commits. The AI is prompted to focus on user-facing changes and filter out internal noise like dependency bumps, CI/CD changes, refactors, and code cleanup. The output reads as if written by your team — it never mentions AI or tools. You can steer what it includes and who it writes for with [content guidelines](/guides/content-guidelines). > [accordion] Can I customize the tone or audience of generated content? > > Yes. In the dashboard, Settings → Content guidelines lets you pick a technical depth — **Technical** (developers; API names, config keys, migration notes), **Business** (plain-language outcomes, no code identifiers), or **Mixed** — and write free-text guidelines such as "Call users 'members', never 'customers'" or "Do announce docs-site changes". Changes to your own marketing site, docs hosting, CI and internal tooling are already left out by default, so you only need to write what the writer can't infer. These apply to every content type. You can override them for a single run when you regenerate a draft, on a schedule, or by passing `audience` / `instructions` to any generation tool or endpoint. > [accordion] Can I edit the generated content? > > Yes. All generated content can be reviewed and edited in the dashboard or via the API before publishing. Use the `PATCH /sources/content/{content_id}` endpoint to update content programmatically. *** ## GitHub Integration > [accordion] Which Git providers are supported? > > Currently, Shipstar supports GitHub. Install the Shipstar GitHub App on your organization or personal account, pick the repositories on GitHub, then select which of them to track in Sources. > [accordion] Does Shipstar work with private repositories? > > Yes. The repositories you grant on the GitHub App install screen — public or private — can be tracked. Shipstar's permissions are read-only: repository metadata and contents (used for commit history). > [accordion] Can I track multiple repositories? > > Yes. You can track as many repositories as you like. When generating content, Shipstar aggregates commits from all active tracked repos into a single piece of content. > [accordion] What date range does Shipstar use? > > By default, Shipstar analyzes the last 7 days of commits. You can specify a custom date range with the `start_date` and `end_date` parameters when triggering content generation. *** ## Publishing & Distribution > [accordion] How do I publish content? > > Use the publish endpoint: `POST /content/{content_id}/publish`. This generates a unique public URL where the content is accessible without authentication. You can also unpublish at any time. > [accordion] Do published pages have RSS feeds? > > Yes. Published changelogs and blog posts automatically include RSS feeds at `/feed` appended to the public URL. There's also a combined feed for all blog posts at `/api/v1/blogs/feed`. > [accordion] Can I publish to Slack? > > Yes. Shipstar can push any content type directly to a connected Slack channel. > [accordion] Can I publish KB articles to Intercom? > > Yes. Shipstar integrates with Intercom to sync generated knowledge base articles directly to your help center. > [accordion] Can I publish help articles to Featurebase? > > Yes. Connect Featurebase under Destinations with an organization API key (no OAuth), pick a default collection, and generate the Featurebase Articles content type. Each run drafts a set of articles you review in the dashboard; approving it creates the articles in your Featurebase help center — live, or as Featurebase drafts if you prefer a second review there. A schedule can send its articles to a different collection. *** ## Scheduling > [accordion] Can I automate content generation? > > Yes. Shipstar supports calendar events that trigger content generation on a schedule. Set up recurring events — for example, a weekly changelog every Friday or a monthly blog post on the 1st. > [accordion] What recurrence patterns are supported? > > Events follow the RFC 5545 RRULE standard, so you can configure daily, weekly, monthly, or custom recurrence patterns. *** ## API & Authentication > [accordion] How do I get an API key? > > 1. Sign up at [app.shipstar.ai](https://app.shipstar.ai) > 2. Navigate to the Dashboard > 3. Click **API Keys** in the sidebar > 4. Create a new API key and copy it > > See the [Quickstart guide](/quickstart) for detailed instructions. > [accordion] How does authentication work? > > The API uses Bearer token authentication. Include your API key in the `Authorization` header: > > ``` > Authorization: Bearer YOUR_API_KEY > ``` > > Public content endpoints (changelogs, blogs, KB articles accessed by slug) don't require authentication. > [accordion] Is content generation synchronous? > > No. Content generation runs asynchronously. When you trigger a generation request, you receive a `content_id` and `status: "pending"`. Poll the content endpoint to check when it's complete. The status transitions from `pending` → `processing` → `completed` (or `failed`). > [accordion] What are the usage limits? > > Usage is metered in monthly **credits**, consumed only by content generation > (API reads and publishing are free). Free includes 1,000 credits and 1 project; > Solo includes 50,000 credits and up to 3 projects; Team pools 50,000 credits per > seat with unlimited projects. Each generation costs 25–100 credits depending on > the content type — see [Plans & Credits](/plans-and-credits) for the full table. > Your current usage is shown in the dashboard sidebar and under **Account → Plan**. *** ## Privacy & Data > [accordion] What data does Shipstar access? > > Shipstar accesses commit metadata (message, author, date, SHA) from your tracked GitHub repositories. It does not access file contents, pull request discussions, or issues — only commit messages. > [accordion] Is my data shared with third parties? > > Generated content and your repository data are only accessible via authenticated API requests using your API key. Published content is accessible at its public URL by design. > [accordion] Can I disconnect my GitHub account? > > Yes. **Disconnect** on the Sources page unlinks the installation from your project and removes its tracked repositories. To revoke Shipstar's access to the organization entirely, uninstall (or suspend) the app from GitHub — Settings → Installed GitHub Apps → Shipstar; Shipstar picks that up automatically. *** ## Still have questions? > [card] [Contact Support](mailto:support@shipstar.ai) > > Reach out to the Shipstar team and we'll get back to you # Shipstar API Authentication Guide Source: https://docs.shipstar.ai/guides/authentication Complete guide to authenticating with the Shipstar API ## Overview The Shipstar API uses Bearer token authentication. There are two types of tokens depending on which API you're using: | API | Token Type | How to Get | | - | - | - | | Internal API (`/api/internal/`) | JWT session token | Login via `/api/internal/auth/jwt/login` | | Public API (`/api/v1/me`) | API key | Create in the Dashboard | | Public content (`/api/v1/changelogs`, `/api/v1/blogs`, etc.) | None required | Publicly accessible | ## API Key Authentication API keys are used for programmatic access to the Shipstar API. Include your key in the `Authorization` header: ```bash theme={null} Authorization: Bearer YOUR_API_KEY ``` ### Creating API Keys **1. Sign In** Go to [app.shipstar.ai](https://app.shipstar.ai) and sign in to your account. **2. Navigate to API Keys** Click on **API Keys** in the sidebar. **3. Create New Key** Click **Create API Key** and enter a descriptive name. > [!TIP] > > Use descriptive names like "Production Server", "Development", or "CI/CD Pipeline" to easily identify keys later. **4. Copy and Store Securely** Copy your API key immediately and store it securely. You won't be able to view the full key again — only the first 8 characters are stored for identification. > [!WARNING] > > Never share your API key or commit it to version control. ### API Key Details * Keys are hashed with SHA-256 before storage — Shipstar never stores your raw key * Each key tracks a `last_used_at` timestamp * Keys can have an optional expiration date * Keys can be deactivated without deletion ## JWT Session Authentication The internal API (dashboard endpoints) uses JWT tokens obtained by logging in: ```bash theme={null} # Login to get a JWT token curl -X POST "https://app.shipstar.ai/api/internal/auth/jwt/login" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=you@example.com&password=your_password" ``` ```json Response theme={null} { "access_token": "eyJhbGciOiJIUzI1...", "token_type": "bearer" } ``` Use the JWT token the same way as an API key: ```bash theme={null} Authorization: Bearer eyJhbGciOiJIUzI1... ``` ### Token Refresh JWT tokens expire after a configurable period. Refresh an expired token (within a 30-day grace period) at: ```bash theme={null} curl -X POST "https://app.shipstar.ai/api/internal/auth/jwt/refresh" \ -H "Authorization: Bearer YOUR_EXPIRED_TOKEN" ``` ## Implementation Examples ### Environment Variables Always store your API key in environment variables: ```bash .env theme={null} SHIPSTAR_API_KEY=your_api_key_here ``` ```javascript Node.js theme={null} const API_KEY = process.env.SHIPSTAR_API_KEY; const response = await fetch('https://api.shipstar.ai/api/v1/me', { headers: { 'Authorization': `Bearer ${API_KEY}` } }); ``` ```python Python theme={null} import os import requests API_KEY = os.environ.get('SHIPSTAR_API_KEY') response = requests.get( 'https://api.shipstar.ai/api/v1/me', headers={'Authorization': f'Bearer {API_KEY}'} ) ``` ### Backend Proxy Pattern For web applications, create a backend proxy to keep your API key secure: ```javascript Next.js API Route theme={null} // app/api/generate/route.ts export async function POST(req) { const { contentType, startDate, endDate } = await req.json(); const response = await fetch( `https://app.shipstar.ai/api/internal/sources/github/${contentType}`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.SHIPSTAR_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ start_date: startDate, end_date: endDate }) } ); const data = await response.json(); return Response.json(data, { status: response.status }); } ``` ```javascript Frontend theme={null} // Call your backend proxy, not the Shipstar API directly const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contentType: 'changelog' }) }); ``` ### Reusable Client Create a reusable client for your application: ```typescript theme={null} // lib/shipstar-client.ts class ShipstarClient { private apiKey: string; private baseUrl = 'https://app.shipstar.ai'; constructor(apiKey: string) { this.apiKey = apiKey; } private async request(endpoint: string, options: RequestInit = {}) { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', ...options.headers } }); if (!response.ok) { const error = await response.json(); throw new Error(`API Error ${response.status}: ${error.detail}`); } return response.json(); } async me() { return this.request('/api/v1/me'); } async generateChangelog(startDate?: string, endDate?: string) { return this.request('/api/internal/sources/github/changelog', { method: 'POST', body: JSON.stringify({ start_date: startDate, end_date: endDate }) }); } async getContent(contentId: string) { return this.request(`/api/internal/sources/content/${contentId}`); } async publishContent(contentId: string) { return this.request(`/api/internal/content/${contentId}/publish`, { method: 'POST' }); } } // Usage const shipstar = new ShipstarClient(process.env.SHIPSTAR_API_KEY!); const { content_id } = await shipstar.generateChangelog(); ``` ## Security Best Practices > [accordion] Never expose keys in client-side code > > API keys should never be included in frontend JavaScript, mobile apps, or anywhere they can be viewed by end users. > > ```javascript theme={null} > // ❌ NEVER do this in frontend code > const response = await fetch('https://api.shipstar.ai/api/v1/me', { > headers: { 'Authorization': 'Bearer sk_abc123' } > }); > > // ✅ Call your backend instead > const response = await fetch('/api/me'); > ``` > [accordion] Use environment variables > > Store API keys in environment variables, never in code: > > * Use `.env` files for local development > * Use secret management services in production (AWS Secrets Manager, HashiCorp Vault, etc.) > * Never commit `.env` files to version control > [accordion] Rotate keys regularly > > Establish a key rotation schedule: > > 1. Create a new API key > 2. Update your application to use the new key > 3. Verify the new key works in production > 4. Deactivate the old key > [accordion] Use separate keys for environments > > Create different API keys for development, staging, and production. This limits the impact if a key is compromised and makes auditing easier. > [accordion] Monitor key usage > > Regularly review your API usage in the Dashboard to detect unusual activity patterns and identify compromised keys. ## Troubleshooting > [accordion] 401 Unauthorized > > ```json theme={null} > { "detail": "Invalid or expired API token." } > ``` > > **Possible causes:** > > * API key is missing from the request > * API key has been deactivated or expired > * Using a JWT token that has expired (refresh it) > > **Solutions:** > > * Verify the `Authorization` header is present with the `Bearer ` prefix > * Check that the key hasn't been deactivated in the Dashboard > * Create a new API key if needed > [accordion] 403 Forbidden > > ```json theme={null} > { "detail": "Not authorized" } > ``` > > **Possible causes:** > > * Accessing a resource that belongs to another project or team > * Account restrictions > > **Solutions:** > > * Check that your API key belongs to the correct project > * Verify your account permissions in the Dashboard ### Testing Your API Key Use this request to verify your API key is working: ```bash theme={null} curl -X GET "https://api.shipstar.ai/api/v1/me" \ -H "Authorization: Bearer YOUR_API_KEY" ``` A successful response confirms your key is valid: ```json theme={null} { "id": "uuid", "email": "you@example.com", "first_name": "Jane", "last_name": "Doe", "is_active": true, "is_verified": true } ``` # Content Guidelines Source: https://docs.shipstar.ai/guides/content-guidelines Tune how technical generated content reads and what it should focus on or leave out ## Overview Content guidelines let you steer every piece Shipstar writes — changelogs, release notes emails, blog posts, social posts, KB articles — without editing prompts. There are two levers: * **Voice** (`audience`) — how technical the writing reads. One of `technical`, `business`, or `mixed`. This is separate from your Ideal Customer Profile: the ICP describes who your customers are and what they struggle with; the voice sets the register of a given piece — the same ICP often gets a business-level release email and a technical changelog. * **Focus & exclusions** (`instructions`) — free-text guidance (up to 2000 characters) on scope, tone, and terminology. Typical uses: house style ("British spelling, no exclamation marks"), naming ("call users *members*, never *customers*"), or widening the default scope ("do announce docs-site changes"). You don't need to exclude your own marketing site, docs hosting, CI or internal tooling — every prompt already skips those and writes only about changes your customers can use. Set them once per project as defaults, and override either field for a single run whenever you need to. ## Voice presets | Preset | Written for | What changes | | - | - | - | | `technical` | Developers, platform/DevOps engineers, technical founders | Names APIs, endpoints, CLI flags, config keys, and versions when the commits mention them; calls out breaking changes, migrations, deprecations, performance and reliability work; no marketing superlatives. | | `business` | Product managers, operations, executives, buyers, end users | Plain-language outcomes — what people can now do, what got faster or safer, and why it matters. No code identifiers, endpoint names, config keys, file paths, or commit hashes. | | `mixed` | A blend of both | Leads with the plain-language outcome, then adds at most one short technical detail where it helps a developer act. | Leave the preset unset and each content type keeps its built-in tone (release notes emails stay non-technical, LinkedIn posts stay professional, and so on). ## Project defaults In the dashboard, open **Settings → Content guidelines**, choose a voice, write your focus & exclusions, and save. The defaults apply to every new generation for that project — manual, scheduled, or kicked off by an agent through MCP. Agents can read the current defaults with [`get_project_context`](/mcp/project/get-project-context), which returns: ```json theme={null} "content_guidelines": { "audience": "business", "instructions": "Call users \"members\", never \"customers\". British spelling." } ``` ## Overriding for one run Each field falls back to the project default independently — pass only what you want to change. > [tab] Dashboard > > * **Regenerate…** on any draft opens a dialog prefilled with the effective guidelines (voice + focus & exclusions); adjust them and re-run. The override applies to that run only and is remembered on the draft, so a later regenerate starts from it. > * **Schedules** carry their own voice and focus & exclusions (set them in the schedule dialog); every occurrence uses them. > [tab] MCP tools > > Every `generate_*` tool accepts `audience` and `instructions`: > > ```json theme={null} > { > "tool": "generate_changelog", > "arguments": { > "audience": "technical", > "instructions": "Include API endpoint names. Skip anything under marketing/." > } > } > ``` > [tab] REST > > Every generation endpoint accepts a `guidelines` object: > > ```bash theme={null} > curl -X POST "https://api.shipstar.ai/api/internal/sources/github/changelog" \ > -H "Authorization: Bearer YOUR_API_KEY" \ > -H "Content-Type: application/json" \ > -d '{ "guidelines": { "audience": "business", "instructions": "British spelling." } }' > ``` ## How guidelines are applied Guidelines are appended to the content-type prompt as an explicit block. They take precedence over the type's built-in tone and its default list of what to skip — so "do mention docs changes" works as well as "don't" — but they never change the required output format. Changelogs and release notes emails still come back as structured JSON, blog posts as Markdown, and so on. The single-feature types (X threads, LinkedIn posts, feature pages, and banners) also decline to post when no commit in the period is a customer-facing product change: the run ends with "nothing to post", nothing is published, and the credits are refunded. Because guidelines win on scope, an instruction such as "do announce docs-site changes" makes those changes count and prevents that outcome. The resolved guidelines for any run are visible in the stored `prompt` of the generated content record, which makes it easy to confirm what the writer was told. ## Limits and validation * `audience` must be one of `technical`, `business`, `mixed` — anything else is rejected with a validation error. * `instructions` are trimmed and capped at 2000 characters. * Blank or omitted fields inherit the project default; there is currently no way to *suppress* a project default for a single run other than overriding it with different text. ## Related * [`get_project_context`](/mcp/project/get-project-context) — read the current defaults * [`generate_changelog`](/mcp/generation/generate-changelog) and the other generation tools — per-call overrides * [How it works](/how-it-works) — the full generation pipeline # Error Handling Guide Source: https://docs.shipstar.ai/guides/error-handling How to handle errors from the Shipstar API ## Overview This guide covers how to handle errors from the Shipstar API, including common error codes, response formats, and best practices for building resilient integrations. ## Error Response Format All API errors return a JSON response with a `detail` field: ```json theme={null} { "detail": "Error message describing the issue" } ``` For example, generation requests fail with `402` when your team's monthly credit allowance is exhausted (see [Plans & Credits](/plans-and-credits)): ```json theme={null} { "detail": "Monthly credit limit reached (1000/1000 used). Upgrade your plan to keep generating content." } ``` ## HTTP Status Codes ### Client Errors (4xx) | Status | Name | Description | | - | - | - | | 400 | Bad Request | Invalid request parameters or validation failure | | 401 | Unauthorized | Missing or invalid API key / JWT token | | 402 | Payment Required | Insufficient credits for the operation | | 403 | Forbidden | Not authorized to access this resource | | 404 | Not Found | Resource doesn't exist | | 409 | Conflict | Resource already exists (duplicate) | | 422 | Unprocessable Entity | Input validation error | | 429 | Too Many Requests | Rate limit exceeded | ### Server Errors (5xx) | Status | Name | Description | | - | - | - | | 500 | Internal Server Error | Unexpected server error | | 502 | Bad Gateway | External service error (GitHub, etc.) | | 503 | Service Unavailable | Service temporarily unavailable | ## Common Errors and Solutions > [accordion] 401 Unauthorized > > ```json theme={null} > { "detail": "Invalid or expired API token." } > ``` > > **Causes:** > > * Missing `Authorization` header > * API key has been deactivated or expired > * JWT session token has expired > > **Solutions:** > > * Ensure header format is `Authorization: Bearer YOUR_KEY` > * Check for typos in the API key > * Create a new API key in the Dashboard > * For JWT tokens, refresh via `/api/internal/auth/jwt/refresh` > > ```javascript theme={null} > // Correct format > headers: { > 'Authorization': 'Bearer your_api_key_here' > } > > // Common mistakes > headers: { > 'Authorization': 'your_api_key_here', // Missing "Bearer " > 'Api-Key': 'your_api_key_here' // Wrong header name > } > ``` > [accordion] 402 Insufficient Credits > > ```json theme={null} > { > "detail": "Monthly credit limit reached (1000/1000 used). Upgrade your plan to keep generating content." > } > ``` > > **Causes:** > > * Your team's monthly credit allowance is exhausted — generation requests > charge credits per content type (see [Plans & Credits](/plans-and-credits)) > > **Solutions:** > > * Wait for credits to reset at the next billing cycle > * Upgrade your plan for more credits > * Check your usage in the dashboard sidebar or under **Account → Plan** > > ```javascript theme={null} > if (response.status === 402) { > const { detail } = await response.json(); > console.log(detail); // human-readable message with used/limit > } > ``` > [accordion] 403 Forbidden > > ```json theme={null} > { "detail": "Not authorized" } > ``` > > **Causes:** > > * Accessing a resource that belongs to another project or team > * Attempting an admin-only operation > > **Solutions:** > > * Check that your API key belongs to the correct project > * Verify your account permissions > [accordion] 404 Not Found > > ```json theme={null} > { "detail": "Content not found" } > ``` > > **Causes:** > > * Content ID doesn't exist > * Public slug doesn't match any published content > * Resource has been deleted > > **Solutions:** > > * Verify the content ID or slug > * Check that the content hasn't been deleted or unpublished > > ```javascript theme={null} > async function getContent(contentId) { > const response = await fetch( > `${API_URL}/api/internal/sources/content/${contentId}`, > { headers } > ); > > if (response.status === 404) { > return null; // Content doesn't exist > } > > return response.json(); > } > ``` > [accordion] 409 Conflict > > ```json theme={null} > { "detail": "Resource already exists" } > ``` > > **Causes:** > > * Attempting to create a duplicate resource > > **Solutions:** > > * Check if the resource already exists before creating > * Use update endpoints instead of create > [accordion] 429 Too Many Requests > > ```json theme={null} > { > "detail": "Rate limit exceeded. Please try again later.", > "retry_after": 60 > } > ``` > > **Causes:** > > * Exceeded requests per minute limit > > **Solutions:** > > * Implement exponential backoff > * Respect the `retry_after` value > * Cache responses where possible > > See [Rate Limiting](#rate-limiting) below. > [accordion] 502 Bad Gateway > > ```json theme={null} > { "detail": "External service error" } > ``` > > **Causes:** > > * GitHub API is unavailable or returning errors > * Other external service failure > > **Solutions:** > > * Retry after a short delay > * Check if GitHub is experiencing an outage ## Implementing Error Handling ### Basic Error Handling ```javascript JavaScript theme={null} async function shipstarRequest(endpoint, options = {}) { const response = await fetch(`https://app.shipstar.ai${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${process.env.SHIPSTAR_API_KEY}`, 'Content-Type': 'application/json', ...options.headers } }); if (!response.ok) { const error = await response.json(); const detail = typeof error.detail === 'string' ? error.detail : error.detail?.message || JSON.stringify(error.detail); switch (response.status) { case 401: throw new Error('Authentication failed. Check your API key.'); case 402: throw new Error(`Insufficient credits: ${detail}`); case 404: throw new Error(`Not found: ${detail}`); case 429: throw new Error('Rate limit exceeded. Try again later.'); default: throw new Error(`API error (${response.status}): ${detail}`); } } return response.json(); } ``` ```python Python theme={null} import requests def shipstar_request(endpoint, method='GET', json=None): response = requests.request( method, f'https://app.shipstar.ai{endpoint}', headers={ 'Authorization': f'Bearer {os.environ["SHIPSTAR_API_KEY"]}', 'Content-Type': 'application/json' }, json=json ) if not response.ok: error = response.json() detail = error.get('detail', 'Unknown error') if response.status_code == 401: raise Exception('Authentication failed. Check your API key.') elif response.status_code == 402: raise Exception(f'Insufficient credits: {detail}') elif response.status_code == 404: raise Exception(f'Not found: {detail}') elif response.status_code == 429: raise Exception('Rate limit exceeded. Try again later.') else: raise Exception(f'API error ({response.status_code}): {detail}') return response.json() ``` ### With Retry Logic ```javascript theme={null} async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url, options); // Don't retry client errors (except 429) if (response.status >= 400 && response.status < 500 && response.status !== 429) { const error = await response.json(); throw new Error(error.detail); } // Retry on rate limit if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') || '60'); await sleep(retryAfter * 1000); continue; } // Retry on server errors with exponential backoff if (response.status >= 500) { await sleep(Math.pow(2, attempt) * 1000); continue; } return response.json(); } catch (error) { // Retry on network errors if (error.name === 'TypeError' && attempt < maxRetries - 1) { await sleep(Math.pow(2, attempt) * 1000); continue; } throw error; } } throw new Error('Max retries exceeded'); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } ``` ### Handling Content Generation Failures Content generation is asynchronous, so you need to handle failures in your polling logic: ```javascript theme={null} async function generateContent(contentType, options = {}) { // Trigger generation const { content_id } = await shipstarRequest( `/api/internal/sources/github/${contentType}`, { method: 'POST', body: JSON.stringify(options) } ); // Poll for result for (let i = 0; i < 30; i++) { await sleep(2000); const content = await shipstarRequest( `/api/internal/sources/content/${content_id}` ); switch (content.status) { case 'completed': return content; case 'failed': throw new Error(`Generation failed: ${content.error_message}`); case 'pending': case 'processing': continue; // Keep polling } } throw new Error('Generation timed out after 60 seconds'); } ``` ## Rate Limiting Shipstar applies rate limits to protect the service. Limits vary by endpoint: | Endpoint Category | Rate Limit | | - | - | | Authentication (login) | 10 requests/minute | | Registration | 5 requests/minute | | Email verification | 10 requests/hour | | Password reset | 5 requests/hour | | Token refresh | 30 requests/minute | | General API endpoints | 100 requests/minute | ### Handling Rate Limits ```javascript theme={null} class ShipstarClient { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://app.shipstar.ai'; } async request(endpoint, options = {}) { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', ...options.headers } }); if (response.status === 429) { const retryAfter = parseInt( response.headers.get('Retry-After') || '60' ); console.log(`Rate limited. Retrying in ${retryAfter}s...`); await sleep(retryAfter * 1000); return this.request(endpoint, options); // Retry once } if (!response.ok) { const error = await response.json(); throw new Error(`${response.status}: ${error.detail}`); } return response.json(); } } ``` ## Best Practices > [card] Handle All Statuses > > Account for pending, processing, completed, and failed states in your polling logic > [card] Use Retries > > Implement exponential backoff for 429 and 5xx errors > [card] Log Errors > > Log errors with context (endpoint, content ID, status) for debugging > [card] Set Timeouts > > Don't poll indefinitely — set a maximum wait time for content generation ### Logging Example ```javascript theme={null} async function generateWithLogging(contentType, options) { const startTime = Date.now(); try { const content = await generateContent(contentType, options); console.log('Generation succeeded:', { contentId: content.id, outputType: content.output_type, duration: Date.now() - startTime }); return content; } catch (error) { console.error('Shipstar API Error:', { contentType, options, error: error.message, timestamp: new Date().toISOString() }); throw error; } } ``` # Sandbox Testing Guide Source: https://docs.shipstar.ai/guides/sandbox-testing How to test your Shipstar integration during development ## Overview Shipstar's content generation runs asynchronously — you trigger a request, and the content is generated in the background. This guide covers strategies for testing your integration during development without waiting for real AI generation. ## Development Workflow ### Understanding the Content Lifecycle Every piece of content follows this lifecycle: ``` pending → processing → completed (or failed) ``` When you call a generation endpoint, you get back a `content_id` and `status: "pending"`. You then poll for the result. ### Testing Content Generation **1. Trigger Generation** Call any content generation endpoint. The request body is optional — defaults work for testing: ```bash theme={null} curl -X POST "https://app.shipstar.ai/api/internal/sources/github/changelog" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Response: ```json theme={null} { "content_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "pending" } ``` **2. Poll for Status** Check the content status until it's `completed` or `failed`: ```bash theme={null} curl -X GET "https://app.shipstar.ai/api/internal/sources/content/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **3. Inspect the Result** Once completed, the response includes the full generated content: ```json theme={null} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "source": "My Product", "output_type": "public_changelog", "status": "completed", "from_date": "2025-03-24T00:00:00Z", "to_date": "2025-03-31T00:00:00Z", "model": "anthropic/claude-sonnet-4-6", "content": "{ ... generated content ... }", "public_slug": null, "is_published": false, "created_at": "2025-03-31T10:00:00Z" } ``` ## Testing Strategies ### Use a Custom Date Range Test with a specific date range that you know has commits: ```bash theme={null} curl -X POST "https://app.shipstar.ai/api/internal/sources/github/blog-post" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "start_date": "2025-03-01T00:00:00Z", "end_date": "2025-03-31T23:59:59Z", "product_name": "Test Product" }' ``` ### Test Content Guidelines Pass a one-run `guidelines` override to check that audience and focus steering reach the writer (the resolved instructions are visible in the stored `prompt` of the generated record): ```bash theme={null} curl -X POST "https://app.shipstar.ai/api/internal/sources/github/changelog" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "guidelines": { "audience": "technical", "instructions": "Include API endpoint names." } }' ``` ### Test All Content Types Run through each content type to verify your integration handles all formats: | Content Type | Endpoint | Response Format | | - | - | - | | Daily Product Update | `POST /sources/github/daily-product-update` | Markdown text | | Weekly Product Update | `POST /sources/github/weekly-product-update` | Markdown text | | Blog Post | `POST /sources/github/blog-post` | Markdown text | | LinkedIn Post | `POST /sources/github/linkedin-post` | Plain text | | Twitter Thread | `POST /sources/github/twitter-thread` | Plain text | | Feature Page | `POST /sources/github/feature-page` | JSON | | KB Articles | `POST /sources/github/kb-articles` | JSON | | Changelog | `POST /sources/github/changelog` | JSON | | Release Notes Email | `POST /sources/github/release-notes-email` | JSON | ### Test Publishing After content is generated, test the publish flow: ```bash theme={null} # Publish curl -X POST "https://app.shipstar.ai/api/internal/content/{content_id}/publish" \ -H "Authorization: Bearer YOUR_API_KEY" # Verify it's publicly accessible (no auth needed) curl "https://api.shipstar.ai/api/v1/changelog/{public_slug}" # Check the RSS feed curl "https://api.shipstar.ai/api/v1/changelog/{public_slug}/feed" # Unpublish when done testing curl -X POST "https://app.shipstar.ai/api/internal/content/{content_id}/unpublish" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Test Error Scenarios Verify your application handles these cases: > [accordion] No tracked repositories > > If no GitHub repos are connected, generation will fail. Test that your app handles the error gracefully. > [accordion] No commits in date range > > Use a date range with no activity to test how your app handles empty content. > [accordion] Content generation failure > > Check for `status: "failed"` responses with an `error_message` field. Your polling logic should handle this state. > [accordion] Expired or invalid API key > > Test with a bad key to ensure your app handles 401 responses properly. ## Polling Implementation Since content generation is asynchronous, you'll need to poll for results. Here's a robust implementation: ```javascript JavaScript theme={null} async function generateAndWait(contentType, options = {}) { const API_URL = 'https://app.shipstar.ai/api/internal'; const headers = { 'Authorization': `Bearer ${process.env.SHIPSTAR_API_KEY}`, 'Content-Type': 'application/json' }; // Trigger generation const genResponse = await fetch(`${API_URL}/sources/github/${contentType}`, { method: 'POST', headers, body: JSON.stringify(options) }); const { content_id } = await genResponse.json(); // Poll for result const maxAttempts = 30; const intervalMs = 2000; for (let i = 0; i < maxAttempts; i++) { await new Promise(resolve => setTimeout(resolve, intervalMs)); const statusResponse = await fetch(`${API_URL}/sources/content/${content_id}`, { headers }); const content = await statusResponse.json(); if (content.status === 'completed') { return content; } if (content.status === 'failed') { throw new Error(`Generation failed: ${content.error_message}`); } } throw new Error('Generation timed out'); } // Usage const changelog = await generateAndWait('changelog', { start_date: '2025-03-01T00:00:00Z', end_date: '2025-03-31T23:59:59Z' }); ``` ```python Python theme={null} import time import requests def generate_and_wait(content_type, options=None, max_attempts=30, interval=2): api_url = 'https://app.shipstar.ai/api/internal' headers = { 'Authorization': f'Bearer {os.environ["SHIPSTAR_API_KEY"]}', 'Content-Type': 'application/json' } # Trigger generation response = requests.post( f'{api_url}/sources/github/{content_type}', headers=headers, json=options or {} ) content_id = response.json()['content_id'] # Poll for result for _ in range(max_attempts): time.sleep(interval) result = requests.get( f'{api_url}/sources/content/{content_id}', headers=headers ) content = result.json() if content['status'] == 'completed': return content if content['status'] == 'failed': raise Exception(f"Generation failed: {content.get('error_message')}") raise TimeoutError('Generation timed out') # Usage changelog = generate_and_wait('changelog', { 'start_date': '2025-03-01T00:00:00Z', 'end_date': '2025-03-31T23:59:59Z' }) ``` ## CI/CD Integration ### Automated Testing with GitHub Actions ```yaml theme={null} # .github/workflows/test.yml name: Shipstar Integration Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Run tests env: SHIPSTAR_API_KEY: ${{ secrets.SHIPSTAR_API_KEY }} run: npm test ``` ### Integration Test Example ```javascript theme={null} describe('Shipstar API Integration', () => { const API_URL = 'https://app.shipstar.ai'; const headers = { 'Authorization': `Bearer ${process.env.SHIPSTAR_API_KEY}`, 'Content-Type': 'application/json' }; test('should verify API key is valid', async () => { const response = await fetch(`${API_URL}/api/v1/me`, { headers }); expect(response.status).toBe(200); const user = await response.json(); expect(user.is_active).toBe(true); }); test('should list generated content', async () => { const response = await fetch(`${API_URL}/api/internal/sources/content`, { headers }); expect(response.status).toBe(200); const content = await response.json(); expect(Array.isArray(content)).toBe(true); }); test('should list published changelogs', async () => { const response = await fetch(`${API_URL}/api/v1/changelogs`); expect(response.status).toBe(200); }); }); ``` ## Best Practices > [accordion] Use a dedicated test project > > Create a separate Shipstar project for testing with its own tracked repositories. This keeps test-generated content separate from production. > [accordion] Clean up after testing > > Unpublish and delete test content to avoid cluttering your project: > > ```bash theme={null} > # Unpublish > curl -X POST "https://app.shipstar.ai/api/internal/content/{id}/unpublish" \ > -H "Authorization: Bearer YOUR_API_KEY" > > # Delete > curl -X DELETE "https://app.shipstar.ai/api/internal/sources/content/{id}" \ > -H "Authorization: Bearer YOUR_API_KEY" > ``` > [accordion] Handle all status transitions > > Your polling logic should handle all possible statuses: `pending`, `processing`, `completed`, and `failed`. Don't assume content will always succeed. > [accordion] Test with realistic date ranges > > Use date ranges that include actual commits in your tracked repos. Empty date ranges will produce less useful test results. # Sync Your Users into a Mailing List Source: https://docs.shipstar.ai/guides/sync-mailing-list Keep a Shipstar mailing list in step with your own user base — signup hooks, nightly jobs, Zapier/n8n, and one-off CSV imports ## Overview A Shipstar mailing list is the audience for your release-notes emails. You can fill one by hand in the dashboard, but for a real product the list should mirror your own users: someone signs up and they start getting release notes; they close their account and the emails stop; they change their address and the next issue reaches the new one. The public API gives you three idempotent calls for that, and the dashboard covers the one-off import and export: | Need | Call | | - | - | | Add or update contacts (signup, profile change, nightly reconcile) | [`PUT /email/lists/{id}/recipients`](/api-reference/mailing-lists/upsert-recipients) | | Remove a contact (account closed, plan downgraded) | [`DELETE /email/lists/{id}/recipients/{email-or-external_id}`](/api-reference/mailing-lists/remove-recipient) or the batch `POST .../recipients/remove` | | Check what is on the list | [`GET /email/lists/{id}/recipients`](/api-reference/mailing-lists/list-recipients), [`GET /email/lists/{id}`](/api-reference/mailing-lists/get-mailing-list) | | Initial load or backup | **Import CSV** / **Export CSV** in the dashboard (Destinations → Email → the list), or [`GET .../recipients/export.csv`](/api-reference/mailing-lists/export-recipients) | All calls take an [API token](/guides/authentication) scoped to the project. List ids come from [`GET /email/lists`](/api-reference/mailing-lists/list-mailing-lists), or create one with [`POST /email/lists`](/api-reference/mailing-lists/create-mailing-list). Management calls are free; only content generation spends credits. ## The four things to know **Key by `external_id`.** Send your own user id as `external_id` on every upsert. Shipstar then matches on it first, so when a user changes their email address the same recipient is updated in place instead of a second one appearing. Without it, matching falls back to the email address, which still works for simple lists. **Three statuses.** A recipient is `active` (gets emails), `removed` (you took them off; an upsert puts them back), or `unsubscribed` (they clicked the link in an email). Unsubscribed is terminal: no API call ever changes those rows, they just come back in the response as `skipped_unsubscribed`. Your sync does not need to track opt-outs itself. **Upsert never deletes.** `PUT` only adds and updates. Departed users have to be removed explicitly — either one at a time from your account-deletion path, or by diffing in a reconcile job (below). **Retries are safe.** Every call is idempotent per entry. A re-sent batch reports the repeats as `unchanged` / `already_removed`. Lists hold at most 500 active recipients; an upsert that would cross that line is rejected as a whole with a 400 so you can split it. ## Recipe 1 — Signup and account-deletion hooks The simplest integration: call the API from the code paths where a user is created, updated or deleted. ```python Python theme={null} import os import requests SHIPSTAR = "https://api.shipstar.ai/api/v1" LIST_ID = os.environ["SHIPSTAR_LIST_ID"] HEADERS = {"Authorization": f"Bearer {os.environ['SHIPSTAR_API_TOKEN']}"} def on_user_created_or_updated(user): requests.put( f"{SHIPSTAR}/email/lists/{LIST_ID}/recipients", headers=HEADERS, json={"recipients": [{ "email": user.email, "name": user.full_name, "external_id": str(user.id), "attributes": {"plan": user.plan}, }]}, timeout=10, ).raise_for_status() def on_user_deleted(user): # 404 just means they were never on the list requests.delete( f"{SHIPSTAR}/email/lists/{LIST_ID}/recipients/{user.id}", headers=HEADERS, timeout=10, ) ``` ```typescript TypeScript theme={null} const SHIPSTAR = "https://api.shipstar.ai/api/v1"; const LIST_ID = process.env.SHIPSTAR_LIST_ID!; const headers = { Authorization: `Bearer ${process.env.SHIPSTAR_API_TOKEN}`, "Content-Type": "application/json", }; export async function onUserCreatedOrUpdated(user: { id: string; email: string; name?: string; plan: string }) { await fetch(`${SHIPSTAR}/email/lists/${LIST_ID}/recipients`, { method: "PUT", headers, body: JSON.stringify({ recipients: [{ email: user.email, name: user.name, external_id: user.id, attributes: { plan: user.plan } }], }), }); } export async function onUserDeleted(userId: string) { await fetch(`${SHIPSTAR}/email/lists/${LIST_ID}/recipients/${encodeURIComponent(userId)}`, { method: "DELETE", headers, }); } ``` Run these after your own transaction commits, ideally from a background job so a Shipstar hiccup never fails a signup. If the call fails, retrying later is safe. ## Recipe 2 — Nightly reconcile from your database Hooks can miss events (a bulk import into your own database, a migration). A scheduled job that pushes the current truth and removes what is no longer there catches everything: ```python Python theme={null} import os import requests SHIPSTAR = "https://api.shipstar.ai/api/v1" LIST_ID = os.environ["SHIPSTAR_LIST_ID"] HEADERS = {"Authorization": f"Bearer {os.environ['SHIPSTAR_API_TOKEN']}"} def chunks(items, size): for i in range(0, len(items), size): yield items[i : i + size] def reconcile(users): """`users` is every account that should receive release notes.""" wanted = { str(u.id): {"email": u.email, "name": u.full_name, "external_id": str(u.id)} for u in users } # 1. Push the current truth (adds, updates, reactivates removed ones) for batch in chunks(list(wanted.values()), 1000): r = requests.put( f"{SHIPSTAR}/email/lists/{LIST_ID}/recipients", headers=HEADERS, json={"recipients": batch}, timeout=30, ) r.raise_for_status() # 2. Remove active recipients that are no longer in your user base stale, cursor = [], None while True: params = {"status": "active", "limit": 500, **({"cursor": cursor} if cursor else {})} page = requests.get( f"{SHIPSTAR}/email/lists/{LIST_ID}/recipients", headers=HEADERS, params=params, timeout=30, ).json() stale += [r["external_id"] for r in page["items"] if r["external_id"] and r["external_id"] not in wanted] cursor = page["next_cursor"] if not cursor: break for batch in chunks(stale, 1000): requests.post( f"{SHIPSTAR}/email/lists/{LIST_ID}/recipients/remove", headers=HEADERS, json={"external_ids": batch}, timeout=30, ).raise_for_status() ``` Recipients without an `external_id` (added by hand or by an email-only import) are left alone by step 2, so a human-curated list and a synced list can coexist. Write calls are limited to 30 per minute per IP, which is 30,000 recipients a minute in 1000-entry batches — more than a list can hold. Read the `RateLimit-Remaining` header if you run several syncs from one host. ## Recipe 3 — Zapier, n8n or Make Use the generic HTTP / Webhook action; there is no dedicated Shipstar app and none is needed. 1. **Trigger**: new row in your CRM, new customer in Stripe, new signup in your auth provider — whatever marks someone as a user. 2. **Action**: HTTP request * Method `PUT` * URL `https://api.shipstar.ai/api/v1/email/lists//recipients` * Headers `Authorization: Bearer `, `Content-Type: application/json` * Body (JSON): ```json theme={null} {"recipients": [{"email": "{{email}}", "name": "{{name}}", "external_id": "{{customer_id}}"}]} ``` 3. For the churn side, a second Zap on your "customer deleted / subscription cancelled" trigger sends `DELETE https://api.shipstar.ai/api/v1/email/lists//recipients/{{customer_id}}` with the same header. Map the trigger's stable id to `external_id` so a later email change updates the same recipient. ## Recipe 4 — One-off CSV import or export For an initial load from a spreadsheet, open the list in the dashboard (Destinations → Email) and use **Import CSV**. The importer looks for these headers, case-insensitively: | Column | Accepted headers | | - | - | | Email (required) | `email`, `e-mail`, `email address` | | Name | `name`, `full name`, or `first name` + `last name` | | External id | `external_id`, `external id`, `user_id`, `id` | | Anything else | stored in `attributes` (first 20 columns) | A file with no header row and email addresses in the first column also works. You get a preview with the detected columns and counts before anything is sent, and the import runs through the same upsert as the API, so re-importing an updated file updates rather than duplicates. **Export CSV** on the same page (or `GET .../recipients/export.csv` with a token) downloads every recipient with their status — a backup you can re-import later, or feed into another tool. ## How the emails look to your recipients Release notes emails, and the changelog emails your subscribers get, are sent as your project: the sender reads **Your Project via Shipstar** (from `notifications@shipstar.ai`), the header shows your project name, and the footer says the email was sent by your project via Shipstar. Every email carries a one-click unsubscribe link. Set your sender details under **Destinations → Email → Sender details** in the dashboard: * **Reply-to address** — where a recipient's reply goes. Use your own support or founder inbox. Until you set one, replies go to Shipstar support. * **Postal address** — the physical address shown in the footer. Anti-spam laws such as CAN-SPAM expect the sender's address, so add your company's. Until you set one, the footer shows Shipstar's. ## Checking your work * [`GET /email/lists/{id}`](/api-reference/mailing-lists/get-mailing-list) shows `active_count`, `removed_count` and `unsubscribed_count`; after a sync, `active_count` should match the number of users you pushed. * [`GET /email/lists/{id}/recipients?q=`](/api-reference/mailing-lists/list-recipients) finds one person and shows their status and the fields you wrote. * In the dashboard, the list shows the same counts, and the **Sync via API** button prints ready-to-paste `curl` commands with the list id filled in. ## Related * [Upsert Recipients](/api-reference/mailing-lists/upsert-recipients) — full matching rules and response fields * [Remove Recipients](/api-reference/mailing-lists/remove-recipient) — single and batch forms * [Authentication](/guides/authentication) — creating and using API tokens * [Error Handling](/guides/error-handling) — rate-limit headers and retry guidance # Show Content on Your Website Source: https://docs.shipstar.ai/guides/website-integration Three ways to put your published changelog, blog posts, and knowledge base on your own site ## Overview Publishing a changelog, blog post, or knowledge base article in Shipstar makes it available from the public API under a stable slug. It shows up on **your** website once you add one of the integrations below. Pick by how much you care about search engines and AI crawlers seeing the content: | | Embed widget | Agent-built page | API / RSS | | - | - | - | - | | Effort | Paste one snippet | One prompt in Claude Code | Write your own rendering | | Crawlable / AI-readable | No (renders in the browser) | Yes (server-rendered HTML) | Up to you | | Auto-updates | Shows one published item | Every published period | Every published period | | Needs an API token | No | Yes (server-side) | Yes (server-side) | Every published item's **Add to your website** button in the dashboard (Timeline → the published item, or Destinations → Website) generates these snippets with your slug filled in. ## Embed widget Paste this where the changelog should appear. `data-shipstar-key` is the public slug of the published item — no secrets, safe in client-side HTML. ```html theme={null}
``` The widget renders inside shadow DOM (your page styles can't leak in), follows the visitor's light/dark preference, and includes an email subscribe form for changelogs. ### Options | Attribute | Default | Meaning | | - | - | - | | `data-shipstar-key` | — | Public slug of the published changelog, blog post, or knowledge base set | | `data-type` | `changelog` | `changelog`, `blog`, `kb`, or `banner` | | `data-container` | script's position | `id` of the element to render into | | `data-theme` | `auto` | `auto`, `light`, or `dark` | | `data-max-items` | `10` | Maximum entries shown | | `data-subscribe` | `true` | Show the changelog email subscribe form | | `data-search` | `false` | Search box (knowledge base) | | `data-branding` | `true` | "Powered by Shipstar" link | | `data-initial-article` | — | Knowledge base article slug to open first | | `data-position` | `top` | Banner only: `top` sticks the bar to the top of the page, `inline` renders it where the tag is | | `data-dismissible` | `true` | Banner only: show a close button; a dismissed banner stays hidden until a new one is published | > [!NOTE] > > Because the widget renders in the browser, search engines and AI answer engines (GPTBot, ClaudeBot, PerplexityBot) fetch an empty container. If discoverability matters, use one of the server-side options. > [!TIP] > > The dashboard's "Published" status links to your site once it knows where the content lives: the embed widget's request carries a `Referer`; a server-rendered page should send `X-Shipstar-Page-Url: https://acme.com/changelog` with its API requests (the agent skill does this for you). You can also set the page URL per content type by hand under **Destinations → Website → Page URLs**. ### Announcement banner The widget can also show your project's **announcement banner** — a one-line bar (a "New" badge, headline, one sentence, call-to-action) generated from the period's commits, highlighting the most impactful change. Exactly one banner is live per project, and publishing a new one replaces it under the same key, so this snippet is pasted once: ```html theme={null} ``` Put the tag anywhere (even in ``): the bar mounts at the top of `` and sticks while visitors scroll. `data-position="inline"` renders it where the tag is instead; `data-dismissible="false"` removes the close button (a dismissed banner stays hidden for that visitor until a new one is published). Colours follow CSS variables on the mount element — `--ss-banner-bg` and `--ss-banner-text`, plus `--ss-banner-badge-bg` and `--ss-banner-badge-text` for the badge (they default to the inverse of the bar) — so with `data-container` you can theme it on your own element. The badge reads "New" unless your project sets a different default (Destinations → Website → Banner badge, including off) or the banner re-words it or switches it off (in the draft editor, on the schedule, in the Regenerate dialog, or with `badge` / `show_badge` over MCP). When no banner is live the widget renders nothing. Schedule the banner weekly or monthly from the dashboard's Generate page; the schedule's "Link to" and "Take down after" choices apply to every run. To render it server-side instead, fetch [`GET /api/v1/banner`](/api-reference/banner/get-live-banner) — a `404` means no banner is live. ## Agent-built page (Claude Code) The `build-changelog-page` skill in the Shipstar plugin builds what shipstar.ai's own changelog does: a server-rendered `/changelog` with per-period permalinks, an RSS feed, and structured data, in your site's stack. 1. Create an API token under **API Tokens** in the dashboard and put it in your site's environment as `SHIPSTAR_API_TOKEN` (server-side only — never in client code). 2. In your site's repository: ```text theme={null} /plugin marketplace add shipstar-ai/shipstar-plugin /plugin install shipstar@shipstar ``` 3. Prompt: ```text theme={null} Use the build-changelog-page skill to add a server-rendered /changelog page to this site from my Shipstar changelog. My Shipstar API token is in SHIPSTAR_API_TOKEN. ``` See [Build a changelog page](/mcp/skills/build-changelog-page) for what the skill produces and the invariants it follows. ## API and RSS Render it yourself from the JSON. With an API token the list endpoints return **your** project's published content, newest first: ```bash theme={null} curl https://api.shipstar.ai/api/v1/changelogs \ -H "Authorization: Bearer $SHIPSTAR_API_TOKEN" \ -H "X-Shipstar-Page-Url: https://acme.com/changelog" ``` `X-Shipstar-Page-Url` is optional: it tells Shipstar where you render the content, so the dashboard's "Published" status links to your page. Each period carries `period_start`, `period_end`, `headline`, `entries[]` (`category` is `new`, `improved`, `fixed`, or `breaking`), `slug`, and `commit_count`. Single items are public by slug — `GET /api/v1/changelog/{slug}` — and each has an RSS/Atom feed at `GET /api/v1/changelog/{slug}/feed` that needs no authentication. The project-wide feed, `GET /api/v1/changelogs/feed`, is token-scoped like the list. Reference: [List Changelogs](/api-reference/changelog/list-changelogs), [Get Changelog](/api-reference/changelog/get-changelog), [Changelog Feed](/api-reference/changelog/changelog-feed). # How Does It Work? Source: https://docs.shipstar.ai/how-it-works How Shipstar transforms your GitHub commits into polished marketing content ## Overview Shipstar connects to your GitHub repositories, analyzes your commits, and uses AI to generate marketing content that focuses on what matters to your users — not implementation details. > [!NOTE] > > All content generation happens asynchronously. You trigger a generation request, and Shipstar processes it in the background. ## The Pipeline **1. Connect & Track** Install the Shipstar GitHub App on your organization, choose the repositories it may read, and select which of them to track. Shipstar monitors commits across all your tracked repos using short-lived installation tokens — no personal access token is stored. **2. Analyze Commits** When you trigger content generation, Shipstar fetches commits from all active tracked repositories for the specified date range (default: last 7 days). Each commit's message, author, date, and repository are collected. **3. Generate Content** The commits are passed to Claude along with a specialized prompt for the content type you requested. The AI filters out internal noise (dependency bumps, CI fixes, refactors) and focuses on user-facing changes. **4. Review & Edit** Generated content is saved to your project. You can review, edit, and refine it in the dashboard or via the API before publishing. **5. Publish & Distribute** Publish content to get a public URL with an RSS feed. You can also push directly to Slack channels, or to Intercom and Featurebase for help-center articles. *** ## Content Generation ### How Commits Become Content Shipstar doesn't just summarize your git log. Each content type has a specialized prompt that instructs the AI to: * **Focus on the user perspective** — describe what changed in terms of benefits, not implementation * **Filter out noise** — skip internal refactors, dependency updates, CI/CD changes, code cleanup, and changes to your own marketing site, docs hosting, or SEO. The test is whether your customers can use or benefit from the change. For X threads, LinkedIn posts, feature pages, and banners, if nothing customer-facing shipped in the period, Shipstar posts nothing and refunds the credits rather than dressing up an internal change as a feature * **Match the format** — follow the specific structure and tone for each content type (professional for LinkedIn, concise for Twitter, detailed for blog posts) * **Follow your content guidelines** — a technical-depth preset (technical, business, or mixed) plus free-text instructions you set once per project, e.g. "call users 'members', never 'customers'"; overridable for a single run. See [Content guidelines](/guides/content-guidelines). * **Never mention tools or AI** — the output reads as if written by your team ### Content Types > [card] Daily Product Update > > Daily summary of what shipped in the last 24 hours. Markdown format covering the highlights across all tracked repos. > [card] Weekly Product Update > > Weekly summary of development highlights for non-technical stakeholders. Focused on business outcomes and user impact. > [card] Feature Blog > > SEO-optimized feature announcement (1200-1800 words). Deep dive into the most significant changes with a narrative arc. > [card] Blog Post > > General blog post (600-1000 words). Concise coverage of recent development activity. > [card] LinkedIn Post > > Professional social post (150-300 words). Polished and engaging, ready to share. > [card] Twitter Thread > > 3-5 tweet thread. Each tweet stays under 280 characters. Designed for engagement. > [card] Feature Page > > Structured JSON with hero section, benefits, how-it-works steps, and CTA. Ready to render on your marketing site. > [card] KB Articles > > Categorized help center articles in JSON format. Multiple articles per generation, organized by topic. > [card] Changelog > > Structured JSON changelog with categorized entries (new features, improvements, fixes). Each entry has a title and description. > [card] Release Notes Email > > Email-formatted release notes with subject line, introduction, structured notes, and closing. Ready to send to subscribers. *** ## Scheduling Shipstar supports automated content generation on a schedule using calendar events. ### Recurring Events Set up recurring calendar events to automatically generate content on a cadence: * **Weekly changelog** every Friday * **Monthly blog post** on the 1st * **Daily product update** for stakeholders Events follow the RFC 5545 RRULE standard, so you can configure any recurrence pattern. Each event triggers the specified content type and can be set to auto-publish. *** ## Publishing & Distribution ### Public URLs When you publish content, Shipstar generates a unique public URL. Published content is accessible without authentication: * **Changelogs**: `https://api.shipstar.ai/api/v1/changelog/{slug}` * **Blog Posts**: `https://api.shipstar.ai/api/v1/blog/{slug}` * **KB Articles**: `https://api.shipstar.ai/api/v1/kb/{slug}` ### RSS Feeds Every published changelog and blog post includes an RSS feed, so your users can subscribe to updates: * `https://api.shipstar.ai/api/v1/changelog/{slug}/feed` * `https://api.shipstar.ai/api/v1/blog/{slug}/feed` * `https://api.shipstar.ai/api/v1/blogs/feed` (all blog posts) ### Integrations Push content directly to the tools your team already uses: | Destination | Content Types | How | | - | - | - | | Slack | All types | Publish to any connected Slack channel | | Intercom | KB Articles | Sync help articles directly to your Intercom help center | | Featurebase | Featurebase Articles | Reviewed article sets are created in your Featurebase help center (in the collection you choose) when approved | *** ## Next Steps > [card] [Quickstart](/quickstart) > > Generate your first piece of content in under 5 minutes > [card] [API Reference](/api-reference/overview) > > Explore the full API documentation # Introduction Source: https://docs.shipstar.ai/introduction Automated product marketing for engineers and agents ## Welcome to Shipstar Shipstar turns your GitHub commits into polished marketing content — changelogs, blog posts, social media threads, knowledge base articles, and more. Connect your repos, and Shipstar handles the rest. ### What You Can Build > [card] [Public Changelogs](/api-reference/overview) > > Auto-generate structured changelogs from your commits and publish them with a public URL and RSS feed > [card] [Blog Posts](/api-reference/overview) > > Generate feature announcements, weekly updates, and SEO-optimized blog content > [card] [Social Media](/api-reference/overview) > > Create LinkedIn posts and Twitter/X threads ready to publish > [card] [Knowledge Base](/api-reference/overview) > > Produce categorized help articles from your feature work and publish them to Intercom or Featurebase ## Content Types Shipstar generates 10 types of content from your commits: | Content Type | Format | Use Case | | - | - | - | | Daily Product Update | Markdown | Daily internal update from the last 24 hours of changes | | Weekly Product Update | Markdown | Weekly summary for non-technical stakeholders | | Feature Blog | Markdown | SEO-optimized feature announcement | | Blog Post | Markdown | General blog post | | LinkedIn Post | Text | Professional social media post | | Twitter Thread | Text | 3-5 tweet thread | | Feature Page | JSON | Structured feature page for your marketing site | | KB Articles | JSON | Categorized help center articles | | Public Changelog | JSON | Structured changelog with entries | | Release Notes Email | JSON | Email-formatted release notes | ## Getting Started **1. Create an Account** Sign up at [app.shipstar.ai](https://app.shipstar.ai) to create your account. **2. Connect GitHub** Install the Shipstar GitHub App on your organization and select the repositories you want to track. **3. Generate Content** Trigger content generation from the dashboard or via the API. Shipstar analyzes your recent commits and produces polished content. **4. Publish & Distribute** Publish content to public URLs with RSS feeds, or distribute to Slack, Intercom, and Featurebase directly. ## Key Features > [accordion] AI-Powered Generation > > Shipstar uses Claude to analyze your commits and produce content that focuses on what matters to your users — not implementation details. > [accordion] Publish Anywhere > > Every piece of content gets a public URL and RSS feed. You can also push directly to Slack, Intercom, and Featurebase. > [accordion] Scheduled Content > > Set up recurring calendar events to automatically generate and publish changelogs, blog posts, and more on a schedule. > [accordion] API-First > > Everything available in the dashboard is also available via the API. Generate content, manage repos, and publish programmatically. ## Need Help? > [card] [Quickstart](/quickstart) > > Get up and running in under 5 minutes > [card] [API Reference](/api-reference/overview) > > Explore the full API documentation # get_banner Source: https://docs.shipstar.ai/mcp/banner/get-banner Get the project's live announcement banner Returns the announcement banner currently live for the connected project — the one-line bar (badge, headline, one-sentence body, call-to-action, link) a site shows at the top of the page. Exactly one banner is live per project; publishing a new one replaces it under the same `slug`, which is the embed key. ## Arguments None — the banner is resolved from the connection's project. ## Returns ```json theme={null} { "slug": "acme-banner", "headline": "Scheduled X threads now post themselves", "body": "Approve once; Shipstar posts the thread at the scheduled time.", "cta_label": "See what's new", "badge": "New", "link_url": "https://acme.com/changelog", "period_start": "2026-08-31", "period_end": "2026-09-07", "published_at": "2026-09-07T09:02:11+00:00", "expires_at": null, "commit_count": 14 } ``` ## Errors * `No banner is live for this project.` — never published, replaced, or past its take-down time. Generate one with [`generate_banner`](/mcp/generation/generate-banner). ## Example prompts > "What does our site banner say right now?" > "Is a banner live? If so, when was it published?" # get_blog_post Source: https://docs.shipstar.ai/mcp/blog/get-blog-post Fetch a single published blog post by its public slug Retrieve one published blog post in full, including the complete markdown body. ## Arguments | Name | Type | Description | | - | - | - | | `slug` | string | The `public_slug` returned by [`list_blog_posts`](/mcp/blog/list-blog-posts). | ## Returns A blog post object with the same shape as the items returned by `list_blog_posts`: ```json theme={null} { "slug": "drop-one-script-tag", "content": "# Drop One Script Tag...\n\nIf you've ever...", "category": "Product", "period_start": "2026-03-12", "period_end": "2026-03-19", "published_at": "2026-03-20T09:00:00+00:00", "created_at": "2026-03-19T14:22:11+00:00" } ``` ## Errors Raises an error if no published blog post exists with that slug. ## Example prompts > "Fetch the `drop-one-script-tag` blog post and turn it into a LinkedIn post under 1000 characters." > "Get the full content of our latest blog and extract every code snippet into a list." # list_blog_posts Source: https://docs.shipstar.ai/mcp/blog/list-blog-posts List the project's published blog posts, newest first List every published blog post, ordered from most recent to oldest. ## Arguments This tool takes no arguments. ## Returns An array of blog post objects. Each object contains: | Field | Type | Description | | - | - | - | | `slug` | string | Public slug, usable with [`get_blog_post`](/mcp/blog/get-blog-post). | | `content` | string | Full markdown body of the post. | | `category` | string \| null | Optional category tag. | | `period_start` | string \| null | Start of the period the post covers (`YYYY-MM-DD`). | | `period_end` | string \| null | End of the period the post covers (`YYYY-MM-DD`). | | `published_at` | string \| null | ISO-8601 timestamp of the latest update. | | `created_at` | string \| null | ISO-8601 timestamp when the post was first generated. | ```json theme={null} [ { "slug": "drop-one-script-tag", "content": "# Drop One Script Tag...", "category": "Product", "period_start": "2026-03-12", "period_end": "2026-03-19", "published_at": "2026-03-20T09:00:00+00:00", "created_at": "2026-03-19T14:22:11+00:00" } ] ``` ## Example prompts > "List our published blog posts and tell me which ones cover the embed widget." > "Find the most recent blog post in the `Product` category and paraphrase the opening paragraph." # get_changelog Source: https://docs.shipstar.ai/mcp/changelog/get-changelog Fetch a single published changelog by its public slug Retrieve the full JSON payload for one published changelog. ## Arguments | Name | Type | Description | | - | - | - | | `slug` | string | The `public_slug` returned by [`list_changelogs`](/mcp/changelog/list-changelogs). | ## Returns The parsed changelog JSON object (`headline`, `summary`, `items`, and any other fields Shipstar stored at publish time). ```json theme={null} { "headline": "March 2026 release", "summary": "Content calendar + GitHub-powered generation", "items": [ { "title": "Content Calendar", "description": "..." } ] } ``` ## Errors The tool raises an error if: * No published changelog exists with that slug. * The stored content is not valid JSON. Both conditions return a descriptive message the model can surface to the user or use to retry with a different slug. ## Example prompts > "Get the full release notes for the `march-2026-release` changelog and format them as a markdown bullet list." > "Fetch the latest changelog and draft a tweet announcing the top three items." # list_changelogs Source: https://docs.shipstar.ai/mcp/changelog/list-changelogs List the project's published changelogs, newest first List every published changelog for your project, ordered from most recent to oldest. ## Arguments This tool takes no arguments. ## Returns An array of changelog objects. Each object is the parsed JSON payload of the changelog (`headline`, `summary`, `items`, etc.) plus a `slug` field you can pass to [`get_changelog`](/mcp/changelog/get-changelog). ```json theme={null} [ { "slug": "march-2026-release", "headline": "Content calendar + GitHub-powered generation", "summary": "Your entire content workflow, in one place.", "items": [ { "title": "Content Calendar", "description": "..." }, { "title": "Drafts hub", "description": "..." } ] } ] ``` Entries whose stored JSON is malformed are skipped silently so one bad row never breaks the list. ## Example prompts > "What features did we ship this month? Use the Shipstar tools." > "Summarise our last three releases into bullet points I can paste into a weekly update." > "Find every changelog entry that mentions webhooks and quote the relevant item descriptions." # send_release_email Source: https://docs.shipstar.ai/mcp/email/send-release-email Send a completed release notes email to the selected mailing lists Sends a completed release notes email to the selected mailing lists. Side effect: marks the content published and queues one email per active recipient — recipients are deduplicated across lists, and each email carries a one-click unsubscribe link. **Each content record can be sent at most once, ever** — a repeat call fails rather than double-sending. Only call this after the user has confirmed the draft and the target lists. ## Arguments | Name | Type | Description | | - | - | - | | `content_id` | string (UUID) | A `completed` record from [`generate_release_notes_email`](/mcp/generation/generate-release-notes-email). | | `mailing_list_ids` | array of strings (UUIDs) | One or more list ids from [`list_mailing_lists`](/mcp/project/list-mailing-lists). | ## Returns ```json theme={null} { "status": "queued", "recipient_count": 347 } ``` `recipient_count` is the deduplicated active audience across the selected lists. Delivery runs on a background queue, so emails go out over the following minutes. ## Errors * `content_id and mailing_list_ids must be UUIDs (from generate_release_notes_email and list_mailing_lists)` * `Select at least one mailing list.` * `Content not found` * `Content is not a release notes email` — only `release_notes_email` records can be sent. * `Content is not ready for sending` — wait for [`get_generation_status`](/mcp/lifecycle/get-generation-status) to report `completed`. * `This release notes email has already been sent` — each record sends once; generate a new one for another send. * `No valid mailing lists selected` — none of the given ids belong to this project. ## Example prompts > "The release email draft is approved — send it to the Customers list." > "Send that release notes email to both the Customers and Beta testers lists." # generate_banner Source: https://docs.shipstar.ai/mcp/generation/generate-banner Kick off generation of an announcement banner from recent commits Analyses commits on the project's tracked GitHub repositories and kicks off a background task that writes an announcement banner: the single most impactful user-facing change of the period as a `headline` (≤ 80 chars), a one-sentence `body` (≤ 160 chars), and a `cta_label` (≤ 24 chars). Returns immediately with a pending content record. Costs 25 credits. Publishing the draft ([`publish_content`](/mcp/lifecycle/publish-content)) makes it the project's live banner, **replacing the current one** under the same embed key — the site's snippet needs no change. Read it back with [`get_banner`](/mcp/banner/get-banner). ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to include. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to include. | | `product_name` | string | project name | Override the product name used in the banner. | | `idea` | object | — | Optional `{title, summary}` suggestion to focus the banner on; commits unrelated to the idea are ignored. | | `link_url` | string (http/https) | resolved at publish | Where the call-to-action points. Omit to fall back to the project's configured Changelog page (Destinations → Website), then the period's published changelog permalink; with neither, the banner renders without a link. | | `expires_after_days` | `7` \| `14` \| `30` | — | Stop serving the banner this many days after publication. Omit to keep it live until the next banner replaces it. | | `badge` | string (≤ 16 chars) | project default | The short tag shown before the headline (e.g. `"Beta"`, `"v2.0"`). Omit to use the project's default badge (Destinations → Website), which is `"New"` unless changed. | | `show_badge` | boolean | `true` | Pass `false` to render the banner without a badge, whatever the project default. | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). | ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "status": "pending" } ``` If nothing customer-facing shipped in the period (only marketing-site, docs-hosting, CI, or refactor commits), the run ends `failed` with `skip_reason: "no_customer_facing_changes"`, the credits are refunded, and the current banner stays up — widen the date range or pass `instructions` if that change should have counted. Poll [`get_generation_status`](/mcp/lifecycle/get-generation-status) until the status is `completed`, then read the draft with [`get_content_draft`](/mcp/lifecycle/get-content-draft). The draft is JSON (`badge`, `headline`, `body`, `cta_label`, `link_url` — an empty `badge` means none); edit it with [`update_content`](/mcp/lifecycle/update-content) before publishing — banner drafts are validated and clamped on save (headline 80, body 160, `cta_label` 24, `badge` 16 characters; `link_url` an http(s) URL or `null`), and the draft's badge becomes the prefill for a later regenerate. ## Errors * `No GitHub account connected. Connect GitHub in Sources first.` * `No active tracked repositories. Add repos in Sources first.` * `Invalid banner options: link_url must be an http(s) URL, expires_after_days one of 7, 14, or 30, and badge at most 16 characters.` * `Monthly credit limit reached (10/10 used). Upgrade your plan to keep generating content.` ## Example prompts > "Put a banner on our site about the biggest thing we shipped this week, linking to the changelog." > "Generate a banner for the new API keys page, take it down after 14 days." > "Banner for the beta of the new editor — tag it 'Beta' instead of 'New'." # generate_blog_post Source: https://docs.shipstar.ai/mcp/generation/generate-blog-post Kick off a blog post generation from recent commits Kicks off background generation of a blog post drawing from commits on the project's tracked repositories. ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to consider. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to consider. | | `product_name` | string | project name | Override the product name. | | `idea` | object | *none* | Optional idea to steer the post. See below. | | `blog_options` | object | *none* | `{ "length": "short" \| "long", "seo": boolean, "focus": "multi_feature" \| "single_feature" }` — see below. | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology, e.g. `"British spelling; do announce docs-site changes"`. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). Omit to use every active tracked repo. | ### `idea` object Pair with [`generate_blog_post_ideas`](/mcp/generation/generate-blog-post-ideas) to pick an angle first, then pass one of the returned ideas back here. ```json theme={null} { "title": "Ship faster with webhook retries", "summary": "We shipped per-endpoint retry budgets this week...", "angle": "Reliability story for platform teams", "category": "Engineering" } ``` ### `blog_options` object | Key | Values | Default | | - | - | - | | `length` | `"short"` (600–1000 words) or `"long"` (1200–1800 words) | `"short"` | | `seo` | `true` to include a target keyword, meta description, and related topics | `false` | | `focus` | `"multi_feature"` (roundup) or `"single_feature"` (deep-dive on one feature) | `"multi_feature"` | `blog_options.focus` controls *breadth*; to steer *what to include or leave out*, use `instructions`. ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "status": "pending" } ``` Poll `list_blog_posts` / `get_blog_post` once the background task completes. ## Example prompts > "Brainstorm three blog post ideas from this week's commits, pick the one about the new webhook retries, and kick off generation." > "Draft a blog post covering everything we shipped in March." # generate_blog_post_ideas Source: https://docs.shipstar.ai/mcp/generation/generate-blog-post-ideas Brainstorm blog post angles from recent commits Unlike the other generation tools, this call is **synchronous** — it returns a list of blog post ideas immediately without creating a background task or persisting anything. Use it as the first step in a two-step flow: brainstorm ideas, let the user (or the model) pick one, then pass that idea back to [`generate_blog_post`](/mcp/generation/generate-blog-post). ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to consider. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to consider. | | `product_name` | string | project name | Override the product name. | ## Returns ```json theme={null} { "ideas": [ { "title": "Ship faster with webhook retries", "summary": "We shipped per-endpoint retry budgets this week, and here's why it matters.", "angle": "Reliability story for platform teams", "category": "Engineering" } ] } ``` Each idea is a valid `idea` payload for `generate_blog_post`. ## Example prompts > "Give me three blog post angles from what we shipped last week." > "Brainstorm ideas, pick the most user-facing one, and kick off generation." # generate_changelog Source: https://docs.shipstar.ai/mcp/generation/generate-changelog Kick off a public changelog generation from recent commits Analyses commits on the project's tracked GitHub repositories and kicks off a background task that generates a public changelog. Returns immediately with a pending content record. ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to include. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to include. | | `product_name` | string | project name | Override the product name used in the generated header. | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology, e.g. `"British spelling; do announce docs-site changes"`. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). Omit to use every active tracked repo. | ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "status": "pending" } ``` Poll [`get_generation_status`](/mcp/lifecycle/get-generation-status) until the status is `completed`, then read the draft with [`get_content_draft`](/mcp/lifecycle/get-content-draft); once published it also appears in `list_changelogs` / `get_changelog` and the REST API. A changelog is only produced when the period contains customer-facing product changes. If nothing shipped that customers can use (only marketing-site, docs-hosting, CI, or refactor commits), the run ends `failed` with `skip_reason: "no_customer_facing_changes"`, no "quiet week" entry is created or published, and the credits are refunded — a scheduled occurrence ends `skipped` and is not re-drafted. Widen the date range, or pass `instructions` if that change should have counted. ## Errors * `No GitHub account connected` — connect GitHub in Sources first. * `No active tracked repositories` — add at least one active repo in Sources. ## Example prompts > "Generate a changelog covering everything we shipped in the last two weeks." > "Kick off a changelog for the 2026-03-01 → 2026-03-15 window." # generate_feature_page Source: https://docs.shipstar.ai/mcp/generation/generate-feature-page Kick off generation of a marketing feature landing page Generates a marketing-style feature landing page from recent commits. Background task; returns a pending content record. ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to include. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to include. | | `product_name` | string | project name | Override the product name. | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology, e.g. `"British spelling; do announce docs-site changes"`. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). Omit to use every active tracked repo. | ## Returns ```json theme={null} { "content_id": "...", "status": "pending" } ``` The page covers the single most impactful customer-facing feature of the period. If nothing customer-facing shipped (only marketing-site, docs-hosting, CI, or refactor commits), the run ends `failed` with `skip_reason: "no_customer_facing_changes"` in [`get_generation_status`](/mcp/lifecycle/get-generation-status), nothing is generated, and the credits are refunded — widen the date range or pass `instructions` if that change should have counted. ## Example prompts > "Draft a feature landing page for what we shipped this sprint." # generate_featurebase_articles Source: https://docs.shipstar.ai/mcp/generation/generate-featurebase-articles Kick off generation of Featurebase help-center articles from recent commits Turns recent shipped work into a set of help-center articles for your Featurebase help center. Background task; returns a pending content record. The draft is a set of articles for review. Approving it ([`approve_content`](/mcp/lifecycle/approve-content)) creates the articles in the project's connected Featurebase help center — in the collection the schedule chose, else the Featurebase destination's default collection — and only then does the content read "published". A failed delivery (rejected API key, missing collection) keeps the set in review with the error shown in the dashboard. Featurebase must be connected first: Dashboard → Destinations → Featurebase, with an organization-level API key. [`list_destinations`](/mcp/project/list-destinations) shows `featurebase.connected` and the default collection. ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to include. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to include. | | `product_name` | string | project name | Override the product name. | | `idea` | object | — | Optional `{title, summary}` suggestion for the article(s) to write (e.g. a specific feature to document). | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology, e.g. `"British spelling; do announce docs-site changes"`. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). Omit to use every active tracked repo. | | `seed` | boolean | `false` | **Seed mode** — build the initial help center instead of a what-changed set (see below). 300 credits. | | `max_articles` | integer (5–30) | 20 | Seed mode: upper bound on proposed articles. | ## Seed mode A normal run documents one commit window. When a help center is empty, pass `seed: true` to document the product as it is: Shipstar reads the tracked repositories' README and docs folders (up to 24 markdown files) plus the last six months of commits, lists what is already in the Featurebase help center, and asks for an outline of 3–6 collections and up to `max_articles` articles — skipping titles that already exist. The articles are then written in batches and assembled into one reviewable set, grouped by collection. On approve, each article is filed under its collection; collections that don't exist yet are created in the help center. The set-level collection (schedule or default) only applies to articles without one. Seed mode is meant as a one-off; run it again later and it proposes only topics the help center doesn't cover yet. ## Returns ```json theme={null} { "content_id": "...", "status": "pending" } ``` Poll [`get_generation_status`](/mcp/lifecycle/get-generation-status), then read the draft with [`get_content_draft`](/mcp/lifecycle/get-content-draft) and approve it with [`approve_content`](/mcp/lifecycle/approve-content). Once delivered, the content's metadata carries one entry per created article (`article_id`, `url`) under `featurebase.articles`. ## Example prompts > "Write Featurebase help articles for everything we shipped this sprint." > "Turn the last two weeks of commits into how-to articles for our help center." > "Our Featurebase help center is empty — seed it from the repo docs, at most 15 articles." # generate_kb_articles Source: https://docs.shipstar.ai/mcp/generation/generate-kb-articles Kick off generation of knowledge base articles from recent commits Turns recent shipped work into a set of knowledge base articles. Background task; returns a pending content record. ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to include. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to include. | | `product_name` | string | project name | Override the product name. | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology, e.g. `"British spelling; do announce docs-site changes"`. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). Omit to use every active tracked repo. | ## Returns ```json theme={null} { "content_id": "...", "status": "pending" } ``` Poll [`list_kb_article_sets`](/mcp/kb/list-kb-article-sets) / [`get_kb_article_set`](/mcp/kb/get-kb-article-set) once the background task completes. ## Example prompts > "Generate KB articles for everything we shipped in March." > "Take the last 30 days of commits and turn them into how-to articles." # generate_linkedin_post Source: https://docs.shipstar.ai/mcp/generation/generate-linkedin-post Kick off generation of a LinkedIn post from recent commits Analyses commits on the project's tracked GitHub repositories and kicks off a background task that generates a LinkedIn post. Returns immediately with a pending content record. ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to include. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to include. | | `product_name` | string | project name | Override the product name used in the post. | | `idea` | object | — | Optional `{title, summary}` suggestion to focus the post on; commits unrelated to the idea are ignored. | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology, e.g. `"British spelling; do announce docs-site changes"`. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). Omit to use every active tracked repo. | ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "status": "pending" } ``` Poll [`get_generation_status`](/mcp/lifecycle/get-generation-status) until the status is `completed`, then read the draft with [`get_content_draft`](/mcp/lifecycle/get-content-draft). The post covers the single most impactful customer-facing feature of the period. If nothing customer-facing shipped (only marketing-site, docs-hosting, CI, or refactor commits), the run ends `failed` with `skip_reason: "no_customer_facing_changes"`, nothing is generated, and the credits are refunded — widen the date range or pass `instructions` if that change should have counted. ## Errors * `No GitHub account connected. Connect GitHub in Sources first.` * `No active tracked repositories. Add repos in Sources first.` * `Monthly credit limit reached (10/10 used). Upgrade your plan to keep generating content.` ## Example prompts > "Draft a LinkedIn post announcing what we shipped in July." > "Generate a LinkedIn post about the new team-invites feature — commits from the last two weeks." # generate_release_notes_email Source: https://docs.shipstar.ai/mcp/generation/generate-release-notes-email Kick off generation of a release notes email from recent commits Analyses commits on the project's tracked GitHub repositories and kicks off a background task that generates a release notes email. Returns immediately with a pending content record. ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to include. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to include. | | `product_name` | string | project name | Override the product name used in the email. | | `idea` | object | — | Optional `{title, summary}` suggestion to focus the email on; commits unrelated to the idea are ignored. | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology, e.g. `"British spelling; do announce docs-site changes"`. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). Omit to use every active tracked repo. | ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "status": "pending" } ``` Poll [`get_generation_status`](/mcp/lifecycle/get-generation-status) until the status is `completed`, review the draft with [`get_content_draft`](/mcp/lifecycle/get-content-draft), then deliver it to mailing lists with [`send_release_email`](/mcp/email/send-release-email). ## Errors * `No GitHub account connected. Connect GitHub in Sources first.` * `No active tracked repositories. Add repos in Sources first.` * `Monthly credit limit reached (10/10 used). Upgrade your plan to keep generating content.` ## Example prompts > "Draft a release notes email covering everything we shipped this sprint." > "Generate a release email for the 2026-07-01 → 2026-07-31 window, focused on the new billing dashboard." # generate_twitter_thread Source: https://docs.shipstar.ai/mcp/generation/generate-twitter-thread Kick off generation of an X (Twitter) thread from recent commits Analyses commits on the project's tracked GitHub repositories and kicks off a background task that generates an X (Twitter) thread. Returns immediately with a pending content record. ## Arguments | Name | Type | Default | Description | | - | - | - | - | | `start_date` | string (ISO-8601) | 7 days ago | Earliest commit timestamp to include. | | `end_date` | string (ISO-8601) | now | Latest commit timestamp to include. | | `product_name` | string | project name | Override the product name used in the thread. | | `idea` | object | — | Optional `{title, summary}` suggestion to focus the thread on; commits unrelated to the idea are ignored. | | `audience` | `"technical"` \| `"business"` \| `"mixed"` | project default | Technical depth of the writing. Omit to use the project's [content guidelines](/guides/content-guidelines) default. | | `instructions` | string (≤ 2000 chars) | project default | Free-text guidance on scope, tone, or terminology, e.g. `"British spelling; do announce docs-site changes"`. Omit to use the project default. | | `repos` | string\[] | all tracked repos | Restrict the commit source to these tracked repositories (full names, e.g. `["acme/backend"]`), as listed by [`get_project_context`](/mcp/project/get-project-context). Omit to use every active tracked repo. | ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "status": "pending" } ``` Poll [`get_generation_status`](/mcp/lifecycle/get-generation-status) until the status is `completed`, then read the draft with [`get_content_draft`](/mcp/lifecycle/get-content-draft). The thread covers the single most impactful customer-facing feature of the period. If nothing customer-facing shipped (only marketing-site, docs-hosting, CI, or refactor commits), the run ends `failed` with `skip_reason: "no_customer_facing_changes"`, nothing is posted, and the credits are refunded — widen the date range or pass `instructions` if that change should have counted. ## Errors * `No GitHub account connected. Connect GitHub in Sources first.` * `No active tracked repositories. Add repos in Sources first.` * `Monthly credit limit reached (10/10 used). Upgrade your plan to keep generating content.` ## Example prompts > "Write an X thread about what we shipped this week." > "Generate a Twitter thread focused on the new API keys page, using commits from the last month." # get_kb_article_set Source: https://docs.shipstar.ai/mcp/kb/get-kb-article-set Fetch a single published knowledge base article set by slug Retrieve one published knowledge base article set, including the full content of every article in the set. ## Arguments | Name | Type | Description | | - | - | - | | `slug` | string | The `public_slug` returned by [`list_kb_article_sets`](/mcp/kb/list-kb-article-sets). | ## Returns ```json theme={null} { "slug": "shipstar-knowledge-base", "articles": [ { "id": "1", "title": "Editing Blog Post Drafts", "content": "You can now edit generated blog post drafts..." }, { "id": "2", "title": "Connecting GitHub", "content": "To connect a repository..." } ], "published_at": "2026-04-01T12:00:00+00:00", "created_at": "2026-03-28T09:15:00+00:00" } ``` ## Errors Raises an error if: * No published KB set exists with that slug. * The stored content is not valid JSON. ## Example prompts > "Get the `shipstar-knowledge-base` set and answer: 'How do I edit a blog post draft?' using only the content of the articles." > "Fetch our KB set and list every article title as a table of contents." # list_kb_article_sets Source: https://docs.shipstar.ai/mcp/kb/list-kb-article-sets List the project's published knowledge base article sets, newest first List every published knowledge base article set. Each "set" bundles multiple articles under a single slug so you can group related how-tos together. ## Arguments This tool takes no arguments. ## Returns An array of article set objects: | Field | Type | Description | | - | - | - | | `slug` | string | Public slug, usable with [`get_kb_article_set`](/mcp/kb/get-kb-article-set). | | `articles` | array | List of article objects (`id`, `title`, `content`, ...). | | `published_at` | string \| null | ISO-8601 timestamp of the latest update. | | `created_at` | string \| null | ISO-8601 timestamp when the set was first generated. | ```json theme={null} [ { "slug": "shipstar-knowledge-base", "articles": [ { "id": "1", "title": "Editing Blog Post Drafts with Rich Text Formatting", "content": "You can now edit generated blog post drafts..." } ], "published_at": "2026-04-01T12:00:00+00:00", "created_at": "2026-03-28T09:15:00+00:00" } ] ``` Sets whose stored content is not valid JSON are skipped silently — bad data never breaks the list. ## Example prompts > "List our KB sets and tell me which articles cover authentication." > "Find every KB article that mentions 'webhook' and give me the article titles." # approve_content Source: https://docs.shipstar.ai/mcp/lifecycle/approve-content Approve generated content for publication Approves generated content for publication — the same transition as the dashboard review flow. Side effect: if the associated review event's scheduled time has already passed, the content is **published immediately**; otherwise it is marked `approved` and the scheduler publishes it when the time arrives. Dispatches the `content.approved` webhook. Only call this after the user has reviewed the draft and confirmed. ## Arguments | Name | Type | Description | | - | - | - | | `content_id` | string (UUID) | The `content_id` returned by a `generate_*` tool. | ## Returns The updated status payload — `review_status` is `approved` (scheduled time still ahead) or `published` (time already passed, published on the spot): ```json theme={null} { "content_id": "7f5e2c1a-...", "output_type": "public_changelog", "status": "completed", "error_message": null, "review_status": "published", "is_published": true, "published_at": "2026-08-10T15:20:04+00:00", "public_slug": "august-2026-release", "created_at": "2026-08-10T14:02:11+00:00" } ``` For on-demand generations that have no review event, use [`publish_content`](/mcp/lifecycle/publish-content) instead. ## Errors * `No review event found for this content — it may not be reviewable through this flow.` — use [`publish_content`](/mcp/lifecycle/publish-content). * `Event cannot be approved (current status: published)` — only events in `ready_for_review` or `scheduled` can be approved. * `Invalid content_id (must be a UUID): abc123` * `Content not found: 7f5e2c1a-...` ## Example prompts > "The draft looks good — approve it." > "Approve the changelog so it goes out at its scheduled time on Friday." # get_content_draft Source: https://docs.shipstar.ai/mcp/lifecycle/get-content-draft Read the full draft of a generated content record Reads the full draft of a generated content record — the same status payload as [`get_generation_status`](/mcp/lifecycle/get-generation-status) plus the draft itself. ## Arguments | Name | Type | Description | | - | - | - | | `content_id` | string (UUID) | The `content_id` returned by a `generate_*` tool. | ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "output_type": "blog_post", "status": "completed", "error_message": null, "review_status": "ready_for_review", "is_published": false, "published_at": null, "public_slug": null, "created_at": "2026-08-10T14:02:11+00:00", "content": "# Shipping faster with ...", "category": "product", "from_date": "2026-08-03T00:00:00+00:00", "to_date": "2026-08-10T00:00:00+00:00" } ``` `content` is markdown for most types, or a JSON string for structured types (changelogs, KB article sets, release notes emails). `from_date` / `to_date` are the commit window the generation covered. Revise the draft with [`update_content`](/mcp/lifecycle/update-content). ## Errors * `Invalid content_id (must be a UUID): abc123` * `Content not found: 7f5e2c1a-...` — no record with that ID exists in this project. ## Example prompts > "Show me the draft of the release email that just finished." > "Read the blog post draft and summarise it in three bullets before I approve it." # get_generation_status Source: https://docs.shipstar.ai/mcp/lifecycle/get-generation-status Check the status of a content record kicked off by a generate_* tool Checks the generation and review state of a content record kicked off by any `generate_*` tool. Poll every 20–30 seconds; generation normally finishes within a few minutes. ## Arguments | Name | Type | Description | | - | - | - | | `content_id` | string (UUID) | The `content_id` returned by a `generate_*` tool. | ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "output_type": "release_notes_email", "status": "completed", "error_message": null, "skip_reason": null, "review_status": "ready_for_review", "is_published": false, "published_at": null, "public_slug": null, "created_at": "2026-08-10T14:02:11+00:00" } ``` * `status` is the generation state: `pending` → `processing` → `completed`, or `failed` (with the failure reason in `error_message`). * `skip_reason` is `"no_customer_facing_changes"` when a single-feature run (X thread, LinkedIn post, feature page, banner) or a public changelog found nothing customer-facing in the period and declined to post: `status` is `failed`, `error_message` reads "nothing to post", nothing was published, and the credits were refunded. Don't retry as-is — widen the date range, or pass `instructions` such as "do announce docs-site changes" if that change should count. It is `"no_commits"` when the period had no commits at all in the selected repos (any output type) — same outcome: `status` `failed`, nothing published, credits refunded; widen the date range or pick a repo with activity. `null` otherwise. * `review_status` is the publication state of the associated review event, when one exists: `ready_for_review`, `approved`, `published`, or `skipped` (also the state of a scheduled occurrence the generator declined). It is `null` for records with no review event. Once `status` is `completed`, read the draft with [`get_content_draft`](/mcp/lifecycle/get-content-draft), then move it forward with [`approve_content`](/mcp/lifecycle/approve-content) or [`publish_content`](/mcp/lifecycle/publish-content). ## Errors * `Invalid content_id (must be a UUID): abc123` * `Content not found: 7f5e2c1a-...` — no record with that ID exists in this project. ## Example prompts > "Is the changelog I kicked off done yet?" > "Check the status of content 7f5e2c1a and tell me if it failed." # publish_content Source: https://docs.shipstar.ai/mcp/lifecycle/publish-content Publish a completed changelog, blog post, or KB article set directly Publishes a completed changelog, blog post, or KB article set directly — for on-demand generations that have no review event (where [`approve_content`](/mcp/lifecycle/approve-content) fails). Assigns a public slug and makes the content live immediately. Only call this after the user has reviewed the draft and confirmed. **KB caveat:** exactly one knowledge base set is live per project — publishing a `kb_articles` record replaces the project's current set and inherits its slug, so API URLs and embed snippets keep working. To add articles instead of replacing the whole KB, first merge the live set's articles into your draft with [`get_kb_article_set`](/mcp/kb/get-kb-article-set) + [`update_content`](/mcp/lifecycle/update-content), then publish the merged record. ## Arguments | Name | Type | Description | | - | - | - | | `content_id` | string (UUID) | The `content_id` returned by a `generate_*` tool. | ## Returns ```json theme={null} { "content_id": "7f5e2c1a-...", "output_type": "blog_post", "is_published": true, "published_at": "2026-08-10T15:20:04+00:00", "public_slug": "shipping-faster-with-shipstar", "public_url": "https://api.shipstar.example/api/v1/blog/shipping-faster-with-shipstar" } ``` `public_url` is the live public endpoint for the content (`/api/v1/changelog/{slug}`, `/api/v1/blog/{slug}`, or `/api/v1/kb/{slug}` depending on type). ## Errors * `Only changelogs, blog posts, and KB articles can be published` — other types go through [`approve_content`](/mcp/lifecycle/approve-content) or [`send_release_email`](/mcp/email/send-release-email). * `Only completed content can be published` — wait for [`get_generation_status`](/mcp/lifecycle/get-generation-status) to report `completed`. * `Invalid content_id (must be a UUID): abc123` * `Content not found` ## Example prompts > "Publish that blog post now and give me the public URL." > "Ship the changelog draft live immediately — no need to schedule it." # update_content Source: https://docs.shipstar.ai/mcp/lifecycle/update-content Revise the draft text and/or category of a generated content record Revises the draft text and/or category of a generated content record. The replacement is **wholesale** — pass the complete revised text, not a diff. For structured types (changelogs, KB article sets, release notes emails) the draft must remain JSON in the same shape as the original; pass it as a JSON string or object. Note that edits to already-published content update the public copy immediately. ## Arguments | Name | Type | Description | | - | - | - | | `content_id` | string (UUID) | The `content_id` returned by a `generate_*` tool. | | `content` | string or object | Replacement draft text (or JSON object for structured types). Optional if `category` is given. | | `category` | string | Replacement category label. Optional if `content` is given. | ## Returns The updated record's status payload: ```json theme={null} { "content_id": "7f5e2c1a-...", "output_type": "public_changelog", "status": "completed", "error_message": null, "review_status": "ready_for_review", "is_published": false, "published_at": null, "public_slug": null, "created_at": "2026-08-10T14:02:11+00:00" } ``` Typical flow: read with [`get_content_draft`](/mcp/lifecycle/get-content-draft), revise here, then [`approve_content`](/mcp/lifecycle/approve-content) or [`publish_content`](/mcp/lifecycle/publish-content). ## Errors * `Provide \`content\` and/or \`category\` to update.\` — called with neither argument. * `Invalid content_id (must be a UUID): abc123` * `Content not found: 7f5e2c1a-...` — no record with that ID exists in this project. ## Example prompts > "Tighten the intro paragraph of that blog draft and save it back." > "Rewrite the changelog headline to mention the API keys page, keeping the JSON structure intact." # OAuth Source: https://docs.shipstar.ai/mcp/oauth Connect Shipstar to claude.ai and other MCP clients with OAuth 2.1 — plus protocol details for client builders Shipstar's backend is a full OAuth 2.1 authorization server. Off-the-shelf MCP clients — claude.ai custom connectors, Claude Code, Claude Desktop — handle the whole flow automatically: add the server URL, sign in, pick a project, done. The first half of this page covers that; the second half is protocol detail for people building their own client or debugging a connection. ## Connecting from claude.ai (custom connector) 1. In claude.ai, go to **Settings → Connectors → Add custom connector** and enter: ``` https://mcp.shipstar.ai/mcp ``` 2. Click **Connect**. Your browser opens Shipstar's consent page (sign in first if needed). 3. **Pick the project** the connection should operate on — every authorization is scoped to exactly one Shipstar project — and approve. 4. You're returned to claude.ai with the 23 Shipstar tools available in any chat. To operate on a different project, add another connector authorization (or re-authorize) and choose the other project at consent time. You can revoke a connection at any time from the Shipstar dashboard or by removing the connector in claude.ai. Claude Code uses the same flow without any token handling: ```bash theme={null} claude mcp add --transport http shipstar https://mcp.shipstar.ai/mcp ``` > [!NOTE] > > Prefer a static token for scripts or CI? Dashboard API tokens work as plain bearer tokens on the same endpoint — see the [MCP overview](/mcp/overview#authentication). ## Discovery Standard metadata at the `mcp.shipstar.ai` domain root, also served with the resource path appended (RFC 8414 / RFC 9728 path-insertion): ``` https://mcp.shipstar.ai/.well-known/oauth-authorization-server (RFC 8414) https://mcp.shipstar.ai/.well-known/oauth-authorization-server/mcp https://mcp.shipstar.ai/.well-known/oauth-protected-resource (RFC 9728) https://mcp.shipstar.ai/.well-known/oauth-protected-resource/mcp ``` The OAuth endpoints themselves live on the API host (`https://api.shipstar.ai/oauth/...`), as advertised in the metadata — both hostnames are served by the same backend, so the documents resolve on either. Endpoints: `/oauth/authorize`, `/oauth/token`, `/oauth/register`, `/oauth/revoke`. Unauthenticated requests to `/mcp` return `401` with a `WWW-Authenticate` challenge pointing at the protected-resource metadata, so spec-compliant clients bootstrap the flow from a bare URL. ## Client registration Dynamic client registration (RFC 7591) is open — `POST /oauth/register` with your client metadata (rate-limited per IP). Redirect URIs must match **exactly** at authorization time. Public clients (`token_endpoint_auth_method: "none"`, the default) are supported for native/desktop apps; confidential clients receive a `client_secret` (stored hashed, `client_secret_expires_at: 0` = non-expiring). Client auth at `/token` is accepted as form parameters or HTTP Basic. ## Authorization * **PKCE S256 is required** on every authorization request (`code_challenge_method=S256`; `plain` is rejected). * `/oauth/authorize` does not itself log the user in: it validates the request, then redirects to Shipstar's consent page (`app.shipstar.ai/oauth/authorize`), which is login-gated. The consent page shows your client's name and logo and asks the user to **select a project** — the minted grant is scoped to that project. * On approval the user agent returns to your redirect URI with `code` + `state`; denial returns `error=access_denied`. * Consent tickets expire after 10 minutes; authorization codes expire after 10 minutes and are single-use. ## Scope A single scope: `mcp`. It is granted by default when unspecified and covers all 23 tools. Effective access control is the per-project grant chosen at consent — a token can only ever see the one project it was authorized for. ## Tokens | Token | Prefix | Lifetime | | - | - | - | | Access | `shipstar_at_` | 60 minutes | | Refresh | `shipstar_rt_` | 30 days | Tokens are opaque (not JWTs) and stored hashed server-side. **Refresh rotation is strict with replay detection**: each refresh revokes the old pair and issues a new one, and presenting an already-revoked refresh token revokes the entire token chain for that grant. Store the new pair from every refresh response. ## Revocation RFC 7009 `POST /oauth/revoke` — always returns `200`, even for unknown tokens. Expect calls to start failing with `401` at any time (user revocation, chain revocation after replay); re-run the authorization flow to reconnect. # Shipstar MCP Server Overview Source: https://docs.shipstar.ai/mcp/overview Use Shipstar's published content directly from LLM agents via the Model Context Protocol Shipstar exposes its full pipeline as [Model Context Protocol](https://modelcontextprotocol.io) (MCP) tools so LLM agents — claude.ai, Claude Code, Claude Desktop, Cursor, Windsurf, custom agents, and anything else that speaks MCP — can generate marketing content from your commits, review and revise drafts, publish them, and send release emails, as well as read your published changelogs, blog posts, and knowledge base articles. Same services, same guarantees as the dashboard — the tools call the identical review/publish/delivery code paths — surfaced as typed tools the model can call directly. ## Endpoint The MCP server is mounted on the main Shipstar backend using the **Streamable HTTP** transport. ``` POST https://mcp.shipstar.ai/mcp ``` ## Authentication Two ways in: **OAuth 2.1 (recommended — what claude.ai uses).** Add the endpoint as a custom connector and sign in; no token handling needed. Each authorization is scoped to **one project**, chosen on the consent screen. Full flow and protocol details: [OAuth](/mcp/oauth). **Static API token (for scripts and CI).** Every dashboard API token works as a bearer token on `/mcp` — the same tokens that protect the [V1 REST API](/api-reference/authentication). Create one at **Settings → API Tokens** (`/dashboard/api-tokens`); the token is bound to the project it was created in. ``` Authorization: Bearer YOUR_API_TOKEN ``` Requests without a token, or with an invalid/expired token, return `401 Unauthorized` with a `WWW-Authenticate` challenge that lets OAuth-capable clients bootstrap automatically. ## Connecting from claude.ai **Settings → Connectors → Add custom connector** → enter `https://mcp.shipstar.ai/mcp` → **Connect** → sign in and pick a project. See [OAuth](/mcp/oauth#connecting-from-claudeai-custom-connector) for the step-by-step. ## Connecting from Claude Code The quickest path is the official plugin, which bundles the server connection plus guided [agent skills](/mcp/skills/setup-shipstar) — [`/shipstar:announce-release`](/mcp/skills/announce-release), [`/shipstar:write-blog-post`](/mcp/skills/write-blog-post), [`/shipstar:email-your-users`](/mcp/skills/email-your-users), and [`/shipstar:build-changelog-page`](/mcp/skills/build-changelog-page): ``` /plugin marketplace add shipstar-ai/shipstar-plugin /plugin install shipstar@shipstar ``` Or add the server directly: ```bash theme={null} claude mcp add --transport http shipstar https://mcp.shipstar.ai/mcp ``` Either way, Claude Code runs the OAuth flow in your browser on first use. To pin a static token instead, append `--header "Authorization: Bearer YOUR_API_TOKEN"` to the `claude mcp add` form. ## Connecting from Claude Desktop Add the server to your `claude_desktop_config.json`, including a bearer token as a custom header: ```json theme={null} { "mcpServers": { "shipstar": { "type": "http", "url": "https://mcp.shipstar.ai/mcp", "headers": { "Authorization": "Bearer YOUR_API_TOKEN" } } } } ``` Restart Claude Desktop and the Shipstar tools will be available in any chat. ## Connecting from Cursor / Windsurf Both editors accept the same MCP server configuration. Add an entry with the URL above and the tools will appear in the agent sidebar. ## Available Tools ### Generation (kick off content from commits) | Tool | Description | | - | - | | [`generate_changelog`](/mcp/generation/generate-changelog) | Generate a public changelog from recent commits | | [`generate_blog_post`](/mcp/generation/generate-blog-post) | Generate a blog post (optionally steered by an idea) | | [`generate_blog_post_ideas`](/mcp/generation/generate-blog-post-ideas) | Brainstorm blog post angles (synchronous) | | [`generate_feature_page`](/mcp/generation/generate-feature-page) | Generate a marketing feature landing page | | [`generate_kb_articles`](/mcp/generation/generate-kb-articles) | Generate a set of knowledge base articles | | [`generate_release_notes_email`](/mcp/generation/generate-release-notes-email) | Generate a release notes email | | [`generate_twitter_thread`](/mcp/generation/generate-twitter-thread) | Generate an X (Twitter) thread | | [`generate_linkedin_post`](/mcp/generation/generate-linkedin-post) | Generate a LinkedIn post | Generation tools (except `generate_blog_post_ideas`) return a pending `content_id` and run in the background — poll [`get_generation_status`](/mcp/lifecycle/get-generation-status) for completion. ### Lifecycle (review, revise, approve, publish) | Tool | Description | | - | - | | [`get_generation_status`](/mcp/lifecycle/get-generation-status) | Poll a generation job's status | | [`get_content_draft`](/mcp/lifecycle/get-content-draft) | Read the full draft of a content record | | [`update_content`](/mcp/lifecycle/update-content) | Revise a draft's text and/or category | | [`approve_content`](/mcp/lifecycle/approve-content) | Approve content for publication | | [`publish_content`](/mcp/lifecycle/publish-content) | Publish completed content directly | ### Project & delivery | Tool | Description | | - | - | | [`get_project_context`](/mcp/project/get-project-context) | Describe the connection's project: repos, destinations, mailing lists | | [`list_destinations`](/mcp/project/list-destinations) | List connected delivery destinations | | [`list_mailing_lists`](/mcp/project/list-mailing-lists) | List mailing lists for release notes emails | | [`send_release_email`](/mcp/email/send-release-email) | Send a release notes email to mailing lists | ### Read (fetch published content) | Tool | Description | | - | - | | [`list_changelogs`](/mcp/changelog/list-changelogs) | List the project's published changelogs, newest first | | [`get_changelog`](/mcp/changelog/get-changelog) | Fetch a single changelog by slug | | [`list_blog_posts`](/mcp/blog/list-blog-posts) | List the project's published blog posts, newest first | | [`get_blog_post`](/mcp/blog/get-blog-post) | Fetch a single blog post by slug | | [`list_kb_article_sets`](/mcp/kb/list-kb-article-sets) | List the project's published knowledge base article sets | | [`get_kb_article_set`](/mcp/kb/get-kb-article-set) | Fetch a single KB article set by slug | ## Use Cases > [card] Release-aware support bots > > Point a support agent at `list_changelogs` / `get_changelog` so it can answer "what's new this week?" and cite the exact release that shipped a feature — without you having to copy release notes into a knowledge base. > [card] Docs-grounded coding agents > > Let a coding agent (Cursor, Claude Code, Windsurf) pull your latest blog walkthroughs and KB articles on demand. The agent grounds its answers in your published guidance instead of hallucinating APIs. > [card] Marketing copy research > > When drafting landing pages, ads, or email campaigns, have Claude fetch recent changelogs and blog posts to pull accurate product details, quotes, and screenshots from material you already shipped. > [card] Investor / exec updates > > Ask an agent to summarise the last N changelogs into a weekly or monthly update. The tool output is structured, so the model doesn't have to scrape your website. > [card] Internal knowledge lookups > > Expose `list_kb_article_sets` / `get_kb_article_set` to an internal agent so employees can ask "how do I X?" in Slack and get answers drawn from your published KB. > [card] Cross-tool content reuse > > Any agent framework that speaks MCP (LangGraph, Agent SDK, Mastra, custom) can pull Shipstar content as a read-only data source without you writing a bespoke REST client. ## How it works under the hood The MCP tools call directly into the same `app.services.content` layer that powers the V1 REST endpoints. That means: * **One source of truth.** Any bug fix or behaviour change in the content service automatically flows to both transports. * **Only published content is returned.** Drafts, pending items, and wrong-type rows are filtered out at the service layer. * **Graceful skipping of invalid data.** List endpoints skip rows whose stored JSON is malformed rather than failing the whole call. If you need finer control (pagination, RSS feeds, custom filtering) or you're building a non-agent integration, use the [V1 REST API](/api-reference/overview) directly. # create_mailing_list Source: https://docs.shipstar.ai/mcp/project/create-mailing-list Create a named mailing list in the project Creates a mailing list in the current project. A list is the audience for a release notes email: [`send_release_email`](/mcp/email/send-release-email) delivers a completed [`generate_release_notes_email`](/mcp/generation/generate-release-notes-email) draft to one or more lists, deduplicating recipients across them and giving every email a one-click unsubscribe link. A project holds at most 20 lists and names are unique per project — a repeat call with the same name fails rather than duplicating. The new list is empty; fill it with [`upsert_mailing_list_recipients`](/mcp/project/upsert-mailing-list-recipients), the dashboard's CSV import under Destinations → Email, or the [REST sync recipes](/guides/sync-mailing-list) from your own user database. Creating lists and managing recipients never costs credits; only generation does. ## Arguments | Name | Type | Description | | - | - | - | | `name` | string | 1–100 characters, unique within the project. Shown in the dashboard and in `list_mailing_lists`; recipients never see it. | ## Returns ```json theme={null} { "id": "9c1f7e55-...", "name": "Design partners", "active_recipients": 0, "removed_recipients": 0, "unsubscribed_recipients": 0 } ``` Keep the `id`: it is what `send_release_email` and the recipient tools take. The three counts track a recipient's status over the life of the list: * `active_recipients` receive the next email. A list holds up to 500 active recipients; an upsert that would cross the cap is rejected whole. * `removed_recipients` were taken off by your team (the dashboard's trash or [`remove_mailing_list_recipients`](/mcp/project/remove-mailing-list-recipients)). Soft and reversible — a later upsert or the dashboard's Restore puts them back. * `unsubscribed_recipients` opted out themselves via the link in an email. Terminal: an upsert reports them as `skipped_unsubscribed` and they are never re-added, so re-importing a CSV cannot resurrect an opt-out. ## Errors * `A project can have at most 20 mailing lists` — delete or reuse an existing list. * `A list with this name already exists` — pick another name, or look the existing one up with [`list_mailing_lists`](/mcp/project/list-mailing-lists). * The name is blank or longer than 100 characters. ## Example prompts > "Create a mailing list called Design partners and add these five people to it." > "Set up a Beta testers list, then send the release email for the latest draft to it." > "We want a separate list for enterprise customers — create it and import the CSV I'm pasting." ## Related * [`list_mailing_lists`](/mcp/project/list-mailing-lists) — every list with its counts. * [`upsert_mailing_list_recipients`](/mcp/project/upsert-mailing-list-recipients) — add or update recipients, keyed by your own `external_id` or by email. * [`send_release_email`](/mcp/email/send-release-email) — deliver a release notes email to one or more lists. * [Sync a mailing list from your app](/guides/sync-mailing-list) — the REST recipes for keeping a list in step with your own user table. # get_project_context Source: https://docs.shipstar.ai/mcp/project/get-project-context Describe the project this MCP connection is scoped to Describes the project this MCP connection is scoped to: the project name, the GitHub repositories Shipstar tracks for content generation, which destinations are connected, the mailing lists available for release notes emails, and the project's default [content guidelines](/guides/content-guidelines) (technical-depth preset `audience` and free-text instructions) that every generation tool applies unless you override them per call. Call this first in a session to understand what generation will operate on. No arguments. ## Returns ```json theme={null} { "project": { "id": "3b9a4d02-...", "name": "Acme App" }, "github_connected": true, "tracked_repositories": ["acme/backend", "acme/frontend"], "destinations": { "slack": { "connected": true, "team": "Acme", "default_channel": "#releases" }, "intercom": { "connected": false }, "featurebase": { "connected": true, "help_center": "Acme Help", "default_collection": "Getting started" }, "x": { "connected": true, "username": "acmeapp" }, "webhook_endpoints": 2 }, "mailing_lists": [ { "id": "9c1f7e55-...", "name": "Customers", "active_recipients": 312 } ], "content_guidelines": { "audience": "business", "instructions": "Call users \"members\", never \"customers\". British spelling." } } ``` `tracked_repositories` covers active repos only — pass any of these names as `repos` to a generation tool to restrict it to that repository. `content_guidelines` are the project defaults set in the dashboard (Settings → Content guidelines); both fields are `null` until the team sets them. Every `generate_*` tool accepts `audience` and `instructions` arguments to override them for one call. The `destinations` and `mailing_lists` sections are the same payloads returned by [`list_destinations`](/mcp/project/list-destinations) and [`list_mailing_lists`](/mcp/project/list-mailing-lists). ## Errors * `Authenticated user or project is no longer available.` — the token's user or project was deleted; reconnect with a fresh token. ## Example prompts > "What project is this connection scoped to, and which repos does it track?" > "Before generating anything, show me what destinations and mailing lists are set up." # list_destinations Source: https://docs.shipstar.ai/mcp/project/list-destinations List the delivery destinations connected to this project Lists the delivery destinations connected to this project: Slack, Intercom, Featurebase, X (Twitter), and the number of webhook endpoints. Destinations are connected in the Shipstar dashboard (OAuth for Slack, Intercom, and X; an organization API key for Featurebase) — if one you want is missing, connect it there rather than retrying. No arguments. ## Returns ```json theme={null} { "slack": { "connected": true, "team": "Acme", "default_channel": "#releases" }, "intercom": { "connected": true, "workspace": "Acme Support" }, "featurebase": { "connected": true, "help_center": "Acme Help", "help_center_url": "https://help.acme.com", "default_collection": "Getting started", "publish_state": "live", "needs_reconnect": false, "mirror_to_kb": true }, "x": { "connected": false }, "webhook_endpoints": 2 } ``` Disconnected destinations return `{ "connected": false }` with no further fields. For Featurebase, `needs_reconnect: true` means the last call was rejected (the API key was revoked) and article sets can't be delivered until a new key is connected in the dashboard; `default_collection` is where approved article sets land unless a schedule picks another collection; `mirror_to_kb` means the live help center is mirrored hourly into the project's Knowledge Base (`list_kb_article_sets`), with the apex domain as canonical. For a fuller picture of the connection (repos, mailing lists), use [`get_project_context`](/mcp/project/get-project-context). ## Example prompts > "Which destinations are connected for this project?" > "Is Slack set up? What channel do releases go to?" # list_mailing_lists Source: https://docs.shipstar.ai/mcp/project/list-mailing-lists List the project's mailing lists with recipient counts by status Lists the project's mailing lists for release notes emails — each list's `id`, `name`, and recipient counts by status. No arguments. ## Returns ```json theme={null} [ { "id": "9c1f7e55-...", "name": "Customers", "active_recipients": 312, "removed_recipients": 9, "unsubscribed_recipients": 5 }, { "id": "b40d2a88-...", "name": "Beta testers", "active_recipients": 47, "removed_recipients": 0, "unsubscribed_recipients": 1 } ] ``` * `active_recipients` receive the next email. * `removed_recipients` were taken off by the team; an upsert or the dashboard's Restore puts them back. * `unsubscribed_recipients` opted out themselves via the link in an email and are never re-added. Pass one or more `id` values to [`send_release_email`](/mcp/email/send-release-email) to deliver a completed release notes email, or to [`upsert_mailing_list_recipients`](/mcp/project/upsert-mailing-list-recipients) / [`remove_mailing_list_recipients`](/mcp/project/remove-mailing-list-recipients) to keep the list in sync. Create a new list with [`create_mailing_list`](/mcp/project/create-mailing-list). Recipients can also be managed in the dashboard under Destinations → Email (CSV import/export included). ## Example prompts > "What mailing lists do we have, and how many people are on each?" > "Send the release email to whichever list has our beta testers." # remove_mailing_list_recipients Source: https://docs.shipstar.ai/mcp/project/remove-mailing-list-recipients Soft-remove recipients from a mailing list by email or external_id Takes recipients off a list without erasing their history: they stop receiving release emails, and a later [`upsert_mailing_list_recipients`](/mcp/project/upsert-mailing-list-recipients) or the dashboard's Restore puts them back. Contacts who unsubscribed themselves are left alone and reported. Idempotent. The agent confirms the exact people with you before calling. ## Arguments | Name | Type | Description | | - | - | - | | `mailing_list_id` | string | From [`list_mailing_lists`](/mcp/project/list-mailing-lists) | | `emails` | string\[] | Addresses to remove (case-insensitive) | | `external_ids` | string\[] | Your ids to remove | At least one of `emails` / `external_ids`; up to 1000 identifiers in total. ## Returns ```json theme={null} { "removed": 2, "already_removed": 0, "skipped_unsubscribed": 1, "not_found": ["nobody@example.com"] } ``` ## Example prompts > "Take the three churned accounts I listed off the Customers mailing list." > "Remove user usr\_42 from every release-notes list." # upsert_mailing_list_recipients Source: https://docs.shipstar.ai/mcp/project/upsert-mailing-list-recipients Add or update recipients on a mailing list — idempotent, keyed by external_id or email The sync primitive: adds new contacts, updates existing ones, reactivates contacts the team removed, and never touches contacts who unsubscribed themselves. Safe to call repeatedly with the same data. Up to 500 entries per call; the public API's [`PUT /email/lists/{id}/recipients`](/api-reference/mailing-lists/upsert-recipients) takes 1000 and is the better fit for a scheduled job. ## Arguments | Name | Type | Description | | - | - | - | | `mailing_list_id` | string | From [`list_mailing_lists`](/mcp/project/list-mailing-lists) | | `recipients` | array | Email strings, or objects `{ "email", "name"?, "external_id"?, "attributes"? }` | Send the user's own id as `external_id`: matching happens on it first, so a later email change updates the same recipient instead of creating a second one. `attributes` is a flat map of up to 20 scalar values and replaces the stored map when provided. ## How entries are resolved | The entry… | Result | | - | - | | has an `external_id` already on the list | that recipient is updated (email included) | | has an email on the list with no `external_id` | that recipient is updated and adopts the `external_id` | | matches nothing | added | | matches a removed recipient | reactivated | | matches an unsubscribed recipient | left untouched, counted in `skipped_unsubscribed` | | has an email that belongs to a different `external_id` | listed in `conflicts`, nothing changed | ## Returns ```json theme={null} { "added": 3, "updated": 1, "reactivated": 0, "unchanged": 12, "skipped_unsubscribed": 1, "invalid": [], "conflicts": [], "summary": "3 added, 1 updated, 0 reactivated, 12 unchanged, 1 unsubscribed (left as is)" } ``` ## Errors * The call would push the list past 500 active recipients — nothing is written; split the batch or remove contacts first. * Unknown list id (or a list from another project). ## Example prompts > "Add these people from the CSV I pasted to the Customers list, with their names." > "Sync our new signups from this week into the release-notes list, keyed by their user id." # Add an announcement banner Source: https://docs.shipstar.ai/mcp/skills/add-announcement-banner Claude Code skill that puts a Shipstar announcement banner at the top of your website — generated from your commits, reviewed with you, embedded once, and scheduled to update itself weekly or monthly. `/shipstar:add-announcement-banner` has your coding agent put a one-line announcement bar at the top of your site: the most impactful change of the period as a headline, one sentence, and a call-to-action, generated from your commits. Exactly one banner is live per project — every publish replaces it under the same embed key, so the site is set up once and stays current. ## Install Via the official Claude Code plugin (bundles the MCP connection): ``` /plugin marketplace add shipstar-ai/shipstar-plugin /plugin install shipstar@shipstar ``` Or install the skills standalone from your dashboard's Agents page (`curl -fsSL /agent-skills/install.sh | sh -s -- `). ## When to use it Say "add a what's-new bar to our homepage", "highlight the new feature at the top of the site this week", or "put an announcement banner on our site that updates itself". ## What the agent does 1. **Checks the starting point** — [`get_project_context`](/mcp/project/get-project-context) for a connected GitHub source, [`get_banner`](/mcp/banner/get-banner) for what is live now, and asks where the banner should click through to and whether it should come down on its own. 2. **Generates and reviews** — [`generate_banner`](/mcp/generation/generate-banner) (25 credits), then shows you the exact headline, body, and button label before anything goes live; edits go through [`update_content`](/mcp/lifecycle/update-content). 3. **Publishes on your say-so** — [`publish_content`](/mcp/lifecycle/publish-content) makes it the live banner and reports the embed key. 4. **Puts it on the site** — the one-tag embed (`data-type="banner"`, sticks to the top, dismissable) or a server-rendered bar from [`GET /api/v1/banner`](/api-reference/banner/get-live-banner), where a `404` means "no banner, render nothing". 5. **Keeps it fresh** — points you at the dashboard's weekly or monthly banner schedule, whose "Link to" and "Take down after" choices apply to every run. ## Guardrails * Never publishes without you seeing the exact text first. * Never invents a link: with no URL from you, Shipstar resolves one at publish (your Changelog page, else the period's changelog) or shows no button. # Announce a release Source: https://docs.shipstar.ai/mcp/skills/announce-release Claude Code skill that generates a changelog and release-notes email from your commits, reviews the drafts with you, then publishes and emails subscribers via Shipstar. `/shipstar:announce-release` turns "we just shipped" into finished release marketing: your agent generates a public changelog entry and a release-notes email from the commits you actually pushed, walks the drafts past you, and only publishes or sends after your explicit approval. ## Install Via the official Claude Code plugin (bundles the MCP connection): ``` /plugin marketplace add shipstar-ai/shipstar-plugin /plugin install shipstar@shipstar ``` Or install the skills standalone from your dashboard's Agents page (`curl -fsSL /agent-skills/install.sh | sh -s -- `). ## When to use it Say any of: "announce my latest release", "we shipped v2.1 — run the release marketing", "write the changelog for this week and email subscribers". ## What the agent does 1. Calls [`get_project_context`](/mcp/project/get-project-context) to confirm GitHub is connected and work out the commit window (since the last published changelog, or the range you name). 2. Kicks off [`generate_changelog`](/mcp/generation/generate-changelog) and — if mailing lists with active recipients exist — [`generate_release_notes_email`](/mcp/generation/generate-release-notes-email), passing any audience or scope guidance you gave ("keep it non-technical", "only the API repo"). 3. Polls [`get_generation_status`](/mcp/lifecycle/get-generation-status), then shows you each draft in full and applies your edits with [`update_content`](/mcp/lifecycle/update-content). 4. Publishes and sends **only on your explicit confirmation**, via [`approve_content`](/mcp/lifecycle/approve-content) / [`publish_content`](/mcp/lifecycle/publish-content) and [`send_release_email`](/mcp/email/send-release-email). ## Requirements * Shipstar MCP server connected (see [Set up Shipstar](/mcp/skills/setup-shipstar)) * GitHub connected with at least one tracked repository * Mailing lists with active recipients, if you want the email half The full skill definition is plain markdown: [`SKILL.md`](https://app.shipstar.ai/agent-skills/announce-release/SKILL.md). # Build a changelog page Source: https://docs.shipstar.ai/mcp/skills/build-changelog-page Claude Code skill that implements a server-rendered, agent-readable changelog on your own website — semantic HTML that search engines and AI answer engines can read, plus permalinks, RSS, structured data, and the SEO/AEO layer that makes it rank and get cited. `/shipstar:build-changelog-page` has your coding agent implement a changelog page **on your own domain** that machines can actually read. Client-rendered changelog widgets (Shipstar's included) are invisible to search engines and AI answer engines — GPTBot, ClaudeBot, and PerplexityBot fetch your page's HTML and see an empty container. This skill implements the same server-rendered pattern [shipstar.ai/changelog](https://shipstar.ai/changelog) itself uses, in your stack, from your project's changelog data. ## Install Via the official Claude Code plugin (bundles the MCP connection): ``` /plugin marketplace add shipstar-ai/shipstar-plugin /plugin install shipstar@shipstar ``` Or install the skills standalone from your dashboard's Agents page (`curl -fsSL /agent-skills/install.sh | sh -s -- `). ## When to use it Say "make our changelog readable by AI agents", "add an SEO-friendly changelog page to our site", "get our changelog cited by ChatGPT / Perplexity", or "search engines can't see our changelog". ## What the agent builds Working in your repository, against the [public changelog API](/api-reference/overview): 1. **A server-rendered index** — fetches `GET /api/v1/changelogs` with your API token kept server-side, and ships every entry as semantic HTML: one `
` per release period with stable anchors, category labels inside the entry headings, machine-readable `