Browse the documentation

The extension API

The extension API is the surface the Chrome extension talks to, and it is the only API Issue Mint has. Five endpoints live under https://app.issuemint.com/api/v1/extension, they are authenticated with a bearer token, and between them they read your newsletters, create a draft, save a link and disconnect the browser.

Every id in every request and response is a UUID version 7 string. Nothing in the product exposes a sequential number, because a counter in a URL tells anybody who looks how many newsletters exist and how fast that number is growing.

Where the pairing code comes from

A browser is paired with a short code generated in the dashboard, on the Extension page, and traded once for a token.

The code is 8 characters long, upper case, drawn from an alphabet with I, O, 0 and 1 left out so it cannot be misread off a screen. It lasts 10 minutes and works once. Generating a new code expires any earlier unused one, so the code on screen is always the only one that works, and the connected browsers list is where the tokens it produced can be revoked afterwards.

Authenticating every request after the exchange

Every endpoint except /exchange wants the token as a bearer header.

Authorization: Bearer <token>
Accept: application/json

Tokens carry the extension ability and nothing else. A token without it gets a 403 rather than a 401, so a client can tell “wrong kind of credential” from “no credential”, and every write re-derives what it is allowed to touch from the token’s owner rather than trusting an id in the request.

The five endpoints

The five endpoints are one exchange and the four calls that need the token it returns.

POST /exchange

Unauthenticated, because the code is the credential. device is optional and is what the connected browsers list shows, so it should mean something to whoever reads it later.

{ "code": "ABCD2345", "device": "Chrome on macOS" }
{ "token": "17|plaintext…", "user": { "name": "Ada", "email": "ada@example.com" } }

A code that is wrong, expired or already used returns the same 422 for all three. Which of them it is would tell somebody guessing more than they should know.

GET /context

Everything the popup needs to draw itself, in one request. Issues are drafts and scheduled issues only, newest first, capped at 20 per newsletter. Sent issues are absent because the query never asks for them, not because the popup hides them.

{
  "user": { "name": "Ada", "email": "ada@example.com" },
  "newsletters": [
    {
      "id": "019fd621-4748-707a-8996-feb0c642b58a",
      "name": "Weekly Dispatch",
      "categories": [{ "id": "019fd621-4b30-7006-b2c1-0a4d9e77f311", "name": "Reading" }],
      "issues": [
        { "id": "019fd621-4b35-70af-9c4d-2b7e1f0a55c2", "number": 12, "subject": "Issue 12", "status": "draft" }
      ]
    }
  ]
}

POST /issues

Creates a draft, so “New issue” in the popup does not need the dashboard. The subject is a placeholder and the number is the newsletter’s next.

{ "newsletter_id": "019fd621-4748-707a-8996-feb0c642b58a" }

Returns 201 with {"issue": {"id": "…", "number": 13, "subject": "Issue 13"}}.

POST /links

Adds a link to an issue, or parks it in the library the composer draws from. issue_id is what chooses: absent or null means the library.

{
  "newsletter_id": "019fd621-4748-707a-8996-feb0c642b58a",
  "issue_id": "019fd621-4b35-70af-9c4d-2b7e1f0a55c2",
  "category_id": "019fd621-4b30-7006-b2c1-0a4d9e77f311",
  "url": "https://example.com/article",
  "title": "A good read",
  "description_md": "Worth your time."
}

Returns 201 with {"saved": "issue", "id": "…", "issue": {"id": "…", "subject": "…"}}, or {"saved": "library", "id": "…"}. The response says which side of the split it landed on, so the popup can report what happened rather than guess from its own payload. A category_id has to belong to the same newsletter, and an issue that has started sending is refused with a 422 whatever the client sends, because the popup’s list of issues is always slightly stale.

DELETE /token

Revokes the calling token and returns 204. It is idempotent: a token already revoked from the dashboard leaves nothing to delete, and a client retrying after a dropped connection should not be told off for getting what it asked for.

The extension’s Disconnect calls this and then clears its own copy whatever the server said, because somebody disconnecting a borrowed laptop is not helped by an error message. Revoking from the dashboard instead has the same effect from the other end: the browser gets a 401 on its next call and drops the credential.

Errors

Errors use Laravel’s shape throughout, {"message": "…", "errors": {"field": ["…"]}}, with errors present only on a 422.

StatusWhat it means
401No token, or a token the server no longer honours. The extension clears its copy and shows Connect again.
403A valid token without the extension ability.
404A newsletter or issue that is not this account’s. Deliberately not 403, because whether an id exists is not something a caller should learn.
422Validation, an unusable pairing code, or an issue that is no longer editable.
429Rate limited.

Rate limits

Pairing is limited hardest, because the code is the only guessable thing in the flow.

POST /exchange allows 5 requests a minute and 20 an hour per IP address. Every other endpoint allows 60 a minute per user. A popup that opens, reads its context and saves a link spends three of those, so the ceiling is only ever met by something automated.

What the token deliberately cannot do

An extension token can read your newsletters and their editable issues, create a draft, add a link and revoke itself. That is the whole list.

It cannot send an issue, schedule one, change any setting, touch billing, read your subscribers or export anything. A browser extension runs on whatever page the reader is looking at, which makes it about as untrusted as a first-party client gets, so the ability is narrow on purpose. If a token is copied off a disk, what somebody has is the power to add a link to a draft.

Questions

Is there an API key I can create in settings?

No. The only way to get a token is to exchange a pairing code, and the only place a pairing code comes from is the Extension page in your dashboard. There are no keys, no secrets and no OAuth application to register.

Does a token expire on its own?

No. A token lives until it is revoked, either by the browser calling DELETE /token or by you removing it from the Extension page. That is why the connected browsers list is worth a look after you retire a laptop.

Can I build my own client against these endpoints?

Nothing stops you pairing your own client with a code, and the five endpoints below are everything a token can reach. It is not a published public API though, so the shapes are documented as they are today rather than promised for the future, and there is no versioning commitment behind them.

Last updated 21 August 2026.

Try it on your own list.

14 days, every feature, no card. Sending works from the moment you sign up.

Start your trial