├── content ├── feeds.md ├── authentication.md ├── icons.md ├── recently-read-entries.md ├── tags.md ├── supporting-twitter.md ├── taggings.md ├── unread-entries.md ├── imports.md ├── starred-entries.md ├── extract-full-content.md ├── saved-searches.md ├── subscriptions.md ├── pages.md ├── updated-entries.md └── entries.md └── README.md /content/feeds.md: -------------------------------------------------------------------------------- 1 | Feeds 2 | ===== 3 | 4 | Get Feed 5 | -------- 6 | 7 | - `GET /v2/feeds/1.json` will return the feed with an id of `1` 8 | 9 | ```json 10 | { 11 | "id": 1, 12 | "title": "Ben Ubois", 13 | "feed_url": "http:\/\/feeds.feedburner.com\/benubois", 14 | "site_url": "http:\/\/benubois.com" 15 | } 16 | ``` 17 | 18 | **Status Codes** 19 | 20 | - `200 OK` will be returned if found 21 | - `404 Not Found` will be returned if no feed is found -------------------------------------------------------------------------------- /content/authentication.md: -------------------------------------------------------------------------------- 1 | Authentication 2 | ============== 3 | 4 | ### `GET /v2/authentication.json` 5 | 6 | Returns status code. You can check a users credentials with this endpoint. 7 | 8 | ```bash 9 | curl --request GET --user "example@example.com:password" https://api.feedbin.com/v2/authentication.json 10 | ``` 11 | 12 | You'll need to include the credentials using HTTP basic auth. You can set this through the `Authorization` header or using your favorite HTTP library's built in support for it. 13 | 14 | **Status Codes** 15 | 16 | - `200 OK` will be returned if credentials are valid 17 | - `401 Unauthorized` will be returned if credentials are invalid 18 | -------------------------------------------------------------------------------- /content/icons.md: -------------------------------------------------------------------------------- 1 | Icons 2 | ===== 3 | 4 | ### `GET /v2/icons.json` 5 | 6 | Returns the feed icons (favicons) for all the feeds a user is subscribed to. Icons are resized to a max resolution of 32x32. If an icon is missing it means Feedbin was unable to find it. 7 | 8 | ```bash 9 | curl --request GET --user "example@example.com:password" https://api.feedbin.com/v2/icons.json 10 | ``` 11 | 12 | **Response** 13 | 14 | ```json 15 | [ 16 | { 17 | "host": "m.signalvnoise.com", 18 | "url": "https://favicons.feedbinusercontent.com/27a/27a62660e820f80421e1e57551b070e6e42e52d4.png" 19 | }, 20 | { 21 | "host": "github.blog", 22 | "url": "https://favicons.feedbinusercontent.com/19a/19ae11001100ad1497fba45580a546b022c2a8d3.png" 23 | } 24 | ] 25 | ``` 26 | -------------------------------------------------------------------------------- /content/recently-read-entries.md: -------------------------------------------------------------------------------- 1 | Recently Read Entries 2 | ===================== 3 | 4 | Recently read entries are a history of what the user has recently read. 5 | 6 | Recently read entries are created if a user is on a post for 10 seconds on Feedbin, but you can use any metric makes sense for determining if an entry should end up here. 7 | 8 | Get Recently Read Entries 9 | ------------------------- 10 | 11 | - `GET /v2/recently_read_entries.json` will return an array of entry_ids 12 | 13 | 14 | ```json 15 | [4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097] 16 | ``` 17 | 18 | **Status Codes** 19 | 20 | - `200 OK` will be returned 21 | 22 | 23 | **Note** The order of IDs is the order these should be displayed in 24 | 25 | 26 | Create Recently Read Entries 27 | ---------------------------- 28 | 29 | - `POST /v2/recently_read_entries.json` will create records for the specified entry_ids. 30 | 31 | **Request** 32 | 33 | ```json 34 | { 35 | "recently_read_entries": [4089, 4090, 4091] 36 | } 37 | ``` 38 | 39 | **Response** 40 | 41 | ```json 42 | [4089,4090,4091] 43 | ``` 44 | 45 | **Status Codes** 46 | 47 | - `200 OK` will be returned if the request is successful 48 | 49 | The response will contain the entry_ids of the recently read records that were created. -------------------------------------------------------------------------------- /content/tags.md: -------------------------------------------------------------------------------- 1 | Tags 2 | ==== 3 | 4 | ### `POST /v2/tags.json` 5 | 6 | Rename a tag. 7 | 8 | ```bash 9 | curl --request POST \ 10 | --user "example@example.com:password" \ 11 | --header "Content-Type: application/json; charset=utf-8" \ 12 | --data-ascii '{"old_name": "Old Name", "new_name": "New Name"}' \ 13 | https://api.feedbin.com/v2/tags.json 14 | ``` 15 | 16 | **Request** 17 | 18 | ```json 19 | { 20 | "old_name": "Old Name", 21 | "new_name": "New Name" 22 | } 23 | ``` 24 | 25 | **Response** 26 | 27 | Returns the new array of [taggings](taggings.md) after the rename. 28 | 29 | ```json 30 | [ 31 | { 32 | "id": 4, 33 | "feed_id": 1, 34 | "name": "New Name" 35 | } 36 | ] 37 | ``` 38 | 39 | ### `DELETE /v2/tags.json` 40 | 41 | Delete a tag. 42 | 43 | ```bash 44 | curl --request DELETE \ 45 | --user "example@example.com:password" \ 46 | --header "Content-Type: application/json; charset=utf-8" \ 47 | --data-ascii '{"name": "Tag Name"}' \ 48 | https://api.feedbin.com/v2/tags.json 49 | ``` 50 | 51 | **Request** 52 | 53 | ```json 54 | { 55 | "name": "Tag Name" 56 | } 57 | ``` 58 | 59 | **Response** 60 | 61 | Returns the new array of [taggings](taggings.md) after the delete. 62 | 63 | ```json 64 | [ 65 | { 66 | "id": 4, 67 | "feed_id": 1, 68 | "name": "Other Tag" 69 | } 70 | ] 71 | ``` 72 | -------------------------------------------------------------------------------- /content/supporting-twitter.md: -------------------------------------------------------------------------------- 1 | Supporting Twitter 2 | ================== 3 | 4 | Feedbin [supports subscribing to Twitter](https://feedbin.com/blog/2018/01/11/feedbin-is-the-best-way-to-read-twitter/) as a data source. You don't have to do anything to support Twitter, but if you want to provide a tailored Twitter experience, Feedbin provides the data to help make that happen. 5 | 6 | The Twitter terms of service do not allow their API data to be redistributed, but there is a straightforward way to get full tweet data. 7 | 8 | 1. Register your app on the [Twitter developer site](https://apps.twitter.com/). 9 | 1. Build a Twitter authentication flow in your app so you can get Twitter data on behalf of the user. 10 | 1. Use the `extended` mode when fetching entries from Feedbin to get the data necessary to load tweets. 11 | 1. Use Twitter's [`statuses/lookup` API](https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-lookup) to bulk load tweets by ID. 12 | 13 | ### Getting Twitter IDs from Feedbin 14 | 15 | Load entries using the extended mode. You'll get the `twitter_id` and `twitter_thread_ids` as well as the normal entry data. Then you can [load tweets by id](https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-lookup), 100 at-a-time, from Twitter. 16 | 17 | ``` 18 | GET /v2/entries.json?mode=extended 19 | ``` 20 | 21 | ```json 22 | { 23 | "id": 1682191545, 24 | "feed_id": 1379740, 25 | ... 26 | "twitter_id": 973315765393920000, 27 | "twitter_thread_ids": [973315765393920000, 973315765393920001], 28 | "extracted_articles": [] 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /content/taggings.md: -------------------------------------------------------------------------------- 1 | Taggings 2 | ======== 3 | 4 | Get Taggings 5 | ------------ 6 | 7 | - `GET /v2/taggings.json` will return taggings. 8 | 9 | 10 | ```json 11 | [ 12 | { 13 | "id": 4, 14 | "feed_id": 1, 15 | "name": "Tech" 16 | }, 17 | { 18 | "id": 5, 19 | "feed_id": 2, 20 | "name": "News" 21 | } 22 | ] 23 | ``` 24 | 25 | **Status Codes** 26 | 27 | - `200 OK` will be returned 28 | 29 | Get Tagging 30 | ----------- 31 | 32 | - `GET /v2/taggings/4.json` will return the tagging with an id of `4` 33 | 34 | ```json 35 | { 36 | "id": 4, 37 | "feed_id": 1, 38 | "name": "Tech" 39 | } 40 | ``` 41 | 42 | **Status Codes** 43 | 44 | - `200 OK` will be returned if found 45 | - `403 Forbidden` will be returned if the user does not own this tagging 46 | 47 | Create Tagging 48 | -------------- 49 | 50 | - `POST /v2/taggings.json` will create a new tagging 51 | 52 | **Request** 53 | 54 | ```json 55 | { 56 | "feed_id": 1, 57 | "name": "Design" 58 | } 59 | ``` 60 | 61 | | Parameter | Required | 62 | | -------------------------------- | -------- | 63 | | `feed_id: number` | true | 64 | | `name: string` | true | 65 | 66 | **Status Codes** 67 | 68 | - `201 Created` will be returned if tagging is successful, the `Location` header will have the URL to the newly created tagging 69 | - `302 Found` will be returned if tagging exists, the `Location` header will have the URL to the tagging 70 | 71 | 72 | Delete Tagging 73 | -------------- 74 | 75 | `DELETE /v2/taggings/4.json` will delete the tagging with and id of `4` 76 | 77 | **Status Codes** 78 | 79 | - `204 No Content` will be returned if the request was successful 80 | - `403 Forbidden` will be returned if the user does not own this tagging 81 | -------------------------------------------------------------------------------- /content/unread-entries.md: -------------------------------------------------------------------------------- 1 | Unread Entries 2 | ============== 3 | 4 | Unread entries deals only with entry_ids. After downloading the unread entry ids, you'll want to follow up with a call to 5 | 6 | `GET /v2/entries.json?ids=4088,4089,4090,4091` 7 | 8 | To actually retrieve the unread entries. Please note, that you can only request up to 100 entries at a time. 9 | 10 | Get Unread Entries 11 | ------------------ 12 | 13 | - `GET /v2/unread_entries.json` will return an array of entry_ids 14 | 15 | 16 | ```json 17 | [4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097] 18 | ``` 19 | 20 | **Status Codes** 21 | 22 | - `200 OK` will be returned 23 | 24 | Create Unread Entries (mark as unread) 25 | -------------------------------------- 26 | 27 | - `POST /v2/unread_entries.json` will mark the specified entry_ids as unread. 28 | 29 | **Request** 30 | 31 | ```json 32 | { 33 | "unread_entries": [4089, 4090, 4091] 34 | } 35 | ``` 36 | 37 | **Response** 38 | 39 | ```json 40 | [4089,4090,4091] 41 | ``` 42 | 43 | **Status Codes** 44 | 45 | - `200 OK` will be returned if the request is successful 46 | 47 | **Note** There is a limit of 1,000 entry_ids per request 48 | 49 | The response will contain all of the entry_ids that were successfully marked as unread. If any ids that were sent are not returned in the response it usually means the user no longer has access to the feed the entry belongs to. 50 | 51 | Delete Unread Entries (mark as read) 52 | ------------------------------------ 53 | 54 | - `DELETE /v2/unread_entries.json` will mark the specified entry_ids as read. 55 | 56 | **Request** 57 | 58 | ```json 59 | { 60 | "unread_entries": [4089, 4090, 4091] 61 | } 62 | ``` 63 | 64 | **Status Codes** 65 | 66 | - `200 OK` will be returned if the request is successful 67 | 68 | **Note** There is a limit of 1,000 entry_ids per request 69 | 70 | The response will contain all of the entry_ids that were successfully marked as read. If any ids that were sent are not returned in the response it usually means the user no longer has access to the feed the entry belongs to. 71 | 72 | **DELETE Alternative** 73 | 74 | Some clients like Android don't easily allow a body with a DELETE request. For these cases there is an alternate endpoint that can be used with POST: 75 | 76 | `POST /v2/unread_entries/delete.json` 77 | -------------------------------------------------------------------------------- /content/imports.md: -------------------------------------------------------------------------------- 1 | Imports 2 | ======= 3 | 4 | The Imports API can be used to import OPML files and get the status of an import. 5 | 6 | ### `POST /v2/imports.json` 7 | 8 | Create a new import. The POST body should be an XML string. Returns the import along with the status of each of the import_items. The `Content-Type` header must be set to `text/xml`. 9 | 10 | ```bash 11 | curl --request POST --user "example@example.com:password" --header "Content-Type: text/xml" --data-binary "@/path/to/subscriptions.xml" https://api.feedbin.com/v2/imports.json 12 | ``` 13 | 14 | **Response** 15 | 16 | ```json 17 | { 18 | "id": 6, 19 | "complete": false, 20 | "created_at": "2019-05-16T20:27:47.038Z", 21 | "import_items": [ 22 | { 23 | "title": "Daring Fireball", 24 | "feed_url": "http://daringfireball.net/feeds/main", 25 | "status": "pending" 26 | } 27 | ] 28 | } 29 | ``` 30 | 31 | The `id` can be used to get the status of the import at `/v2/imports/6.json`. 32 | 33 | ### `GET /v2/imports.json` 34 | 35 | Returns all imports for a user. 36 | 37 | ```bash 38 | curl --request GET --user "example@example.com:password" https://api.feedbin.com/v2/imports.json 39 | ``` 40 | 41 | **Response** 42 | 43 | ```json 44 | [ 45 | { 46 | "id": 1, 47 | "complete": true, 48 | "created_at": "2019-05-16T19:58:59.214Z" 49 | }, 50 | { 51 | "id": 2, 52 | "complete": false, 53 | "created_at": "2019-05-16T20:01:12.090Z" 54 | } 55 | ] 56 | ``` 57 | 58 | 59 | ### `GET /v2/imports/1.json` 60 | 61 | Returns the import along with the status of each of the import_items. 62 | 63 | ```bash 64 | curl --request GET --user "example@example.com:password" https://api.feedbin.com/v2/imports/1.json 65 | ``` 66 | 67 | **Response** 68 | 69 | ```json 70 | { 71 | "id": 4, 72 | "complete": true, 73 | "created_at": "2019-05-16T20:07:13.043Z", 74 | "import_items": [ 75 | { 76 | "title": "Daring Fireball", 77 | "feed_url": "http://daringfireball.net/feeds/main", 78 | "status": "pending" 79 | }, 80 | { 81 | "title": "inessential.com", 82 | "feed_url": "http://inessential.com/xml/rss.xml", 83 | "status": "complete" 84 | }, 85 | { 86 | "title": "kottke.org", 87 | "feed_url": "http://feeds.kottke.org/main", 88 | "status": "failed" 89 | } 90 | ] 91 | } 92 | ``` 93 | 94 | The possible values for `status` are: 95 | 96 | - `pending` The feed is queued for import 97 | - `complete` The feed has been successfully imported 98 | - `failed` The import failed due to HTTP/invalid feed errors 99 | 100 | -------------------------------------------------------------------------------- /content/starred-entries.md: -------------------------------------------------------------------------------- 1 | Starred Entries 2 | =============== 3 | 4 | Starred entries deals only with entry_ids. After downloading the starred entry ids, you'll want to follow up with a call to 5 | 6 | `GET /v2/entries.json?ids=4088,4089,4090,4091` 7 | 8 | To actually retrieve the entries that have been starred. Please note, that you can only request up to 100 entries at a time. 9 | 10 | Get Starred Entries 11 | ------------------- 12 | 13 | - `GET /v2/starred_entries.json` will return an array of entry_ids 14 | 15 | **Request** 16 | 17 | ```bash 18 | curl --request GET \ 19 | --user "example@example.com:password" \ 20 | https://api.feedbin.com/v2/starred_entries.json 21 | ``` 22 | 23 | **Response** 24 | 25 | ```json 26 | [4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097] 27 | ``` 28 | 29 | **Status Codes** 30 | 31 | - `200 OK` will be returned 32 | 33 | Create Starred Entries 34 | ---------------------- 35 | 36 | - `POST /v2/starred_entries.json` will star the specified entry_ids 37 | 38 | **Request** 39 | 40 | ```bash 41 | curl --request POST \ 42 | --user "example@example.com:password" \ 43 | --header "Content-Type: application/json; charset=utf-8" \ 44 | --data-ascii '{"starred_entries": [4089, 4090, 4091]}' \ 45 | https://api.feedbin.com/v2/starred_entries.json 46 | ``` 47 | 48 | **Response** 49 | 50 | ```json 51 | [4089,4090,4091] 52 | ``` 53 | 54 | The response will contain all of the entry_ids that were successfully starred. If any ids that were sent are not returned in the response it usually means the user no longer has access to the feed the entry belongs to. 55 | 56 | **Status Codes** 57 | 58 | - `200 OK` will be returned if the request is successful 59 | 60 | **Note** There is a limit of 1,000 entry_ids per request 61 | 62 | 63 | Delete Starred Entries (unstar) 64 | ------------------------------- 65 | 66 | - `DELETE /v2/starred_entries.json` will remove unstar the specified entry_ids 67 | 68 | **Request** 69 | 70 | ```bash 71 | curl --request DELETE \ 72 | --user "example@example.com:password" \ 73 | --header "Content-Type: application/json; charset=utf-8" \ 74 | --data-ascii '{"starred_entries": [4089, 4090, 4091]}' \ 75 | https://api.feedbin.com/v2/starred_entries.json 76 | ``` 77 | 78 | **Response** 79 | 80 | ```json 81 | [4089,4090,4091] 82 | ``` 83 | 84 | **Status Codes** 85 | 86 | - `200 OK` will be returned if the request is successful 87 | 88 | **Note** There is a limit of 1,000 entry_ids per request 89 | 90 | The response will contain all of the entry_ids that were successfully unstarred. If any ids that were sent are not returned in the response it usually means the user no longer has access to the feed the entry belongs to. 91 | 92 | **DELETE Alternative** 93 | 94 | Some clients like Android don't easily allow a body with a DELETE request. For these cases there is an alternate endpoint that can be used with POST: 95 | 96 | `POST /v2/starred_entries/delete.json` -------------------------------------------------------------------------------- /content/extract-full-content.md: -------------------------------------------------------------------------------- 1 | Extracting Full Content 2 | ======================= 3 | 4 | If you have a native app that supports Feedbin, you can use Feedbin's full content service. 5 | 6 | The service uses [Mercury Parser](https://github.com/postlight/mercury-parser) to attempt to extract the full content of a webpage. 7 | 8 | In order to use the service, you need a username and signing key. The signing key is used to generate an HMAC-SHA1 signature to authenticate your request. 9 | 10 | An example request looks like: 11 | 12 | ``` 13 | https://extract.feedbin.com/parser/:username/:signature?base64_url=:base64_url 14 | ``` 15 | 16 | The parts that you need are: 17 | 18 | - `username` your username 19 | - `signature` the HMAC-SHA1 signature of the URL you want to parse 20 | - `base64_url` base64 encoded version of the URL you want to parse 21 | 22 | The URL is base64 encoded to avoid any issues in the way different systems encode URLs. It must use the [RFC 4648](https://tools.ietf.org/html/rfc4648#section-5) url-safe variant with no newlines. 23 | 24 | If your platform does not offer a URL safe base64 option, you can replicate it. First create the base64 encoded string. Then replace the following characters: 25 | 26 | - `+` => `-` 27 | - `/` => `_` 28 | - `\n` => "" 29 | 30 | 31 | ```ruby 32 | require "uri" 33 | require "openssl" 34 | require "base64" 35 | 36 | username = "username" 37 | secret = "secret" 38 | url = "https://feedbin.com/blog/2018/09/11/private-by-default/" 39 | 40 | digest = OpenSSL::Digest.new("sha1") 41 | signature = OpenSSL::HMAC.hexdigest(digest, secret, url) 42 | 43 | base64_url = Base64.urlsafe_encode64(url).gsub("\n", "") 44 | 45 | URI::HTTPS.build({ 46 | host: "extract.feedbin.com", 47 | path: "/parser/#{username}/#{signature}", 48 | query: "base64_url=#{base64_url}" 49 | }).to_s 50 | ``` 51 | 52 | The above example would produce: 53 | 54 | ``` 55 | https://extract.feedbin.com/parser/username/e4696f8630bb68c21d77a9629ce8d063d8e5f81c?base64_url=aHR0cHM6Ly9mZWVkYmluLmNvbS9ibG9nLzIwMTgvMDkvMTEvcHJpdmF0ZS1ieS1kZWZhdWx0Lw== 56 | ``` 57 | 58 | With the output: 59 | 60 | ```json 61 | { 62 | "title": "Private by Default", 63 | "author": null, 64 | "date_published": "2018-09-11T00:00:00.000Z", 65 | "dek": null, 66 | "lead_image_url": "https://assets.feedbin.com/assets-site/blog/2018-09-11/embed-3f43088538ae5ed7e585c00013adc13a915fd35de31990b3081a085b963ed7dd.png", 67 | "content": "
content
", 68 | "next_page_url": null, 69 | "url": "https://feedbin.com/blog/2018/09/11/private-by-default/", 70 | "domain": "feedbin.com", 71 | "excerpt": "September 11, 2018 by Ben Ubois I want Feedbin to be the opposite of Big Social. I think people should have the right not to be tracked on the Internet and Feedbin can help facilitate that. Since…", 72 | "word_count": 787, 73 | "direction": "ltr", 74 | "total_pages": 1, 75 | "rendered_pages": 1 76 | } 77 | ``` 78 | 79 | You can use this as a reference for matching your implementation. 80 | -------------------------------------------------------------------------------- /content/saved-searches.md: -------------------------------------------------------------------------------- 1 | Saved Searches 2 | ============== 3 | 4 | Get Saved Searches 5 | ------------------ 6 | 7 | - `GET /v2/saved_searches.json` will return all saved_searches. 8 | 9 | ```json 10 | [ 11 | { 12 | "id": 1, 13 | "name": "JavaScript", 14 | "query": "javascript is:unread" 15 | } 16 | ] 17 | ``` 18 | 19 | **Status Codes** 20 | 21 | - `200 OK` will be returned 22 | 23 | Get Saved Search 24 | ---------------- 25 | 26 | - `GET /v2/saved_searches/1.json` will return the entry_ids of the matching entries 27 | 28 | ```json 29 | [ 30 | 2058, 31 | 1, 32 | 1525, 33 | 1025, 34 | 410, 35 | 432 36 | ] 37 | ``` 38 | 39 | By default an array of entry ids is returned in the correct order. The theory is that you will already have some or all of the entries and the ones that you don't yet have can be picked up using `GET /v2/entries.json?ids=1,2,3`. 40 | 41 | | Parameter | Required | Example | 42 | | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | 43 | | `include_entries: bool` | false | `GET /v2/saved_searches/1.json?include_entries=true` will return entry objects instead of an array of entry_ids | 44 | | `page: number` | false | `GET /v2/saved_searches.json?page=2` will get page two of the search results | 45 | 46 | **Status Codes** 47 | 48 | - `200 OK` will be returned if found 49 | - `403 Forbidden` will be returned if the user does not own this saved search 50 | 51 | Create Saved Search 52 | ------------------- 53 | 54 | - `POST /v2/saved_searches.json` will create a new saved search with the specified parameters 55 | 56 | **Request** 57 | 58 | ```json 59 | { 60 | "name": "JavaScript", 61 | "query": "javascript is:unread" 62 | } 63 | ``` 64 | 65 | | Parameter | Required | 66 | | --------------- | -------- | 67 | | `name: string` | true | 68 | | `query: string` | true | 69 | 70 | 71 | **Status Codes** 72 | 73 | - `201 Created` will be returned if creating the saved search is successful, the `Location` header will have the URL to the newly created saved search 74 | 75 | Delete Saved Search 76 | ------------------- 77 | 78 | `DELETE /v2/saved_searches/1.json` will delete the saved search with and id of `1` 79 | 80 | **Status Codes** 81 | 82 | - `204 No Content` will be returned if the request was successful 83 | - `403 Forbidden` will be returned if the user does not own this saved search 84 | 85 | Update Saved Search 86 | ------------------- 87 | 88 | Updating a saved search can be used to set a custom title for a feed. 89 | 90 | `PATCH /v2/saved_searches/1.json` will update the saved search with and id of `1` 91 | 92 | 93 | **Request** 94 | 95 | ```json 96 | { 97 | "name": "Unread JavaScript" 98 | } 99 | ``` 100 | 101 | **Response** 102 | 103 | ```json 104 | { 105 | "id": 1, 106 | "name": "Unread JavaScript", 107 | "query": "javascript is:unread" 108 | } 109 | ``` 110 | 111 | **Status Codes** 112 | 113 | - `200 OK` will be returned if the request was successful 114 | - `403 Forbidden` will be returned if the user does not own this saved search 115 | 116 | **PATCH Alternative** 117 | 118 | Some proxies block or filter PATCH requests. A POST alternative is available for these cases: 119 | 120 | `POST /v2/saved_searches/1/update.json` will update the saved search with and id of `1` 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Feedbin API V2 2 | ============== 3 | 4 | This is the official documentation for the Feedbin REST-style API. 5 | 6 | API Objects 7 | ----------- 8 | 9 | - [Authentication](content/authentication.md) 10 | - [Subscriptions](content/subscriptions.md) 11 | - [Entries](content/entries.md) 12 | - [Unread Entries](content/unread-entries.md) 13 | - [Starred Entries](content/starred-entries.md) 14 | - [Taggings](content/taggings.md) 15 | - [Tags](content/tags.md) 16 | - [Saved Searches](content/saved-searches.md) 17 | - [Recently Read Entries](content/recently-read-entries.md) 18 | - [Updated Entries](content/updated-entries.md) 19 | - [Icons](content/icons.md) 20 | - [Imports](content/imports.md) 21 | - [Pages](content/pages.md) 22 | 23 | Tech Notes 24 | ---------- 25 | 26 | - [Extracting Full Content](content/extract-full-content.md) 27 | - [Supporting Twitter](content/supporting-twitter.md) 28 | 29 | Changes 30 | ------- 31 | - 2019-12-27: Added [Pages API](content/pages.md) 32 | - 2019-05-17: Added [Imports API](content/imports.md) 33 | - 2019-03-13: Added [Icons API](content/icons.md) 34 | - 2015-01-22: Added [Updated Entries API](content/updated-entries.md) 35 | - 2015-01-22: Added [Recently Read Entries API](content/recently-read-entries.md) 36 | - 2013-10-14: Added [Saved Search API](content/saved-searches.md) 37 | - 2013-05-20: Added `PATCH subscriptions.json` for editing feed titles to [Subscriptions](content/subscriptions.md#update-subscription) 38 | - 2013-05-20: Added `per_page` paramter to [Entries](content/entries.md) 39 | 40 | Making Requests 41 | --------------- 42 | 43 | The base URL for all requests is `https://api.feedbin.com/v2/` Only https is supported. 44 | 45 | The Feedbin API uses HTTP Basic authentication 46 | 47 | Using cURL you would make a request like: 48 | 49 | ```shell 50 | curl -u 'example@example.com:password' https://api.feedbin.com/v2/subscriptions.json 51 | ``` 52 | 53 | When creating or updating a record you must set `application/json; charset=utf-8` as the `Content-Type` header. 54 | 55 | ```shell 56 | curl -u 'example@example.com:password' \ 57 | -H "Content-Type: application/json; charset=utf-8" \ 58 | -X POST -d '{"feed_url":"http://daringfireball.net"}' \ 59 | https://api.feedbin.com/v2/subscriptions.json 60 | ``` 61 | 62 | Without this header you'll wind up with a `415 Unsupported Media Type` response. 63 | 64 | HTTP Caching, Use It 65 | -------------------- 66 | For extra speed, please use HTTP Caching. `GET` request set ETag and a Last-Modified headers. 67 | 68 | ```shell 69 | curl -v -u 'example@example.com:password' https://api.feedbin.com/v2/subscriptions/3.json 70 | < ETag: "c7d001e87bda1f0d3745b6bd2811b055" 71 | < Last-Modified: Sat, 02 Feb 2013 15:20:46 GMT 72 | ``` 73 | 74 | You can use these headers in subsequent requests like: 75 | 76 | ```shell 77 | curl -v -u 'example@example.com:password' --header 'If-Modified-Since:Sat, 02 Feb 2013 15:20:46 GMT' --header 'If-None-Match:"c7d001e87bda1f0d3745b6bd2811b055"' https://api.feedbin.com/v2/subscriptions/3.json 78 | < HTTP/1.1 304 Not Modified 79 | ``` 80 | 81 | Pagination 82 | ---------- 83 | Entries use pagination. Each page has a limit of 100 items. Paginated request will include a link header like: 84 | 85 | ``` 86 | Links: ; rel="next", ; rel="last" 87 | ``` 88 | 89 | The possible rel values are: 90 | 91 | - `first` The URL of the first page of results. 92 | - `prev` The URL of the previous page of results. 93 | - `next` The URL of the next page of results. 94 | - `last` The URL of the last page of results. 95 | 96 | On paginated resources there is a header in the response called: `X-Feedbin-Record-Count`. This header reports the total number of records for a resource. 97 | 98 | Dates 99 | ----- 100 | The Feedbin API uses the [ISO 8601](http://www.w3.org/TR/NOTE-datetime) date format. All responses will include dates formatted to this spec and all requests should stick to it as well. The dates are as high resolution as possible to allow for rapid state changes. When using the `since` parameter it is best to include the date exactly as it was returned by the server when the last request was made. Rounding down to the nearest second will most likely cause duplicates. 101 | 102 | The date should include complete date plus hours, minutes, seconds and timezone `YYYY-MM-DDThh:mm:ss.ssssssTZD` (eg `2013-02-19T07:33:38.449047-08:00` or in UTC `2013-02-19T15:33:38.449047Z`). All dates will be converted to UTC, so the timezone is very important. 103 | 104 | **Note** 105 | 106 | In Objective-C it's important to force a locale for the date because different regions can cause unexpected output. For example you can properly format a date for the Feedbin API like: 107 | 108 | ```objectivec 109 | NSDateFormatter *feedbinDateFormatter = [[NSDateFormatter alloc] init]; 110 | feedbinDateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; 111 | [feedbinDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"]; 112 | [feedbinDateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]]; 113 | ``` 114 | 115 | Thanks to [Stefan Pauwels](http://zoziapps.ch/) for sharing this tip. 116 | 117 | Domain 118 | ------ 119 | 120 | Until 2014-03-14 the API hostname was `api.feedbin.me`. `api.feedbin.me` will remain available but `api.feedbin.com` is recommended because it is the new primary domain. 121 | -------------------------------------------------------------------------------- /content/subscriptions.md: -------------------------------------------------------------------------------- 1 | Subscriptions 2 | ============= 3 | 4 | Get Subscriptions 5 | ----------------- 6 | 7 | - `GET /v2/subscriptions.json` will return all subscriptions. 8 | 9 | ```json 10 | [ 11 | { 12 | "id": 525, 13 | "created_at": "2013-03-12T11:30:25.209432Z", 14 | "feed_id": 47, 15 | "title": "Daring Fireball", 16 | "feed_url": "http://daringfireball.net/index.xml", 17 | "site_url": "http://daringfireball.net/" 18 | } 19 | ] 20 | ``` 21 | 22 | | Parameter | Required | Example | 23 | | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- | 24 | | `since: date` | false | `GET /v2/subscriptions.json?since=2013-03-08T09:44:20.449047Z` will get all subscriptions created after the iso 8601 timestamp. | 25 | | `mode: enum` | false | `GET /v2/subscriptions.json?mode=extended` the only mode available is `extended`. This includes more metadata for the feed. | 26 | 27 | 28 | **Status Codes** 29 | 30 | - `200 OK` will be returned if found 31 | - `403 Forbidden` will be returned if the user does not own this subscription 32 | 33 | Get Subscription 34 | ---------------- 35 | 36 | - `GET /v2/subscriptions/525.json` will return the feed with an id of `525` 37 | 38 | ```json 39 | { 40 | "id": 525, 41 | "created_at": "2013-03-12T11:30:25.209432Z", 42 | "feed_id": 47, 43 | "title": "Daring Fireball", 44 | "feed_url": "http://daringfireball.net/index.xml", 45 | "site_url": "http://daringfireball.net/" 46 | } 47 | ``` 48 | 49 | #### About `extended` Mode 50 | 51 | The `extended` mode includes a additional meta-data for subscriptions. Currently this includes a `json_feed` key with all of the additional metadata that a [JSON Feed offers](https://jsonfeed.org/version/1). 52 | 53 | ```json 54 | [ 55 | { 56 | "id" : 1, 57 | "feed_id" : 1, 58 | "site_url" : "https://micro.blog/", 59 | "title" : "Micro.blog - manton timeline", 60 | "feed_url" : "https://micro.blog/feeds/manton.json", 61 | "created_at" : "2019-05-23T23:49:14.487938Z", 62 | "json_feed" : { 63 | "favicon" : "https://micro.blog/images/icons/favicon_32.png", 64 | "feed_url" : "https://micro.blog/feeds/manton.json", 65 | "icon" : "https://micro.blog/images/icons/favicon_256.png", 66 | "version" : "https://jsonfeed.org/version/1", 67 | "home_page_url" : "https://micro.blog/", 68 | "title" : "Micro.blog - manton timeline" 69 | } 70 | } 71 | ] 72 | ``` 73 | 74 | **Status Codes** 75 | 76 | - `200 OK` will be returned if found 77 | - `403 Forbidden` will be returned if the user does not own this subscription 78 | 79 | Create Subscription 80 | ------------------- 81 | 82 | - `POST /v2/subscriptions.json` will create a new subscription to the specified `feed_url` 83 | 84 | **Request** 85 | 86 | ```json 87 | { 88 | "feed_url": "http://daringfireball.net/index.xml" 89 | } 90 | ``` 91 | 92 | | Parameter | Required | Example | 93 | | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | 94 | | `feed_url: string` | true | can be the fully qualified url to the feed like `http://daringfireball.net/index.xml`, or the url to the website like `daringfireball.net` | 95 | 96 | 97 | **Status Codes** 98 | 99 | - `201 Created` will be returned if subscription is successful, the `Location` header will have the URL to the newly created subscription 100 | - `302 Found` will be returned if subscription exists, the `Location` header will have the URL to the newly created subscription 101 | - `404 Not Found` will be returned if no feed is found at the `feed_url` 102 | - `300 Multiple Choices` will be returned if multiple feeds are found at the `feed_url` 103 | 104 | If `201` or `302` are returned, the response will be the same as **Get Subscription**: 105 | 106 | **Request** 107 | 108 | ```json 109 | { 110 | "feed_url": "http://daringfireball.net/" 111 | } 112 | ``` 113 | 114 | **Response** 115 | 116 | ```json 117 | { 118 | "id": 4105850, 119 | "created_at": "2017-10-28T14:30:39.324314Z", 120 | "feed_id": 838741, 121 | "title": "Daring Fireball", 122 | "feed_url": "https://daringfireball.net/feeds/main", 123 | "site_url": "https://daringfireball.net/" 124 | } 125 | ``` 126 | 127 | If a `300 Multiple Choices` is returned, it means the requested site exposes more than one feed. In this case the response will let you know what the options are. For example: 128 | 129 | **Request** 130 | 131 | `POST /v2/subscriptions.json` 132 | 133 | ```json 134 | { 135 | "feed_url": "https://blog.github.com" 136 | } 137 | ``` 138 | 139 | **Response** 140 | 141 | ```json 142 | [ 143 | { 144 | "feed_url": "https://github.com/blog.atom", 145 | "title": "The GitHub Blog" 146 | }, 147 | { 148 | "feed_url": "https://github.com/blog/broadcasts.atom", 149 | "title": "The GitHub Blog (Broadcasts)" 150 | } 151 | ] 152 | ``` 153 | 154 | > [!NOTE] 155 | > You can also get the subscription API to return search results. If Feedbin detects a URL or hostname, then Feedbin tries to get the feed from that site. If any other string is passed in `feed_url`, then the API returns relevant search results instead. 156 | 157 | Delete Subscription 158 | ------------------- 159 | 160 | `DELETE /v2/subscriptions/3.json` will delete the subscription with an id of `3` 161 | 162 | **Status Codes** 163 | 164 | - `204 No Content` will be returned if the request was successful 165 | - `403 Forbidden` will be returned if the user does not own this subscription 166 | 167 | Update Subscription 168 | ------------------- 169 | 170 | Updating a subscrition can be used to set a custom title for a feed. 171 | 172 | `PATCH /v2/subscriptions/525.json` will update the subscription with an id of `525` 173 | 174 | 175 | **Request** 176 | 177 | ```json 178 | { 179 | "title": "Custom Title" 180 | } 181 | ``` 182 | 183 | **Response** 184 | 185 | ```json 186 | { 187 | "id": 525, 188 | "created_at": "2013-03-12T11:30:25.209432Z", 189 | "feed_id": 47, 190 | "title": "Custom Title", 191 | "feed_url": "http://daringfireball.net/index.xml", 192 | "site_url": "http://daringfireball.net/" 193 | } 194 | ``` 195 | 196 | **Status Codes** 197 | 198 | - `200 OK` will be returned if the request was successful 199 | - `403 Forbidden` will be returned if the user does not own this subscription 200 | 201 | **PATCH Alternative** 202 | 203 | Some proxies block or filter PATCH requests. A POST alternative is available for these cases: 204 | 205 | `POST /v2/subscriptions/525/update.json` will update the subscription with an id of `525` 206 | 207 | Thanks to [Oliver Fürniß](http://curioustimes.de/) for pointing this out. 208 | -------------------------------------------------------------------------------- /content/pages.md: -------------------------------------------------------------------------------- 1 | Pages 2 | ===== 3 | 4 | The Pages API can be used to [create a new entry](https://feedbin.com/blog/2019/08/20/save-webpages-to-read-later/) from the URL of an article. 5 | 6 | ### `POST /v2/pages.json` 7 | 8 | Create a new page. 9 | 10 | ```bash 11 | curl --request POST \ 12 | --user "example@example.com:password" \ 13 | --header "Content-Type: application/json; charset=utf-8" \ 14 | --data-ascii '{"url": "https://feedbin.com/blog/2018/09/11/private-by-default/", "title": "Private by Default"}' \ 15 | https://api.feedbin.com/v2/pages.json 16 | ``` 17 | 18 | **Request Body** 19 | 20 | ```json 21 | { 22 | "url": "https://feedbin.com/blog/2018/09/11/private-by-default/", 23 | "title": "Private by Default" 24 | } 25 | ``` 26 | 27 | The title is optional and will only be used if Feedbin cannot find the title of the content. 28 | 29 | **Response** 30 | 31 | If successful, the response will be the full [entry](entries.md). 32 | 33 | ```json 34 | { 35 | "author": null, 36 | "content": "
\n

