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.
| Status | What it means |
|---|---|
401 | No token, or a token the server no longer honours. The extension clears its copy and shows Connect again. |
403 | A valid token without the extension ability. |
404 | A newsletter or issue that is not this account’s. Deliberately not 403, because whether an id exists is not something a caller should learn. |
422 | Validation, an unusable pairing code, or an issue that is no longer editable. |
429 | Rate 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.
Try it on your own list.
14 days, every feature, no card. Sending works from the moment you sign up.
Start your trial