instapaper-api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/dist/index.cjs +680 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +452 -0
- package/dist/index.d.ts +452 -0
- package/dist/index.js +664 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Instapaper` client for the Instapaper API v2: bookmarks, folders, tags, highlights, and the current user.
|
|
13
|
+
- Helpers for paging through bookmarks (`iterate`) and syncing changes (`changes`, `sync`).
|
|
14
|
+
- Parsed article text with `bookmarks.parse`, including your own Instaparser key for non-personal use.
|
|
15
|
+
- `OAuth` helper for the authorization code flow: building the authorization URL and exchanging a code for a token.
|
|
16
|
+
- Typed errors for each kind of API failure, plus timeouts and network errors.
|
|
17
|
+
- ESM and CommonJS builds with TypeScript declarations, and no runtime dependencies.
|
|
18
|
+
|
|
19
|
+
[0.1.0]: https://github.com/Instapaper/instapaper-api-js/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Instant Paper, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Instapaper API for JavaScript and TypeScript
|
|
2
|
+
|
|
3
|
+
The official client for the [Instapaper API v2](https://www.instapaper.com/developers). Save and organize a user's bookmarks, read and sync their library, manage folders, tags, and highlights, and fetch parsed article text.
|
|
4
|
+
|
|
5
|
+
It's written in TypeScript, ships ESM and CommonJS builds, and has no runtime dependencies.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install instapaper-api
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node 18 or newer. It also runs on Bun, Deno, and edge runtimes that provide `fetch`.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
The quickest way to try the API is with your own account. [Register an application](https://www.instapaper.com/developers/applications/create), then [generate a personal access token](https://www.instapaper.com/developers/overview/authentication#accessing-your-own-account) on its page.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Instapaper } from 'instapaper-api';
|
|
21
|
+
|
|
22
|
+
const client = new Instapaper({ accessToken: process.env.INSTAPAPER_ACCESS_TOKEN! });
|
|
23
|
+
|
|
24
|
+
const { bookmarks } = await client.bookmarks.list();
|
|
25
|
+
for (const bookmark of bookmarks) {
|
|
26
|
+
console.log(bookmark.title);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const saved = await client.bookmarks.save({
|
|
30
|
+
url: 'https://example.com/article',
|
|
31
|
+
title: 'An Article',
|
|
32
|
+
tags: ['Recipes'],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await client.bookmarks.archive(saved.id);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
With CommonJS:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const { Instapaper } = require('instapaper-api');
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Authorizing other users
|
|
45
|
+
|
|
46
|
+
To act on behalf of other Instapaper users, send them through the OAuth 2 authorization code flow. Your client ID and secret come from your [application's page](https://www.instapaper.com/developers/applications), and the redirect URI must match one of its registered callback URIs exactly.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Instapaper, OAuth } from 'instapaper-api';
|
|
50
|
+
|
|
51
|
+
const oauth = new OAuth({
|
|
52
|
+
clientId: process.env.INSTAPAPER_CLIENT_ID!,
|
|
53
|
+
clientSecret: process.env.INSTAPAPER_CLIENT_SECRET!,
|
|
54
|
+
redirectUri: 'https://app.example.com/callback',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 1. Send the user here. Keep `state` in their session.
|
|
58
|
+
const url = oauth.authorizationUrl({ state });
|
|
59
|
+
|
|
60
|
+
// 2. On your callback, check `state`, then exchange the code.
|
|
61
|
+
const token = await oauth.exchangeCode(code);
|
|
62
|
+
|
|
63
|
+
// 3. Store the token. Access tokens don't expire.
|
|
64
|
+
const client = new Instapaper({ accessToken: token.access_token });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
[`examples/oauth-server.ts`](examples/oauth-server.ts) runs the whole flow with a small local server. See [Authentication](https://www.instapaper.com/developers/overview/authentication) for how applications are approved.
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
Methods and options use camelCase. The objects you get back match the API's JSON exactly, so their fields are snake_case (`folder_id`, `private_source`, `deleted_ids`). The [API reference](https://www.instapaper.com/developers/v2/reference/bookmarks) documents every field.
|
|
72
|
+
|
|
73
|
+
### Bookmarks
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// One page from a section: home (the default), archive, liked, folder, or tag.
|
|
77
|
+
// Passing folderId or tag picks that section for you.
|
|
78
|
+
const { bookmarks, total } = await client.bookmarks.list({ section: 'archive', limit: 50 });
|
|
79
|
+
await client.bookmarks.list({ folderId: 99 });
|
|
80
|
+
await client.bookmarks.list({ tag: 'Recipes' });
|
|
81
|
+
|
|
82
|
+
// Save, including private content that has no public URL
|
|
83
|
+
await client.bookmarks.save({ url: 'https://example.com/article', folderId: 99 });
|
|
84
|
+
await client.bookmarks.save({ privateSource: 'Acme Reader', content: '<p>...</p>' });
|
|
85
|
+
|
|
86
|
+
// Update title, description, or reading progress (0 to 1)
|
|
87
|
+
await client.bookmarks.update(12345, { progress: 0.42 });
|
|
88
|
+
|
|
89
|
+
// Move and like
|
|
90
|
+
await client.bookmarks.archive(12345);
|
|
91
|
+
await client.bookmarks.unarchive(12345);
|
|
92
|
+
await client.bookmarks.moveToFolder(12345, 99);
|
|
93
|
+
await client.bookmarks.like(12345);
|
|
94
|
+
await client.bookmarks.unlike(12345);
|
|
95
|
+
|
|
96
|
+
// Tags: add by name (created if needed) or ID, remove by ID
|
|
97
|
+
await client.bookmarks.updateTags(12345, { add: ['Recipes', 7], remove: [3] });
|
|
98
|
+
|
|
99
|
+
// Delete permanently. This is not the same as archiving.
|
|
100
|
+
await client.bookmarks.delete(12345);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Paging and syncing
|
|
104
|
+
|
|
105
|
+
`iterate` walks every bookmark in a section, fetching pages as it goes:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
for await (const bookmark of client.bookmarks.iterate({ section: 'liked' })) {
|
|
109
|
+
console.log(bookmark.title);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
To keep a local copy up to date, record when you start a sync and pass that time next time. `sync` fetches every change across all sections since then, including the IDs of deleted bookmarks:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const startedAt = new Date();
|
|
117
|
+
const { bookmarks, deleted_ids } = await client.bookmarks.sync(lastSyncedAt);
|
|
118
|
+
// Save `startedAt` as the next `lastSyncedAt`.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`changes` returns a single page if you'd rather page yourself. Both accept a `Date` or a Unix timestamp in seconds.
|
|
122
|
+
|
|
123
|
+
### Parsed article text
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const article = await client.bookmarks.parse(12345);
|
|
127
|
+
console.log(article.metadata.title, article.content.words);
|
|
128
|
+
console.log(article.content.body); // HTML
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Pass `content` to parse HTML you already have instead of fetching the URL.
|
|
132
|
+
|
|
133
|
+
You can use this without a key when you're the authenticated user of your own application, like a personal script. For anyone else's account, pass your own [Instaparser](https://www.instaparser.com) key as `instaparserApiKey`. See [Non-personal use](https://www.instapaper.com/developers/v2/reference/bookmarks#non-personal-use). Parsed text is for showing an article to the user who saved it, as covered by the [API Terms of Use](https://www.instapaper.com/developers/overview/api-terms).
|
|
134
|
+
|
|
135
|
+
### Folders, tags, and highlights
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const folders = await client.folders.list();
|
|
139
|
+
const folder = await client.folders.create('Recipes');
|
|
140
|
+
await client.folders.reorder({ [folder.id]: 1 }); // positions start at 1
|
|
141
|
+
await client.folders.delete(folder.id); // its bookmarks move back to home
|
|
142
|
+
|
|
143
|
+
const tags = await client.tags.list();
|
|
144
|
+
const tag = await client.tags.create('Cooking');
|
|
145
|
+
await client.tags.rename(tag.id, 'Baking');
|
|
146
|
+
|
|
147
|
+
const highlights = await client.highlights.list(12345);
|
|
148
|
+
// position 1 highlights the second time the text appears in the article.
|
|
149
|
+
const highlight = await client.highlights.create(12345, { text: 'A passage', position: 1 });
|
|
150
|
+
await client.highlights.delete(highlight.id);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Accounts without Premium can create five highlights a month. Past that, `highlights.create` throws a `PermissionDeniedError`.
|
|
154
|
+
|
|
155
|
+
## Errors
|
|
156
|
+
|
|
157
|
+
Every error the client throws extends `InstapaperError`. When the API returns an error status, you get an `APIError` subclass with the HTTP `status`, the server's `message`, and the response `body`:
|
|
158
|
+
|
|
159
|
+
| Error | Status | When |
|
|
160
|
+
| ----------------------- | ------ | ------------------------------------------------------------------ |
|
|
161
|
+
| `BadRequestError` | 400 | A missing or invalid argument. |
|
|
162
|
+
| `AuthenticationError` | 401 | The access token is missing, unknown, or revoked. |
|
|
163
|
+
| `QuotaExceededError` | 402 | An external quota ran out, such as Instaparser credits. |
|
|
164
|
+
| `PermissionDeniedError` | 403 | An unapproved or suspended application, or a Premium-only feature. |
|
|
165
|
+
| `NotFoundError` | 404 | No such endpoint. |
|
|
166
|
+
| `RateLimitError` | 429 | Too many requests. Back off and retry. |
|
|
167
|
+
| `ServerError` | 5xx | Something went wrong on Instapaper's side. Retry with backoff. |
|
|
168
|
+
|
|
169
|
+
`InstapaperConnectionError` means the request never got a response, because of a network failure or a timeout. `OAuthError` comes from `exchangeCode` and carries the OAuth `error` code and `description`.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { AuthenticationError, RateLimitError } from 'instapaper-api';
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await client.bookmarks.save({ url });
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof AuthenticationError) {
|
|
178
|
+
// Ask the user to reconnect
|
|
179
|
+
} else if (err instanceof RateLimitError) {
|
|
180
|
+
// Try again later
|
|
181
|
+
} else {
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Branch on the error class or `status`. Message text can change over time.
|
|
188
|
+
|
|
189
|
+
Arguments the API would reject, like `folderId` together with `tag`, or reading progress outside 0 to 1, throw a `TypeError` or `RangeError` before any request is made. The client also never follows a redirect, so your access token is only ever sent to Instapaper; a redirect response throws an `APIError`.
|
|
190
|
+
|
|
191
|
+
## Configuration
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const client = new Instapaper({
|
|
195
|
+
accessToken,
|
|
196
|
+
timeout: 10_000, // milliseconds; 0 turns it off. Defaults to 30 seconds.
|
|
197
|
+
fetch: customFetch, // defaults to the global fetch
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`OAuth` takes the same `timeout` and `fetch` options.
|
|
202
|
+
|
|
203
|
+
## Runtime support
|
|
204
|
+
|
|
205
|
+
This library is meant for servers, scripts, and other trusted environments. It won't work from a web page: the API doesn't allow cross-origin browser requests, and the OAuth code exchange needs your client secret, which must never be shipped to a browser.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
[MIT](LICENSE)
|