by Ben Ubois

\n

I want Feedbin to be the opposite of Big Social. I think people should have the right not to be tracked on the Internet and Feedbin can help facilitate that.

Since Feedbin is 100% funded by paying customers, I can focus solely on making the best product possible without compromises. Therefore, Feedbin can be private by default.

To me this means eliminating all potential points of leaking user data while using Feedbin.

Since Feedbin displays web content, this isn\u2019t the easiest thing to do. Here are the leaks I\u2019ve identified and eliminated.

iFrames

The biggest visual and functional change is how iFrames work.

Feedbin previously whitelisted a number of iFrame sources like YouTube and Vimeo so you could see embedded content. iFrames embed full web-pages from a 3rd-party source. They\u2019re usually resource intensive to load and they enable cross-site tracking.

Feedbin now replaces all iFrames with a custom new module. The new module still includes the poster frame from videos (where available) and will fetch the title and other metadata.

Clicking on the module will swap in the original iFrame. For YouTube and Vimeo, clicking will also start playing the video.

I prefer the look of this module to the original iFrame. It loads faster, has a clearer, consistent look with richer meta-data, and uses fewer resources doing it.

Third-party JavaScript

Google Analytics is probably the number-one tracker. It\u2019s ubiquitous on the web. For a long time it was a no-brainer to install on any website because you get a lot of functionality for free.

Feedbin used Google Analytics up until April, 2018. It was useful to see some of the stats it provided. The browser stats were good to get a sense of when it would be appropriate to drop support for older browsers. It was also useful to see referrer information to see where customers were coming from.

There are good private alternatives to Google Analytics out there. Matomo is one that I came across. They have a great privacy policy for their hosted product and you can choose to run it yourself for even more control.

I thought about replacing Google Analytics with Matomo, but I came to the same conclusion that it didn\u2019t provide anything I need in order to run Feedbin. Better to not collect that data at all.

Twitter & Instagram embeds were another source of third-party JavaScript I identified. I would bet that the second largest contributor to tracking you across the web, comes from sites that embed social widgets. Feedbin previously used the Twitter and Instagram widgets to render embedded tweets and images that appeared in blog posts. This provided a richer experience by showing the full embed as intended by the author.

However there is an alternative. Both Twitter and Instagram offer public oEmbed endpoints. oEmbed can give you much of the data needed to properly render this content. Feedbin takes this a step further by making the oEmbed requests from the server. If your browser made the requests client-side, this would give the publishers the opportunity to read and set tracking cookies. The end result is that you see pretty much the same content as you did before.

JavaScript in blog posts is worth mentioning. RSS uses HTML for rendering content. All HTML is allowed including <script> tags. Feedbin has always used an HTML sanitizer to strip dangerous content out of posts, including scripts, since that would be the definition of an XSS vulnerability.

Images

Images are another potential source of leaking data. Feedbin has used an image proxy since launch to prevent mixed content warnings. A side benefit of the image proxy, is that your browser only makes requests to the proxy and the proxy gets the image data, preventing your request from reaching the origin.

Fonts

Feedbin has the option to use fonts from Hoefler & Co.. This requires a single request to their service, which means that they have the opportunity to track you if they wish. To eliminate this source, the default article font is now a system font. Custom fonts will only be loaded if they\u2019re chosen.

Exceptions

Stripe is the only third-party exception I can think of. Stripe provides the invaluable functionality of billing and subscriptions. Using Stripe means Feedbin does not have to collect, store or ever see any sensitive payment data. However, since Stripe makes their money from paying customers, I think they are incentivized to be careful with this data. Their privacy policy has more details on how they store and use data.

I think with these changes in place, the only external requests that should ever be made by your browser, with the exception of Stripe, are ones initiated by you.

\n
", 37 | "created_at": "2019-12-27T18:16:19.986803Z", 38 | "extracted_content_url": "https://extract.feedbin.com/parser/feedbin/04a050a680b063a1f5033927fdd665eb472f5ee0?base64_url=aHR0cHM6Ly9mZWVkYmluLmNvbS9ibG9nLzIwMTgvMDkvMTEvcHJpdmF0ZS1ieS1kZWZhdWx0Lw==", 39 | "feed_id": 6, 40 | "id": 109, 41 | "published": "2018-09-11T00:00:00.000000Z", 42 | "summary": "September 11, 2018 by Ben Ubois I want Feedbin to be the opposite of Big Social. I think people should have the right not to be tracked on the Internet and Feedbin can help facilitate that. Since Feedbin is 100% funded by paying customers, I can focus", 43 | "title": "Private by Default", 44 | "url": "https://feedbin.com/blog/2018/09/11/private-by-default/" 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /content/updated-entries.md: -------------------------------------------------------------------------------- 1 | Updated Entries 2 | =============== 3 | 4 | Updated entries are entries that have been modified after they were originally published. 5 | 6 | Feedbin stores both the original version of an entry and the latest version. 7 | 8 | On the web [Feedbin gives the user an option to show a diff of the original and the latest](http://blog.feedbin.com/2014/12/16/never-miss-an-update-with-the-new-updated-section/) which highlights what has changed. 9 | 10 | Using this API 11 | -------------- 12 | 13 | The updated entry API gives you a list of entry ids that have been updated. Once you know which ids to ask for you can use the `entries` API to get the actual changes: 14 | 15 | 1. `include_original` 16 | 1. `include_content_diff` 17 | 18 | For example: 19 | 20 | `GET /v2/entries.json?include_original=true&include_content_diff=true&ids=703369824` 21 | 22 | ```json 23 | [ 24 | { 25 | "id": 703369824, 26 | "feed_id": 47, 27 | "title": "The Talk Show: \u2018Malaprops\u2019", 28 | "author": "John Gruber", 29 | "content": "

New episode of my podcast, The Talk Show, with special guest Ben Thompson<\/a>. Topics include Apple\u2019s pseudo \u201csabbaticals\u201d (employees who leave the company but then return after a year or two); Google\u2019s cultural similarities to Microsoft; the ways that Apple (and iOS users) might miss Scott Forstall; accessibility as a high priority for Apple; Instagram\u2019s success (and how they effectively ate Hipstamatic\u2019s lunch); a debate on just how \u201csimple\u201d Twitter is; Box\u2019s successful IPO, and Dropbox\u2019s support for Yosemite\u2019s official Finder integration for such services; MIT economist Jonathan Gruber pissing in my Google juice; Chromebooks; Amazon\u2019s overall strategy, and the colossal failure of their Fire Phone; and, lastly, a good chunk on Microsoft\u2019s Windows 10\/HoloLens event last week.<\/p>\n\n

Brought to you by three excellent sponsors:<\/p>\n\n