├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .env.template ├── .github └── workflows │ └── postman-sync.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── notices.txt ├── package.json ├── postman └── threads-api.postman_collection.json ├── public ├── css │ └── style.css ├── favicon.ico ├── img │ ├── attachment.png │ └── loading.gif └── scripts │ ├── form.js │ ├── hide-reply.js │ ├── publish.js │ └── upload.js ├── src └── index.js ├── test_data ├── test-video-threads-bad-audio-bit-rate.mp4 ├── test-video-threads-bad-frame-rate.mp4 ├── test-video-threads-bad-video-bit-rate.mp4 └── test-video-threads.mp4 └── views ├── account.pug ├── index.pug ├── insights.pug ├── keyword_search.pug ├── layout.pug ├── layout_with_account.pug ├── mentions.pug ├── oembed.pug ├── publish.pug ├── publishing_limit.pug ├── thread.pug ├── thread_conversation.pug ├── thread_insights.pug ├── thread_replies.pug ├── thread_replies_layout.pug ├── threads.pug ├── upload.pug └── user_insights.pug /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | RUN wget https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 4 | RUN mv mkcert-v1.4.4-linux-amd64 /usr/bin/mkcert 5 | RUN chmod +x /usr/bin/mkcert 6 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "Dockerfile" 4 | }, 5 | "forwardPorts": [ 6 | "localhost:8000" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | HOST=threads-sample.meta 2 | APP_ID=YOUR_APP_ID 3 | API_SECRET=YOUR_APP_SECRET 4 | REDIRECT_URI=https://threads-sample.meta:8000/callback 5 | SESSION_SECRET=RANDOM_SESSION_SECRET_STRING 6 | PORT=8000 7 | 8 | # Optional config setting to change the Graph API version. 9 | # Leave blank or commented to default to the app's default version 10 | # GRAPH_API_VERSION=v1.0 11 | 12 | # If both INITIAL_ACCESS_TOKEN and INITIAL_USER_ID are provided, then the authentication step is bypassed once. 13 | # These settings can be useful when testing the publishing or reading functionality, as it 14 | # prevents having to authenticate each time the session is destroyed. 15 | # INITIAL_ACCESS_TOKEN=SOME-ACCESS-TOKEN 16 | # INITIAL_USER_ID=SOME-USER-ID 17 | -------------------------------------------------------------------------------- /.github/workflows/postman-sync.yaml: -------------------------------------------------------------------------------- 1 | name: Sync Postman Collection 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | sync: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Upload Postman Collection 17 | run: | 18 | curl -L -X PUT 'https://api.getpostman.com/collections/34203612-b2ec0e9b-19bb-4a28-b96f-ee426777509c' \ 19 | --header 'X-Api-Key: ${{ secrets.POSTMAN_API_KEY }}' \ 20 | -F 'collection=@postman/threads-api.postman_collection.json;type=application/json' 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Ignore Pem 5 | *.pem 6 | 7 | # Local folder 8 | local/ 9 | 10 | .DS_Store 11 | 12 | .env 13 | 14 | package-lock.json 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Threads API Sample App 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Our Development Process 6 | ... (in particular how this is synced with internal changes to the project) 7 | 8 | ## Pull Requests 9 | We actively welcome your pull requests. 10 | 11 | 1. Fork the repo and create your branch from `main`. 12 | 2. If you've added code that should be tested, add tests. 13 | 3. If you've changed APIs, update the documentation. 14 | 4. Ensure the test suite passes. 15 | 5. Make sure your code lints. 16 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 17 | 18 | ## Contributor License Agreement ("CLA") 19 | In order to accept your pull request, we need you to submit a CLA. You only need 20 | to do this once to work on any of Meta's open source projects. 21 | 22 | Complete your CLA here: 23 | 24 | ## Issues 25 | We use GitHub issues to track public bugs. Please ensure your description is 26 | clear and has sufficient instructions to be able to reproduce the issue. 27 | 28 | Meta has a [bounty program](https://bugbounty.meta.com/) for the safe 29 | disclosure of security bugs. In those cases, please go through the process 30 | outlined on that page and do not file a public issue. 31 | 32 | ## License 33 | By contributing to Threads API Sample App, you agree that your contributions will be licensed 34 | under the LICENSE file in the root directory of this source tree. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Meta Platforms, Inc. and affiliates. 2 | All rights reserved. 3 | 4 | You are hereby granted a non-exclusive, worldwide, royalty-free license to use, 5 | copy, modify, and distribute this software in source code or binary form for use 6 | in connection with the web services and APIs provided by Facebook. 7 | 8 | As with any software that integrates with the Facebook platform, your use of 9 | this software is subject to the Facebook Platform Policy 10 | [http://developers.facebook.com/policy/]. This copyright notice shall be 11 | included in all copies or substantial portions of the software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Threads Publishing API Sample App 2 | 3 | > ⚠️ We will update the sample app over time. Please note that all of the latest features may not be demonstrated in the sample app. Please refer to the [developer documentation changelog](https://developers.facebook.com/docs/threads/changelog) for the most up-to-date features. 4 | 5 | You can use this Sample App to test the [Threads API](https://developers.facebook.com/docs/threads). 6 | 7 | 1. Make sure that you are using the APP ID and Secret defined for the Threads API of your app. These ARE not the same as the regular app ID and app secret. 8 | 2. Make sure you add your application's redirect URL e.g. https://threads-sample.meta:8000/callback, to your app's redirect callback URLs in the app dashboard. 9 | 10 | ## Required software 11 | 12 | In order to run the Sample App you will need to install some required software, as follows: 13 | 14 | - Node JS 15 | 16 | ## Running the Sample App 17 | 18 | Note: If you are using devcontainers, ensure that containers are enabled and supported by your IDE. 19 | 20 | 1. Install necessary tools 21 | * If you are using a [devcontainer](https://code.visualstudio.com/docs/devcontainers/containers), skip to step 2. 22 | * Install [nodeJS](https://nodejs.org/en/download/) to run the application. If you're using a Mac, you can install it via Homebrew: `brew install node` 23 | * Install [mkcert](https://mkcert.org/) to create the OpenSSL Certificate. If you're using a Mac, you can install it via Homebrew: `brew install mkcert` 24 | 25 | 2. Run `npm install` in your terminal 26 | 27 | 3. Create a new file called `.env` and copy/paste all the environment variables from `.env.template`. Replace any environnment variables that have placeholders, such as APP_ID. 28 | 29 | 4. Map a domain to your local machine for local development 30 | * Note: Threads apps do not support redirect URLs with using `localhost` so you must map a domain to test locally this Sample App. 31 | * Map a new entry in your hosts file to the domain that you will use to test the Sample App e.g. `threads-sample.meta`. 32 | * If you're using a Linux or Mac, this will be your `/etc/hosts` file. 33 | * Add an entry like so: 34 | ``` 35 | 127.0.0.1 threads-sample.meta 36 | ``` 37 | * This will map threads-sample.meta to localhost, so you may visit https://threads-sample.meta:8000 to see the Threads Sample App. 38 | * This domain must match the one defined in your `.env` file as the value of the `HOST` variable. 39 | 40 | 5. Create an OpenSSL Cert 41 | * OAuth redirects are only supported over HTTPS so you must create an SSL certificate 42 | * `mkcert threads-sample.meta` - This will create pem files for SSL. 43 | * You will see `threads-sample.meta.pem` and `threads-sample.meta-key.pem `files generated. 44 | * If you are using a host that is different than `threads-sample.meta` then replace it with your specific domain. 45 | 46 | 6. Run the Sample App 47 | * Run `npm start` from the command line. 48 | * Once the Sample App starts running, go to https://threads-sample.meta:8000 to test the workflow. 49 | * If you are using a different domain or port then replace those values accordingly. 50 | 51 | ## License 52 | Threads API is Meta Platform Policy licensed, as found in the LICENSE file. 53 | -------------------------------------------------------------------------------- /notices.txt: -------------------------------------------------------------------------------- 1 | Third Party Notices 2 | THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF THIS PRODUCT. 3 | 4 | BSD 2-Clause "Simplified" License 5 | 6 | The following components are licensed under BSD 2-Clause "Simplified" License reproduced below: 7 | * dotenv, Copyright (c) 2015, Scott Motte, All rights reserved. 8 | 9 | Copyright (c) . All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without modification, 12 | are permitted provided that the following conditions are met: 13 | 14 | 1. Redistributions of source code must retain the above copyright notice, 15 | this list of conditions and the following disclaimer. 16 | 17 | 2. Redistributions in binary form must reproduce the above copyright notice, 18 | this list of conditions and the following disclaimer in the documentation 19 | and/or other materials provided with the distribution. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 25 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 30 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | 33 | 34 | 35 | ISC License 36 | 37 | The following components are licensed under ISC License reproduced below: 38 | * axios 39 | 40 | ISC License: 41 | 42 | Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") 43 | Copyright (c) 1995-2003 by Internet Software Consortium 44 | 45 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 48 | 49 | 50 | 51 | 52 | MIT License 53 | 54 | The following components are licensed under MIT License reproduced below: 55 | * express, Copyright (c) 2009-2014 TJ Holowaychuk , Copyright (c) 2013-2014 Roman Shtylman , Copyright (c) 2014-2015 Douglas Christopher Wilson 56 | * express-session, Copyright (c) 2010 Sencha Inc., Copyright (c) 2011 TJ Holowaychuk , Copyright (c) 2014-2015 Douglas Christopher Wilson 57 | * multer, Copyright (c) 2014 Hage Yaapa <[http://www.hacksparrow.com](http://www.hacksparrow.com)> 58 | * pug, Copyright (c) 2009-2014 TJ Holowaychuk 59 | 60 | MIT License Copyright (c) 61 | 62 | Permission is hereby granted, free of charge, to any person obtaining a copy 63 | of this software and associated documentation files (the "Software"), to deal 64 | in the Software without restriction, including without limitation the rights 65 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 66 | copies of the Software, and to permit persons to whom the Software is furnished 67 | to do so, subject to the following conditions: 68 | 69 | The above copyright notice and this permission notice (including the next 70 | paragraph) shall be included in all copies or substantial portions of the 71 | Software. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 75 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 76 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 77 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 78 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threads_api_sample", 3 | "version": "1.0.0", 4 | "description": "Threads API Sample App", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./src/index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^1.6.1", 14 | "dotenv": "^16.0.1", 15 | "express": "^4.17.3", 16 | "express-session": "^1.17.2", 17 | "multer": "^1.4.5-lts.1", 18 | "pug": "^3.0.2" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^8.15.0", 22 | "eslint-config-prettier": "^8.5.0", 23 | "eslint-plugin-prettier": "^4.0.0", 24 | "prettier": "^2.6.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /postman/threads-api.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "a93be385-2264-4766-826f-b53cb6f50d7e", 4 | "name": "Threads API", 5 | "description": "> ⚠️ We will update the Postman collection over time. Please note that all of the latest features may not be showcased in the Postman collection. Please refer to the developer documentation changelog for the most up-to-date features: [https://developers.facebook.com/docs/threads/changelog](https://developers.facebook.com/docs/threads/changelog) \n \n\nWelcome to the Threads API from Meta. This collection contains the [Graph API](https://developers.facebook.com/docs/graph-api/) requests to create, manage, and publish Threads content, programmatically. For the full Threads documentation, refer to the [developer documentation](https://developers.facebook.com/docs/threads).\n\n# Getting Started\n\n## Step 1 - Create a Meta App\n\nYou need to [create a Meta app with the Threads use case](https://developers.facebook.com/docs/threads/get-started#meta-app).\n\n## Step 2 - Enable App Users To Authorize Data Access To Your App\n\nApp users must authorize data access for the app you are developing. Ensure that proper authorization protocols are implemented to comply with data privacy and security requirements. For detailed instructions and guidelines, refer to the [authorization documentation](https://developers.facebook.com/docs/threads/get-started#authorization).\n\n## Step 3 - Request an Access Token\n\n### API Provisioned Token\n\nTo request an access token via the API, use the requests under the _**Authorization**_ folder within this collection. See the [Threads User Access Tokens](https://developers.facebook.com/docs/threads/get-started#threads-user-access-tokens) section in the Threads API documentation for details on authenticating your requests. Once you have an access token, you will need to manually add the **access_token** header to your requests with this value.\n\nNote: You can receive an access token for your requests either by using the requests under the _**Authorization**_ folder within this collection, or by using the _Authorization_ tab above in which the token will be provisioned by Postman.\n\n### Postman Provisioned Token\n\nIf using the _Authorization_ tab to receive an access token, make sure to include the following:\n\n- The Grant type must be set to **Authorization Code**.\n \n- You must supply the values for your Auth URL, Access Token URL, Client ID (your Threads app's ID), Client Secret (your Threads app's secret), and Scope.\n \n\nAfter these values are entered, you may press **Get New Access Token** at the bottom of the page to receive an access token. This access token will automatically be attached to your requests.\n\n### Access Token Debugger\n\nYou can paste any token you generate into the [access token debugger](https://developers.facebook.com/tools/debug/accesstoken/) to see what type of token it is and what permissions you have granted to your app.\n\n# Limitations\n\nPlease be aware that certain limitations may apply to the API usage. For the most accurate and up-to-date details, we strongly recommend referring to the online documentation. Visit the [developer documentation](https://developers.facebook.com/docs/threads/overview#rate-limiting) for more information.\n\n# Using Variables\n\nWhen using variables in requests (see the _Variables_ tab above), make sure to **save the updated values** after modifying them (e.g. with Ctrl-S / Cmd-S). The values must be saved in the _Variables_ tab each time they are modified.\n\n# Autosaving Variables\n\nVariable values may be auto-updated and saved after a request. For example, when a new thread is published, the _thread_id_ field can be auto-updated with the new thread ID to be used in retrieval requests.\n\nTo use this feature, make sure to **set the auto_save_variables variable value to true**. Otherwise, variables will not be auto-updated and you will need to update them manually.\n\n# Installation\n\nFork this collection into Postman to start using the Threads API.\n\nYou can generate the code for the API calls in your language by following the steps here: [https://learning.postman.com/docs/sending-requests/generate-code-snippets/#generating-code-snippets-in-postman](https://learning.postman.com/docs/sending-requests/generate-code-snippets/#generating-code-snippets-in-postmanEnvironmentChangelogUpdates)", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 7 | "_exporter_id": "34767301", 8 | "_collection_link": "https://www.postman.com/meta/threads/collection/dht3nzz/threads-api?action=share&source=collection_link&creator=29882223" 9 | }, 10 | "item": [ 11 | { 12 | "name": "Authorization", 13 | "item": [ 14 | { 15 | "name": "Exchange the Code For a Token", 16 | "request": { 17 | "auth": { 18 | "type": "noauth" 19 | }, 20 | "method": "POST", 21 | "header": [], 22 | "body": { 23 | "mode": "urlencoded", 24 | "urlencoded": [] 25 | }, 26 | "url": { 27 | "raw": "{{api_host}}/oauth/access_token?client_id={{app_id}}&client_secret={{app_secret}}&code={{code}}&grant_type=authorization_code&redirect_uri={{redirect_uri}}", 28 | "host": [ 29 | "{{api_host}}" 30 | ], 31 | "path": [ 32 | "oauth", 33 | "access_token" 34 | ], 35 | "query": [ 36 | { 37 | "key": "client_id", 38 | "value": "{{app_id}}", 39 | "description": "Your Threads App ID displayed in App Dashboard > App settings > Basic > Threads App ID." 40 | }, 41 | { 42 | "key": "client_secret", 43 | "value": "{{app_secret}}", 44 | "description": "Your Threads App Secret displayed in App Dashboard > App settings > Basic > Threads App secret." 45 | }, 46 | { 47 | "key": "code", 48 | "value": "{{code}}", 49 | "description": "The authorization code we passed you in the code parameter when redirecting the user to your redirect_uri." 50 | }, 51 | { 52 | "key": "grant_type", 53 | "value": "authorization_code", 54 | "description": "This value is required. Set this value to authorization_code." 55 | }, 56 | { 57 | "key": "redirect_uri", 58 | "value": "{{redirect_uri}}", 59 | "description": "The redirect URI you passed when you directed the user to the Authorization Window. This must be the same URI or the request will be rejected." 60 | } 61 | ] 62 | }, 63 | "description": "Once you receive a code, exchange it for a short-lived access token by sending a `POST` request to the /oauth/access_token [endpoint](https://developers.facebook.com/docs/threads/get-started/get-access-tokens-and-permissions#step-2--exchange-the-code-for-a-token)" 64 | }, 65 | "response": [ 66 | { 67 | "name": "Exchange the Code For a Token", 68 | "originalRequest": { 69 | "method": "POST", 70 | "header": [], 71 | "body": { 72 | "mode": "urlencoded", 73 | "urlencoded": [] 74 | }, 75 | "url": { 76 | "raw": "{{api_host}}/oauth/access_token?client_id={{app_id}}&client_secret={{app_secret}}&code={{code}}&grant_type=authorization_code&redirect_uri={{redirect_uri}}", 77 | "host": [ 78 | "{{api_host}}" 79 | ], 80 | "path": [ 81 | "oauth", 82 | "access_token" 83 | ], 84 | "query": [ 85 | { 86 | "key": "client_id", 87 | "value": "{{app_id}}", 88 | "description": "Your Threads App ID displayed in App Dashboard > App settings > Basic > Threads App ID." 89 | }, 90 | { 91 | "key": "client_secret", 92 | "value": "{{app_secret}}", 93 | "description": "Your Threads App Secret displayed in App Dashboard > App settings > Basic > Threads App secret." 94 | }, 95 | { 96 | "key": "code", 97 | "value": "{{code}}", 98 | "description": "The authorization code we passed you in the code parameter when redirecting the user to your redirect_uri." 99 | }, 100 | { 101 | "key": "grant_type", 102 | "value": "authorization_code", 103 | "description": "This value is required. Set this value to authorization_code." 104 | }, 105 | { 106 | "key": "redirect_uri", 107 | "value": "{{redirect_uri}}", 108 | "description": "The redirect URI you passed when you directed the user to the Authorization Window. This must be the same URI or the request will be rejected." 109 | } 110 | ] 111 | } 112 | }, 113 | "_postman_previewlanguage": "json", 114 | "header": [ 115 | { 116 | "key": "Content-Type", 117 | "value": "application/json", 118 | "description": "", 119 | "type": "text" 120 | } 121 | ], 122 | "cookie": [], 123 | "body": "{\n \"access_token\": \"string\",\n \"user_id\": \"string\"\n}" 124 | } 125 | ] 126 | }, 127 | { 128 | "name": "Get Long-Lived Access Token", 129 | "request": { 130 | "method": "GET", 131 | "header": [], 132 | "url": { 133 | "raw": "{{api_host}}/access_token?grant_type=th_exchange_token&client_secret={{app_secret}}", 134 | "host": [ 135 | "{{api_host}}" 136 | ], 137 | "path": [ 138 | "access_token" 139 | ], 140 | "query": [ 141 | { 142 | "key": "grant_type", 143 | "value": "th_exchange_token", 144 | "description": "This value is required. Set this to th_exchange_token." 145 | }, 146 | { 147 | "key": "client_secret", 148 | "value": "{{app_secret}}", 149 | "description": "Your Threads App Secret displayed in App Dashboard > App settings > Basic > Threads App secret." 150 | } 151 | ] 152 | }, 153 | "description": "Use the `GET` /access_token [endpoint](https://developers.facebook.com/docs/threads/get-started/long-lived-tokens#get-a-long-lived-token) to exchange a short-lived Threads user access token for a long-lived token." 154 | }, 155 | "response": [ 156 | { 157 | "name": "Get Long-Lived Access Token", 158 | "originalRequest": { 159 | "method": "GET", 160 | "header": [], 161 | "url": { 162 | "raw": "{{api_host}}/access_token?grant_type=th_exchange_token&client_secret={{app_secret}}", 163 | "host": [ 164 | "{{api_host}}" 165 | ], 166 | "path": [ 167 | "access_token" 168 | ], 169 | "query": [ 170 | { 171 | "key": "grant_type", 172 | "value": "th_exchange_token", 173 | "description": "This value is required. Set this to th_exchange_token." 174 | }, 175 | { 176 | "key": "client_secret", 177 | "value": "{{app_secret}}", 178 | "description": "Your Threads App Secret displayed in App Dashboard > App settings > Basic > Threads App secret." 179 | } 180 | ] 181 | } 182 | }, 183 | "_postman_previewlanguage": "json", 184 | "header": [ 185 | { 186 | "key": "Content-Type", 187 | "value": "application/json", 188 | "description": "", 189 | "type": "text" 190 | } 191 | ], 192 | "cookie": [], 193 | "body": "{\n \"access_token\": \"string\",\n \"token_type\": \"bearer\",\n \"expires_in\": 5184000\n}" 194 | } 195 | ] 196 | }, 197 | { 198 | "name": "Get App Access Token", 199 | "event": [ 200 | { 201 | "listen": "test", 202 | "script": { 203 | "exec": [ 204 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 205 | " var jsonData = pm.response.json();", 206 | " ", 207 | " if (jsonData.access_token)", 208 | " pm.collectionVariables.set(\"app_access_token\", jsonData.access_token);", 209 | "}" 210 | ], 211 | "type": "text/javascript", 212 | "packages": {} 213 | } 214 | } 215 | ], 216 | "request": { 217 | "auth": { 218 | "type": "noauth" 219 | }, 220 | "method": "GET", 221 | "header": [], 222 | "url": { 223 | "raw": "{{api_host}}/oauth/access_token?grant_type=client_credentials&client_id={{app_id}}&client_secret={{app_secret}}", 224 | "host": [ 225 | "{{api_host}}" 226 | ], 227 | "path": [ 228 | "oauth", 229 | "access_token" 230 | ], 231 | "query": [ 232 | { 233 | "key": "grant_type", 234 | "value": "client_credentials", 235 | "description": "This value is required. Set this value to client_credentials." 236 | }, 237 | { 238 | "key": "client_id", 239 | "value": "{{app_id}}", 240 | "description": "Your Threads App ID displayed in App Dashboard > App settings > Basic > Threads App ID." 241 | }, 242 | { 243 | "key": "client_secret", 244 | "value": "{{app_secret}}", 245 | "description": "Your Threads App Secret displayed in App Dashboard > App settings > Basic > Threads App secret." 246 | } 247 | ] 248 | } 249 | }, 250 | "response": [ 251 | { 252 | "name": "Get App Access Token", 253 | "originalRequest": { 254 | "method": "GET", 255 | "header": [], 256 | "url": { 257 | "raw": "{{api_host}}/oauth/access_token?grant_type=client_credentials&client_id={{app_id}}&client_secret={{app_secret}}", 258 | "host": [ 259 | "{{api_host}}" 260 | ], 261 | "path": [ 262 | "oauth", 263 | "access_token" 264 | ], 265 | "query": [ 266 | { 267 | "key": "grant_type", 268 | "value": "client_credentials" 269 | }, 270 | { 271 | "key": "client_id", 272 | "value": "{{app_id}}" 273 | }, 274 | { 275 | "key": "client_secret", 276 | "value": "{{app_secret}}" 277 | } 278 | ] 279 | } 280 | }, 281 | "_postman_previewlanguage": null, 282 | "header": null, 283 | "cookie": [], 284 | "body": "{\n \"access_token\": \"TH|1234567890|abcd1234\",\n \"token_type\": \"bearer\"\n}" 285 | } 286 | ] 287 | } 288 | ], 289 | "description": "Authorization is a required step to get the necessary user permissions to act on behalf of the user. Please take a look at the [walkthrough of the authentication process](https://developers.facebook.com/docs/threads/get-started/get-access-tokens-and-permissions). Once the access token is retrieved, use the [access token debugger](https://developers.facebook.com/tools/debug/accesstoken) to check if the access token has the correct permissions (including `threads_basic`, `threads_content_publish`, `threads_read_replies`, `threads_manage_replies`, `threads_manage_insights`) and verify that the access token hasn't expired.\n\nFollow the [documentation](https://developers.facebook.com/docs/threads/get-started/long-lived-tokens) to exchange a short-lived Threads User Access Token for a long-lived token and refresh unexpired long-lived Threads User Access tokens. **Note:** The `threads_basic` permission is sufficient for the exchange or refresh process." 290 | }, 291 | { 292 | "name": "Post to Threads", 293 | "item": [ 294 | { 295 | "name": "Single Threads Posts", 296 | "item": [ 297 | { 298 | "name": "1.1 Create Text Container", 299 | "event": [ 300 | { 301 | "listen": "test", 302 | "script": { 303 | "exec": [ 304 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 305 | " var jsonData = pm.response.json();", 306 | " ", 307 | " if (jsonData.id)", 308 | " pm.collectionVariables.set(\"container_id\", jsonData.id);", 309 | "}" 310 | ], 311 | "type": "text/javascript", 312 | "packages": {} 313 | } 314 | } 315 | ], 316 | "request": { 317 | "method": "POST", 318 | "header": [], 319 | "url": { 320 | "raw": "{{api_host}}/me/threads?text=This is a test post.&media_type=TEXT", 321 | "host": [ 322 | "{{api_host}}" 323 | ], 324 | "path": [ 325 | "me", 326 | "threads" 327 | ], 328 | "query": [ 329 | { 330 | "key": "text", 331 | "value": "This is a test post.", 332 | "description": "The text associated with the post. For text-only posts, this parameter is required if no link attachment parameter is provided. When no link attachment parameter is provided, the first URL included in the text field will be used as the link preview for the post." 333 | }, 334 | { 335 | "key": "media_type", 336 | "value": "TEXT", 337 | "description": "Indicates the current media type. Set to TEXT. Note: Type CAROUSEL is not available for single thread posts." 338 | }, 339 | { 340 | "key": "reply_control", 341 | "value": "everyone", 342 | "description": "Can be used to specify who can reply to a post.", 343 | "disabled": true 344 | }, 345 | { 346 | "key": "reply_to_id", 347 | "value": "", 348 | "description": "Required if replying to a specific post.", 349 | "disabled": true 350 | }, 351 | { 352 | "key": "link_attachment", 353 | "value": "https://developers.facebook.com/", 354 | "description": "Attach a link to your post (only available for text posts, and not compatible with poll attachments).", 355 | "disabled": true 356 | }, 357 | { 358 | "key": "poll_attachment", 359 | "value": "{\"option_a\":\"first\",\"option_b\":\"second\",\"option_c\":\"third\",\"option_d\":\"fourth\"}", 360 | "description": "Attach a poll to your post (only available for text posts, and not compatible with link attachments).", 361 | "disabled": true 362 | } 363 | ] 364 | }, 365 | "description": "Use the `POST` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/posts#step-1--create-a-threads-media-container) to create a text container." 366 | }, 367 | "response": [ 368 | { 369 | "name": "1.1 Create Text Container", 370 | "originalRequest": { 371 | "method": "POST", 372 | "header": [], 373 | "url": { 374 | "raw": "{{api_host}}/me/threads?text=This is a test post.&media_type=TEXT", 375 | "host": [ 376 | "{{api_host}}" 377 | ], 378 | "path": [ 379 | "me", 380 | "threads" 381 | ], 382 | "query": [ 383 | { 384 | "key": "text", 385 | "value": "This is a test post.", 386 | "description": "The text associated with the post. For text-only posts, this parameter is required if no link attachment parameter is provided. When no link attachment parameter is provided, the first URL included in the text field will be used as the link preview for the post." 387 | }, 388 | { 389 | "key": "media_type", 390 | "value": "TEXT", 391 | "description": "Indicates the current media type. Set to TEXT. Note: Type CAROUSEL is not available for single thread posts." 392 | }, 393 | { 394 | "key": "reply_control", 395 | "value": "everyone", 396 | "description": "Can be used to specify who can reply to a post.", 397 | "disabled": true 398 | }, 399 | { 400 | "key": "reply_to_id", 401 | "value": null, 402 | "description": "Required if replying to a specific post.", 403 | "disabled": true 404 | }, 405 | { 406 | "key": "link_attachment", 407 | "value": "https://developers.facebook.com/", 408 | "description": "Attach a link to your post (only available for text posts).", 409 | "disabled": true 410 | } 411 | ] 412 | } 413 | }, 414 | "_postman_previewlanguage": "json", 415 | "header": [ 416 | { 417 | "key": "Content-Type", 418 | "value": "application/json", 419 | "description": "", 420 | "type": "text" 421 | } 422 | ], 423 | "cookie": [], 424 | "body": "{\n \"id\": \"string\"\n}" 425 | } 426 | ] 427 | }, 428 | { 429 | "name": "1.2 Create Image Container", 430 | "event": [ 431 | { 432 | "listen": "test", 433 | "script": { 434 | "exec": [ 435 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 436 | " var jsonData = pm.response.json();", 437 | " ", 438 | " if (jsonData.id)", 439 | " pm.collectionVariables.set(\"container_id\", jsonData.id);", 440 | "}" 441 | ], 442 | "type": "text/javascript", 443 | "packages": {} 444 | } 445 | } 446 | ], 447 | "request": { 448 | "method": "POST", 449 | "header": [], 450 | "url": { 451 | "raw": "{{api_host}}/me/threads?text=This is an image.&media_type=IMAGE&image_url={{image_url}}&alt_text=An example image.", 452 | "host": [ 453 | "{{api_host}}" 454 | ], 455 | "path": [ 456 | "me", 457 | "threads" 458 | ], 459 | "query": [ 460 | { 461 | "key": "text", 462 | "value": "This is an image.", 463 | "description": "The text associated with the post. This is optional on image posts." 464 | }, 465 | { 466 | "key": "media_type", 467 | "value": "IMAGE", 468 | "description": "Indicates the current media type. Set to IMAGE. Note: Type CAROUSEL is not available for single thread posts." 469 | }, 470 | { 471 | "key": "image_url", 472 | "value": "{{image_url}}", 473 | "description": "(For images only.) The path to the image. We will cURL your image using the URL provided so it must be on a public server." 474 | }, 475 | { 476 | "key": "alt_text", 477 | "value": "An example image.", 478 | "description": "The accessibility label or alt text for the image." 479 | }, 480 | { 481 | "key": "reply_control", 482 | "value": "everyone", 483 | "description": "Can be used to specify who can reply to a post.", 484 | "disabled": true 485 | }, 486 | { 487 | "key": "reply_to_id", 488 | "value": "", 489 | "description": "Required if replying to a specific post.", 490 | "disabled": true 491 | } 492 | ] 493 | }, 494 | "description": "Use the `POST` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/posts#step-1--create-a-threads-media-container) to create an image container." 495 | }, 496 | "response": [ 497 | { 498 | "name": "1.2 Create Image Container", 499 | "originalRequest": { 500 | "method": "POST", 501 | "header": [], 502 | "url": { 503 | "raw": "{{api_host}}/me/threads?text=This is an image.&media_type=IMAGE&image_url={{image_url}}&alt_text=An example image.", 504 | "host": [ 505 | "{{api_host}}" 506 | ], 507 | "path": [ 508 | "me", 509 | "threads" 510 | ], 511 | "query": [ 512 | { 513 | "key": "text", 514 | "value": "This is an image.", 515 | "description": "The text associated with the post. This is optional on image posts." 516 | }, 517 | { 518 | "key": "media_type", 519 | "value": "IMAGE", 520 | "description": "Indicates the current media type. Set to IMAGE. Note: Type CAROUSEL is not available for single thread posts." 521 | }, 522 | { 523 | "key": "image_url", 524 | "value": "{{image_url}}", 525 | "description": "(For images only.) The path to the image. We will cURL your image using the URL provided so it must be on a public server." 526 | }, 527 | { 528 | "key": "alt_text", 529 | "value": "An example image.", 530 | "description": "The accessibility label or alt text for the image." 531 | }, 532 | { 533 | "key": "reply_control", 534 | "value": "everyone", 535 | "description": "Can be used to specify who can reply to a post.", 536 | "disabled": true 537 | }, 538 | { 539 | "key": "reply_to_id", 540 | "value": null, 541 | "description": "Required if replying to a specific post.", 542 | "disabled": true 543 | } 544 | ] 545 | } 546 | }, 547 | "_postman_previewlanguage": "json", 548 | "header": [ 549 | { 550 | "key": "Content-Type", 551 | "value": "application/json", 552 | "description": "", 553 | "type": "text" 554 | } 555 | ], 556 | "cookie": [], 557 | "body": "{\n \"id\": \"string\"\n}" 558 | } 559 | ] 560 | }, 561 | { 562 | "name": "1.3 Create Video Container", 563 | "event": [ 564 | { 565 | "listen": "test", 566 | "script": { 567 | "exec": [ 568 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 569 | " var jsonData = pm.response.json();", 570 | " ", 571 | " if (jsonData.id)", 572 | " pm.collectionVariables.set(\"container_id\", jsonData.id);", 573 | "}" 574 | ], 575 | "type": "text/javascript", 576 | "packages": {} 577 | } 578 | } 579 | ], 580 | "request": { 581 | "method": "POST", 582 | "header": [], 583 | "url": { 584 | "raw": "{{api_host}}/me/threads?text=This is a video.&media_type=VIDEO&video_url={{video_url}}&alt_text=An example video.", 585 | "host": [ 586 | "{{api_host}}" 587 | ], 588 | "path": [ 589 | "me", 590 | "threads" 591 | ], 592 | "query": [ 593 | { 594 | "key": "text", 595 | "value": "This is a video.", 596 | "description": "The text associated with the post. This is optional on video posts." 597 | }, 598 | { 599 | "key": "media_type", 600 | "value": "VIDEO", 601 | "description": "Indicates the current media type. Set to VIDEO. Note: Type CAROUSEL is not available for single thread posts." 602 | }, 603 | { 604 | "key": "video_url", 605 | "value": "{{video_url}}", 606 | "description": "(For videos only.) Path to the video. We will cURL your video using the URL provided so it must be on a public server." 607 | }, 608 | { 609 | "key": "alt_text", 610 | "value": "An example video.", 611 | "description": "The accessibility label or alt text for the video." 612 | }, 613 | { 614 | "key": "reply_control", 615 | "value": "everyone", 616 | "description": "Can be used to specify who can reply to a post.", 617 | "disabled": true 618 | }, 619 | { 620 | "key": "reply_to_id", 621 | "value": "", 622 | "description": "Required if replying to a specific post.", 623 | "disabled": true 624 | } 625 | ] 626 | }, 627 | "description": "Use the `POST` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/posts#step-1--create-a-threads-media-container) to create a video container." 628 | }, 629 | "response": [ 630 | { 631 | "name": "1.3 Create Video Container", 632 | "originalRequest": { 633 | "method": "POST", 634 | "header": [], 635 | "url": { 636 | "raw": "{{api_host}}/me/threads?text=This is a video.&media_type=VIDEO&video_url={{video_url}}&alt_text=An example video.", 637 | "host": [ 638 | "{{api_host}}" 639 | ], 640 | "path": [ 641 | "me", 642 | "threads" 643 | ], 644 | "query": [ 645 | { 646 | "key": "text", 647 | "value": "This is a video.", 648 | "description": "The text associated with the post. This is optional on video posts." 649 | }, 650 | { 651 | "key": "media_type", 652 | "value": "VIDEO", 653 | "description": "Indicates the current media type. Set to VIDEO. Note: Type CAROUSEL is not available for single thread posts." 654 | }, 655 | { 656 | "key": "video_url", 657 | "value": "{{video_url}}", 658 | "description": "(For videos only.) Path to the video. We will cURL your video using the URL provided so it must be on a public server." 659 | }, 660 | { 661 | "key": "alt_text", 662 | "value": "An example video.", 663 | "description": "The accessibility label or alt text for the video." 664 | }, 665 | { 666 | "key": "reply_control", 667 | "value": "everyone", 668 | "description": "Can be used to specify who can reply to a post.", 669 | "disabled": true 670 | }, 671 | { 672 | "key": "reply_to_id", 673 | "value": null, 674 | "description": "Required if replying to a specific post.", 675 | "disabled": true 676 | } 677 | ] 678 | } 679 | }, 680 | "_postman_previewlanguage": "json", 681 | "header": [ 682 | { 683 | "key": "Content-Type", 684 | "value": "application/json", 685 | "description": "", 686 | "type": "text" 687 | } 688 | ], 689 | "cookie": [], 690 | "body": "{\n \"id\": \"string\"\n}" 691 | } 692 | ] 693 | }, 694 | { 695 | "name": "2. Publish Threads Post", 696 | "event": [ 697 | { 698 | "listen": "test", 699 | "script": { 700 | "exec": [ 701 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 702 | " var jsonData = pm.response.json();", 703 | " ", 704 | " if (jsonData.id)", 705 | " pm.collectionVariables.set(\"thread_id\", jsonData.id);", 706 | "}" 707 | ], 708 | "type": "text/javascript", 709 | "packages": {} 710 | } 711 | } 712 | ], 713 | "request": { 714 | "method": "POST", 715 | "header": [], 716 | "url": { 717 | "raw": "{{api_host}}/me/threads_publish?creation_id={{container_id}}", 718 | "host": [ 719 | "{{api_host}}" 720 | ], 721 | "path": [ 722 | "me", 723 | "threads_publish" 724 | ], 725 | "query": [ 726 | { 727 | "key": "creation_id", 728 | "value": "{{container_id}}", 729 | "description": "Identifier of the Threads media container created from the /threads endpoint." 730 | } 731 | ] 732 | }, 733 | "description": "Use the `POST` /{threads-user-id}/threads_publish [endpoint](https://developers.facebook.com/docs/threads/posts#step-2--publish-a-threads-media-container) to publish the container ID returned in the previous step." 734 | }, 735 | "response": [ 736 | { 737 | "name": "2. Publish Threads Post", 738 | "originalRequest": { 739 | "method": "POST", 740 | "header": [], 741 | "url": { 742 | "raw": "{{api_host}}/me/threads_publish?creation_id={{container_id}}", 743 | "host": [ 744 | "{{api_host}}" 745 | ], 746 | "path": [ 747 | "me", 748 | "threads_publish" 749 | ], 750 | "query": [ 751 | { 752 | "key": "creation_id", 753 | "value": "{{container_id}}", 754 | "description": "Identifier of the Threads media container created from the /threads endpoint." 755 | } 756 | ] 757 | } 758 | }, 759 | "_postman_previewlanguage": "json", 760 | "header": [ 761 | { 762 | "key": "Content-Type", 763 | "value": "application/json", 764 | "description": "", 765 | "type": "text" 766 | } 767 | ], 768 | "cookie": [], 769 | "body": "{\n \"id\": \"string\"\n}" 770 | } 771 | ] 772 | } 773 | ], 774 | "description": "This folder will enable you to:\n\n1. Create a Media Container of text/image/video type. The API will return a Media Container ID which will be used in the second step.\n2. Publish a single Threads post." 775 | }, 776 | { 777 | "name": "Carousel Threads Posts", 778 | "item": [ 779 | { 780 | "name": "1.1 Create an Image Item Container", 781 | "event": [ 782 | { 783 | "listen": "test", 784 | "script": { 785 | "exec": [ 786 | "if (pm.collectionVariables.get(\"auto_save_variables\")) {", 787 | " var jsonData = pm.response.json();", 788 | "", 789 | " if (jsonData.id) {", 790 | " var ids = pm.collectionVariables.get(\"carousel_children_ids\");", 791 | " ", 792 | " let newIds = ids", 793 | " ? `${ids},${jsonData.id}`", 794 | " : jsonData.id;", 795 | " ", 796 | " pm.collectionVariables.set(\"carousel_children_ids\", newIds);", 797 | "", 798 | " pm.collectionVariables.set(\"container_id\", jsonData.id);", 799 | " }", 800 | "}" 801 | ], 802 | "type": "text/javascript", 803 | "packages": {} 804 | } 805 | } 806 | ], 807 | "request": { 808 | "method": "POST", 809 | "header": [], 810 | "url": { 811 | "raw": "{{api_host}}/me/threads?media_type=IMAGE&image_url={{image_url}}&is_carousel_item=true&alt_text=An example image.", 812 | "host": [ 813 | "{{api_host}}" 814 | ], 815 | "path": [ 816 | "me", 817 | "threads" 818 | ], 819 | "query": [ 820 | { 821 | "key": "media_type", 822 | "value": "IMAGE", 823 | "description": "Set to IMAGE or VIDEO. Indicates media is an image or a video." 824 | }, 825 | { 826 | "key": "image_url", 827 | "value": "{{image_url}}", 828 | "description": "(For images only.) The path to the image. We will cURL your image using the passed in URL so it must be on a public server." 829 | }, 830 | { 831 | "key": "is_carousel_item", 832 | "value": "true", 833 | "description": "Set to true. Indicates the image or video will appear in a carousel." 834 | }, 835 | { 836 | "key": "alt_text", 837 | "value": "An example image.", 838 | "description": "The accessibility label or alt text for the image." 839 | } 840 | ] 841 | }, 842 | "description": "Use the `POST` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/posts#step-1--create-an-item-container) to create an image container for the image that will appear in a carousel." 843 | }, 844 | "response": [ 845 | { 846 | "name": "1.1 Create an Image Item Container", 847 | "originalRequest": { 848 | "method": "POST", 849 | "header": [], 850 | "url": { 851 | "raw": "{{api_host}}/me/threads?media_type=IMAGE&image_url={{image_url}}&is_carousel_item=true&alt_text=An example image.", 852 | "host": [ 853 | "{{api_host}}" 854 | ], 855 | "path": [ 856 | "me", 857 | "threads" 858 | ], 859 | "query": [ 860 | { 861 | "key": "media_type", 862 | "value": "IMAGE", 863 | "description": "Set to IMAGE or VIDEO. Indicates media is an image or a video." 864 | }, 865 | { 866 | "key": "image_url", 867 | "value": "{{image_url}}", 868 | "description": "(For images only.) The path to the image. We will cURL your image using the passed in URL so it must be on a public server." 869 | }, 870 | { 871 | "key": "is_carousel_item", 872 | "value": "true", 873 | "description": "Set to true. Indicates the image or video will appear in a carousel." 874 | }, 875 | { 876 | "key": "alt_text", 877 | "value": "An example image.", 878 | "description": "The accessibility label or alt text for the image." 879 | } 880 | ] 881 | } 882 | }, 883 | "_postman_previewlanguage": "json", 884 | "header": [ 885 | { 886 | "key": "Content-Type", 887 | "value": "application/json", 888 | "description": "", 889 | "type": "text" 890 | } 891 | ], 892 | "cookie": [], 893 | "body": "{\n \"id\": \"string\"\n}" 894 | } 895 | ] 896 | }, 897 | { 898 | "name": "1.2 Create a Video Item Container", 899 | "event": [ 900 | { 901 | "listen": "test", 902 | "script": { 903 | "exec": [ 904 | "if (pm.collectionVariables.get(\"auto_save_variables\")) {", 905 | " var jsonData = pm.response.json();", 906 | "", 907 | " if (jsonData.id) {", 908 | " var ids = pm.collectionVariables.get(\"carousel_children_ids\");", 909 | " ", 910 | " let newIds = ids", 911 | " ? `${ids},${jsonData.id}`", 912 | " : jsonData.id;", 913 | " ", 914 | " pm.collectionVariables.set(\"carousel_children_ids\", newIds);", 915 | "", 916 | " pm.collectionVariables.set(\"container_id\", jsonData.id);", 917 | " }", 918 | "}" 919 | ], 920 | "type": "text/javascript", 921 | "packages": {} 922 | } 923 | } 924 | ], 925 | "request": { 926 | "method": "POST", 927 | "header": [], 928 | "url": { 929 | "raw": "{{api_host}}/me/threads?media_type=VIDEO&video_url={{video_url}}&is_carousel_item=true&alt_text=An example video.", 930 | "host": [ 931 | "{{api_host}}" 932 | ], 933 | "path": [ 934 | "me", 935 | "threads" 936 | ], 937 | "query": [ 938 | { 939 | "key": "media_type", 940 | "value": "VIDEO", 941 | "description": "Set to IMAGE or VIDEO. Indicates media is an image or a video." 942 | }, 943 | { 944 | "key": "video_url", 945 | "value": "{{video_url}}", 946 | "description": "(For videos only.) Path to the video. We will cURL your video using the passed in URL so it must be on a public server." 947 | }, 948 | { 949 | "key": "is_carousel_item", 950 | "value": "true", 951 | "description": "Set to true. Indicates the image or video will appear in a carousel." 952 | }, 953 | { 954 | "key": "alt_text", 955 | "value": "An example video.", 956 | "description": "The accessibility label or alt text for the video." 957 | } 958 | ] 959 | }, 960 | "description": "Use the `POST` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/posts#step-1--create-an-item-container) to create a video container for the video that will appear in a carousel." 961 | }, 962 | "response": [ 963 | { 964 | "name": "1.2 Create a Video Item Container", 965 | "originalRequest": { 966 | "method": "POST", 967 | "header": [], 968 | "url": { 969 | "raw": "{{api_host}}/me/threads?media_type=VIDEO&video_url={{video_url}}&is_carousel_item=true&alt_text=An example video.", 970 | "host": [ 971 | "{{api_host}}" 972 | ], 973 | "path": [ 974 | "me", 975 | "threads" 976 | ], 977 | "query": [ 978 | { 979 | "key": "media_type", 980 | "value": "VIDEO", 981 | "description": "Set to IMAGE or VIDEO. Indicates media is an image or a video." 982 | }, 983 | { 984 | "key": "video_url", 985 | "value": "{{video_url}}", 986 | "description": "(For videos only.) Path to the video. We will cURL your video using the passed in URL so it must be on a public server." 987 | }, 988 | { 989 | "key": "is_carousel_item", 990 | "value": "true", 991 | "description": "Set to true. Indicates the image or video will appear in a carousel." 992 | }, 993 | { 994 | "key": "alt_text", 995 | "value": "An example video.", 996 | "description": "The accessibility label or alt text for the video." 997 | } 998 | ] 999 | } 1000 | }, 1001 | "_postman_previewlanguage": "json", 1002 | "header": [ 1003 | { 1004 | "key": "Content-Type", 1005 | "value": "application/json", 1006 | "description": "", 1007 | "type": "text" 1008 | } 1009 | ], 1010 | "cookie": [], 1011 | "body": "{\n \"id\": \"string\"\n}" 1012 | } 1013 | ] 1014 | }, 1015 | { 1016 | "name": "2. Create a Carousel Container", 1017 | "event": [ 1018 | { 1019 | "listen": "test", 1020 | "script": { 1021 | "exec": [ 1022 | "if (pm.collectionVariables.get(\"auto_save_variables\")) {", 1023 | " var jsonData = pm.response.json();", 1024 | "", 1025 | " if (jsonData.id) {", 1026 | " // Clear the value", 1027 | " pm.collectionVariables.set(\"carousel_children_ids\", \"\");", 1028 | "", 1029 | " pm.collectionVariables.set(\"container_id\", jsonData.id);", 1030 | " }", 1031 | "}" 1032 | ], 1033 | "type": "text/javascript", 1034 | "packages": {} 1035 | } 1036 | } 1037 | ], 1038 | "request": { 1039 | "method": "POST", 1040 | "header": [], 1041 | "url": { 1042 | "raw": "{{api_host}}/me/threads?media_type=CAROUSEL&children={{carousel_children_ids}}&text=This is a carousel", 1043 | "host": [ 1044 | "{{api_host}}" 1045 | ], 1046 | "path": [ 1047 | "me", 1048 | "threads" 1049 | ], 1050 | "query": [ 1051 | { 1052 | "key": "media_type", 1053 | "value": "CAROUSEL", 1054 | "description": "The text associated with the post. This is optional on carousel posts." 1055 | }, 1056 | { 1057 | "key": "children", 1058 | "value": "{{carousel_children_ids}}", 1059 | "description": "A comma-separated list of up to 20 container IDs of each image and/or video that should appear in the published carousel. Carousels can have at least 2 and up to 20 total images or videos or a mix of the two." 1060 | }, 1061 | { 1062 | "key": "text", 1063 | "value": "This is a carousel", 1064 | "description": "The text associated with the post. This is optional on carousel posts." 1065 | } 1066 | ] 1067 | }, 1068 | "description": "Use the `POST` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/posts#step-2--create-a-carousel-container) to create a carousel container." 1069 | }, 1070 | "response": [ 1071 | { 1072 | "name": "2. Create a Carousel Container", 1073 | "originalRequest": { 1074 | "method": "POST", 1075 | "header": [], 1076 | "url": { 1077 | "raw": "{{api_host}}/me/threads?media_type=CAROUSEL&children={{carousel_children_ids}}&text=This is a carousel", 1078 | "host": [ 1079 | "{{api_host}}" 1080 | ], 1081 | "path": [ 1082 | "me", 1083 | "threads" 1084 | ], 1085 | "query": [ 1086 | { 1087 | "key": "media_type", 1088 | "value": "CAROUSEL", 1089 | "description": "The text associated with the post. This is optional on carousel posts." 1090 | }, 1091 | { 1092 | "key": "children", 1093 | "value": "{{carousel_children_ids}}", 1094 | "description": "A comma-separated list of up to 10 container IDs of each image and/or video that should appear in the published carousel. Carousels can have at least 2 and up to 10 total images or videos or a mix of the two." 1095 | }, 1096 | { 1097 | "key": "text", 1098 | "value": "This is a carousel", 1099 | "description": "(Optional.) The text associated with the post." 1100 | } 1101 | ] 1102 | } 1103 | }, 1104 | "_postman_previewlanguage": "json", 1105 | "header": [ 1106 | { 1107 | "key": "Content-Type", 1108 | "value": "application/json", 1109 | "description": "", 1110 | "type": "text" 1111 | } 1112 | ], 1113 | "cookie": [], 1114 | "body": "{\n \"id\": \"string\"\n}" 1115 | } 1116 | ] 1117 | }, 1118 | { 1119 | "name": "3. Publish a Carousel Container", 1120 | "event": [ 1121 | { 1122 | "listen": "test", 1123 | "script": { 1124 | "exec": [ 1125 | "if (pm.collectionVariables.get(\"auto_save_variables\")) {", 1126 | " var jsonData = pm.response.json();", 1127 | "", 1128 | " if (jsonData.id)", 1129 | " pm.collectionVariables.set(\"thread_id\", jsonData.id);", 1130 | "}" 1131 | ], 1132 | "type": "text/javascript", 1133 | "packages": {} 1134 | } 1135 | } 1136 | ], 1137 | "request": { 1138 | "method": "POST", 1139 | "header": [], 1140 | "url": { 1141 | "raw": "{{api_host}}/me/threads_publish?creation_id={{container_id}}", 1142 | "host": [ 1143 | "{{api_host}}" 1144 | ], 1145 | "path": [ 1146 | "me", 1147 | "threads_publish" 1148 | ], 1149 | "query": [ 1150 | { 1151 | "key": "creation_id", 1152 | "value": "{{container_id}}", 1153 | "description": "The carousel container ID." 1154 | } 1155 | ] 1156 | }, 1157 | "description": "Use the `POST` /{threads-user-id}/threads_publish [endpoint](https://developers.facebook.com/docs/threads/posts#step-3--publish-the-carousel-container) to publish a carousel post." 1158 | }, 1159 | "response": [ 1160 | { 1161 | "name": "3. Publish a Carousel Container", 1162 | "originalRequest": { 1163 | "method": "POST", 1164 | "header": [], 1165 | "url": { 1166 | "raw": "{{api_host}}/me/threads_publish?creation_id={{container_id}}", 1167 | "host": [ 1168 | "{{api_host}}" 1169 | ], 1170 | "path": [ 1171 | "me", 1172 | "threads_publish" 1173 | ], 1174 | "query": [ 1175 | { 1176 | "key": "creation_id", 1177 | "value": "{{container_id}}", 1178 | "description": "The carousel container ID." 1179 | } 1180 | ] 1181 | } 1182 | }, 1183 | "_postman_previewlanguage": "json", 1184 | "header": [ 1185 | { 1186 | "key": "Content-Type", 1187 | "value": "application/json", 1188 | "description": "", 1189 | "type": "text" 1190 | } 1191 | ], 1192 | "cookie": [], 1193 | "body": "{\n \"id\": \"string\"\n}" 1194 | } 1195 | ] 1196 | } 1197 | ], 1198 | "description": "This folder will enable you to:\n\n1. Create an Item Container for each image or video that will appear in a Carousel.\n \n2. Create a Carousel Container.\n \n3. Publish a Carousel post." 1199 | }, 1200 | { 1201 | "name": "Quote Threads Posts", 1202 | "item": [ 1203 | { 1204 | "name": "1. Create Media Container", 1205 | "event": [ 1206 | { 1207 | "listen": "test", 1208 | "script": { 1209 | "exec": [ 1210 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 1211 | " var jsonData = pm.response.json();", 1212 | " ", 1213 | " if (jsonData.id)", 1214 | " pm.collectionVariables.set(\"container_id\", jsonData.id);", 1215 | "}" 1216 | ], 1217 | "type": "text/javascript", 1218 | "packages": {} 1219 | } 1220 | } 1221 | ], 1222 | "request": { 1223 | "method": "POST", 1224 | "header": [], 1225 | "url": { 1226 | "raw": "{{api_host}}/me/threads?text=This is a quoted post.&media_type=TEXT"e_post_id={{quote_post_id}}", 1227 | "host": [ 1228 | "{{api_host}}" 1229 | ], 1230 | "path": [ 1231 | "me", 1232 | "threads" 1233 | ], 1234 | "query": [ 1235 | { 1236 | "key": "text", 1237 | "value": "This is a quoted post.", 1238 | "description": "The text associated with the post." 1239 | }, 1240 | { 1241 | "key": "media_type", 1242 | "value": "TEXT", 1243 | "description": "Indicates the current media type." 1244 | }, 1245 | { 1246 | "key": "quote_post_id", 1247 | "value": "{{quote_post_id}}", 1248 | "description": "The ID of the Threads post that is being quoted." 1249 | }, 1250 | { 1251 | "key": "reply_control", 1252 | "value": "everyone", 1253 | "description": "Can be used to specify who can reply to a post.", 1254 | "disabled": true 1255 | }, 1256 | { 1257 | "key": "reply_to_id", 1258 | "value": "", 1259 | "description": "Required if replying to a specific post.", 1260 | "disabled": true 1261 | }, 1262 | { 1263 | "key": "alt_text", 1264 | "value": "This is alt text.", 1265 | "description": "The accessibility text label or description for an image or video in a Threads post.", 1266 | "disabled": true 1267 | } 1268 | ] 1269 | }, 1270 | "description": "Use the `POST` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/posts#step-1--create-a-threads-media-container) to create a text container." 1271 | }, 1272 | "response": [ 1273 | { 1274 | "name": "1. Create Media Container", 1275 | "originalRequest": { 1276 | "method": "POST", 1277 | "header": [], 1278 | "url": { 1279 | "raw": "https://graph.threads.net/me/threads?text=This is a quoted post.&media_type=TEXT"e_post_id={{quote_post_id}}", 1280 | "protocol": "https", 1281 | "host": [ 1282 | "graph", 1283 | "threads", 1284 | "net" 1285 | ], 1286 | "path": [ 1287 | "me", 1288 | "threads" 1289 | ], 1290 | "query": [ 1291 | { 1292 | "key": "text", 1293 | "value": "This is a quoted post.", 1294 | "description": "The text associated with the post." 1295 | }, 1296 | { 1297 | "key": "media_type", 1298 | "value": "TEXT", 1299 | "description": "Indicates the current media type." 1300 | }, 1301 | { 1302 | "key": "quote_post_id", 1303 | "value": "{{quote_post_id}}", 1304 | "description": "The ID of the Threads post that is being quoted." 1305 | }, 1306 | { 1307 | "key": "reply_control", 1308 | "value": "everyone", 1309 | "description": "Can be used to specify who can reply to a post.", 1310 | "disabled": true 1311 | }, 1312 | { 1313 | "key": "reply_to_id", 1314 | "value": null, 1315 | "description": "Required if replying to a specific post.", 1316 | "disabled": true 1317 | }, 1318 | { 1319 | "key": "alt_text", 1320 | "value": "This is alt text.", 1321 | "description": "The accessibility text label or description for an image or video in a Threads post.", 1322 | "disabled": true 1323 | } 1324 | ] 1325 | } 1326 | }, 1327 | "_postman_previewlanguage": "json", 1328 | "header": [ 1329 | { 1330 | "key": "Content-Type", 1331 | "value": "application/json", 1332 | "description": "", 1333 | "type": "text" 1334 | } 1335 | ], 1336 | "cookie": [], 1337 | "body": "{\n \"id\": \"string\"\n}" 1338 | } 1339 | ] 1340 | }, 1341 | { 1342 | "name": "2. Publish Threads Quote Post", 1343 | "event": [ 1344 | { 1345 | "listen": "test", 1346 | "script": { 1347 | "exec": [ 1348 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 1349 | " var jsonData = pm.response.json();", 1350 | " ", 1351 | " if (jsonData.id)", 1352 | " pm.collectionVariables.set(\"thread_id\", jsonData.id);", 1353 | "}" 1354 | ], 1355 | "type": "text/javascript", 1356 | "packages": {} 1357 | } 1358 | } 1359 | ], 1360 | "request": { 1361 | "method": "POST", 1362 | "header": [], 1363 | "url": { 1364 | "raw": "{{api_host}}/me/threads_publish?creation_id={{container_id}}", 1365 | "host": [ 1366 | "{{api_host}}" 1367 | ], 1368 | "path": [ 1369 | "me", 1370 | "threads_publish" 1371 | ], 1372 | "query": [ 1373 | { 1374 | "key": "creation_id", 1375 | "value": "{{container_id}}", 1376 | "description": "Identifier of the Threads media container created from the /threads endpoint." 1377 | } 1378 | ] 1379 | }, 1380 | "description": "Use the `POST` /{threads-user-id}/threads_publish [endpoint](https://developers.facebook.com/docs/threads/posts#step-2--publish-a-threads-media-container) to publish the container ID returned in the previous step." 1381 | }, 1382 | "response": [ 1383 | { 1384 | "name": "2. Publish Threads Post", 1385 | "originalRequest": { 1386 | "method": "POST", 1387 | "header": [], 1388 | "url": { 1389 | "raw": "{{api_host}}/me/threads_publish?creation_id={{container_id}}", 1390 | "host": [ 1391 | "{{api_host}}" 1392 | ], 1393 | "path": [ 1394 | "me", 1395 | "threads_publish" 1396 | ], 1397 | "query": [ 1398 | { 1399 | "key": "creation_id", 1400 | "value": "{{container_id}}", 1401 | "description": "Identifier of the Threads media container created from the /threads endpoint." 1402 | } 1403 | ] 1404 | } 1405 | }, 1406 | "_postman_previewlanguage": "json", 1407 | "header": [ 1408 | { 1409 | "key": "Content-Type", 1410 | "value": "application/json", 1411 | "description": "", 1412 | "type": "text" 1413 | } 1414 | ], 1415 | "cookie": [], 1416 | "body": "{\n \"id\": \"string\"\n}" 1417 | } 1418 | ] 1419 | } 1420 | ], 1421 | "description": "This folder will enable you to:\n\n1. Create a Media Container. The API will return a Media Container ID which will be used in the second step.\n \n2. Publish a single Quote post." 1422 | }, 1423 | { 1424 | "name": "Repost Threads Posts", 1425 | "item": [ 1426 | { 1427 | "name": "Repost Threads Post", 1428 | "event": [ 1429 | { 1430 | "listen": "test", 1431 | "script": { 1432 | "exec": [ 1433 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 1434 | " var jsonData = pm.response.json();", 1435 | " ", 1436 | " if (jsonData.id)", 1437 | " pm.collectionVariables.set(\"thread_id\", jsonData.id);", 1438 | "}" 1439 | ], 1440 | "type": "text/javascript", 1441 | "packages": {} 1442 | } 1443 | } 1444 | ], 1445 | "request": { 1446 | "method": "POST", 1447 | "header": [], 1448 | "url": { 1449 | "raw": "{{api_host}}/{{thread_id}}/repost", 1450 | "host": [ 1451 | "{{api_host}}" 1452 | ], 1453 | "path": [ 1454 | "{{thread_id}}", 1455 | "repost" 1456 | ] 1457 | }, 1458 | "description": "Use the `POST` /{threads-user-id}/threads_publish [endpoint](https://developers.facebook.com/docs/threads/posts#step-2--publish-a-threads-media-container) to publish the container ID returned in the previous step." 1459 | }, 1460 | "response": [ 1461 | { 1462 | "name": "Repost Threads Post", 1463 | "originalRequest": { 1464 | "method": "POST", 1465 | "header": [], 1466 | "url": { 1467 | "raw": "{{api_host}}/{{thread_id}}/repost", 1468 | "host": [ 1469 | "{{api_host}}" 1470 | ], 1471 | "path": [ 1472 | "{{thread_id}}", 1473 | "repost" 1474 | ] 1475 | } 1476 | }, 1477 | "_postman_previewlanguage": "json", 1478 | "header": [ 1479 | { 1480 | "key": "Content-Type", 1481 | "value": "application/json", 1482 | "description": "", 1483 | "type": "text" 1484 | } 1485 | ], 1486 | "cookie": [], 1487 | "body": "{\n \"id\": \"string\"\n}" 1488 | } 1489 | ] 1490 | } 1491 | ], 1492 | "description": "This folder will enable you to repost an original Threads post." 1493 | } 1494 | ], 1495 | "description": "This folder enables you to use the Threads API to publish single image, video, text or carousel posts." 1496 | }, 1497 | { 1498 | "name": "Read And Manage Threads", 1499 | "item": [ 1500 | { 1501 | "name": "Retrieve Threads Profiles", 1502 | "item": [ 1503 | { 1504 | "name": "Get Threads User's Profile Information", 1505 | "event": [ 1506 | { 1507 | "listen": "test", 1508 | "script": { 1509 | "exec": [ 1510 | "", 1511 | "if (pm.collectionVariables.get(\"auto_save_variables\") === \"true\") {", 1512 | " var jsonData = pm.response.json();", 1513 | " ", 1514 | " if (jsonData.id)", 1515 | " pm.collectionVariables.set(\"user_id\", jsonData.id);", 1516 | "}" 1517 | ], 1518 | "type": "text/javascript", 1519 | "packages": {} 1520 | } 1521 | } 1522 | ], 1523 | "request": { 1524 | "method": "GET", 1525 | "header": [], 1526 | "url": { 1527 | "raw": "{{api_host}}/me?fields={{fields_profile}}", 1528 | "host": [ 1529 | "{{api_host}}" 1530 | ], 1531 | "path": [ 1532 | "me" 1533 | ], 1534 | "query": [ 1535 | { 1536 | "key": "fields", 1537 | "value": "{{fields_profile}}", 1538 | "description": "A comma-separated list of fields for a user on Threads." 1539 | } 1540 | ] 1541 | }, 1542 | "description": "Use the `GET` /{threads-user-id}?fields=id,username,... [endpoint](https://developers.facebook.com/docs/threads/threads-profiles#retrieve-a-threads-user-s-profile-information) to return profile information about a Threads user." 1543 | }, 1544 | "response": [ 1545 | { 1546 | "name": "Get Threads User's Profile Information", 1547 | "originalRequest": { 1548 | "method": "GET", 1549 | "header": [], 1550 | "url": { 1551 | "raw": "https://graph.threads.net/me?fields=id,username,name,threads_profile_picture_url,threads_biography", 1552 | "protocol": "https", 1553 | "host": [ 1554 | "graph", 1555 | "threads", 1556 | "net" 1557 | ], 1558 | "path": [ 1559 | "me" 1560 | ], 1561 | "query": [ 1562 | { 1563 | "key": "fields", 1564 | "value": "id,username,name,threads_profile_picture_url,threads_biography", 1565 | "description": "A comma-separated list of fields for a user on Threads." 1566 | } 1567 | ] 1568 | } 1569 | }, 1570 | "_postman_previewlanguage": "json", 1571 | "header": [ 1572 | { 1573 | "key": "Content-Type", 1574 | "value": "application/json", 1575 | "description": "", 1576 | "type": "text" 1577 | } 1578 | ], 1579 | "cookie": [], 1580 | "body": "{\n \"id\": \"string\",\n \"username\": \"string\",\n \"name\": \"string\",\n \"threads_profile_picture_url\": \"string\",\n \"threads_biography\": \"string\"\n}" 1581 | } 1582 | ] 1583 | } 1584 | ], 1585 | "description": "This folder will enable you to get profile information about a Threads user." 1586 | }, 1587 | { 1588 | "name": "Retrieve Threads Media Objects", 1589 | "item": [ 1590 | { 1591 | "name": "Get Threads Post Details", 1592 | "request": { 1593 | "method": "GET", 1594 | "header": [], 1595 | "url": { 1596 | "raw": "{{api_host}}/{{thread_id}}?fields={{fields_threads}}", 1597 | "host": [ 1598 | "{{api_host}}" 1599 | ], 1600 | "path": [ 1601 | "{{thread_id}}" 1602 | ], 1603 | "query": [ 1604 | { 1605 | "key": "fields", 1606 | "value": "{{fields_threads}}", 1607 | "description": "A comma-separated list of fields for a media object on Threads." 1608 | } 1609 | ] 1610 | }, 1611 | "description": "Use the `GET` /{threads-media-id} [endpoint](https://developers.facebook.com/docs/threads/threads-media#retrieve-a-single-threads-media-object) to return an individual Threads media object." 1612 | }, 1613 | "response": [ 1614 | { 1615 | "name": "Get Threads Post Details", 1616 | "originalRequest": { 1617 | "method": "GET", 1618 | "header": [], 1619 | "url": { 1620 | "raw": "https://graph.threads.net/?fields=id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,has_replies,alt_text,link_attachment_url", 1621 | "protocol": "https", 1622 | "host": [ 1623 | "graph", 1624 | "threads", 1625 | "net" 1626 | ], 1627 | "path": [ 1628 | "" 1629 | ], 1630 | "query": [ 1631 | { 1632 | "key": "fields", 1633 | "value": "id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,has_replies,alt_text,link_attachment_url", 1634 | "description": "A comma-separated list of fields for a media object on Threads." 1635 | } 1636 | ] 1637 | } 1638 | }, 1639 | "_postman_previewlanguage": "json", 1640 | "header": [ 1641 | { 1642 | "key": "Content-Type", 1643 | "value": "application/json", 1644 | "description": "", 1645 | "type": "text" 1646 | } 1647 | ], 1648 | "cookie": [], 1649 | "body": "{\n \"id\": \"string\",\n \"media_product_type\": \"THREADS\",\n \"media_type\": \"TEXT_POST\",\n \"permalink\": \"string\",\n \"owner\": {\n \"id\": \"string\"\n },\n \"username\": \"string\",\n \"text\": \"This is a test post\",\n \"timestamp\": \"2024-09-12T23:17:39+0000\",\n \"shortcode\": \"string\",\n \"is_quote_post\": false,\n \"has_replies\": false,\n \"poll_attachment\": {\n \"option_a\": \"first\",\n \"option_b\": \"second\",\n \"option_c\": \"third\",\n \"option_d\": \"fourth\",\n \"option_a_votes_percentage\": 0.25,\n \"option_b_votes_percentage\": 0.25,\n \"option_c_votes_percentage\": 0.25,\n \"option_d_votes_percentage\": 0.25,\n \"expiration_timestamp\": \"2024-09-12T23:17:39+0000\"\n }\n}" 1650 | } 1651 | ] 1652 | }, 1653 | { 1654 | "name": "Get a List of All a User's Threads", 1655 | "request": { 1656 | "method": "GET", 1657 | "header": [], 1658 | "url": { 1659 | "raw": "{{api_host}}/me/threads?fields={{fields_threads}}&limit=50", 1660 | "host": [ 1661 | "{{api_host}}" 1662 | ], 1663 | "path": [ 1664 | "me", 1665 | "threads" 1666 | ], 1667 | "query": [ 1668 | { 1669 | "key": "fields", 1670 | "value": "{{fields_threads}}", 1671 | "description": "A comma-separated list of fields for media objects on Threads." 1672 | }, 1673 | { 1674 | "key": "since", 1675 | "value": "", 1676 | "description": "Query string parameter representing the start date for retrieval.", 1677 | "disabled": true 1678 | }, 1679 | { 1680 | "key": "until", 1681 | "value": "", 1682 | "description": "Query string parameter representing the end date for retrieval.", 1683 | "disabled": true 1684 | }, 1685 | { 1686 | "key": "limit", 1687 | "value": "50", 1688 | "description": "Query string parameter representing the maximum number of media objects or records to return." 1689 | }, 1690 | { 1691 | "key": "before", 1692 | "value": "", 1693 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 1694 | "disabled": true 1695 | }, 1696 | { 1697 | "key": "after", 1698 | "value": "", 1699 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 1700 | "disabled": true 1701 | } 1702 | ] 1703 | }, 1704 | "description": "Use the `GET` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/threads-media#retrieve-a-list-of-all-a-user-s-threads) to return a paginated list of all threads created by a user." 1705 | }, 1706 | "response": [ 1707 | { 1708 | "name": "Get a List of All a User's Threads", 1709 | "originalRequest": { 1710 | "method": "GET", 1711 | "header": [], 1712 | "url": { 1713 | "raw": "https://graph.threads.net/me/threads?fields=id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,has_replies,alt_text,link_attachment_url&limit=50", 1714 | "protocol": "https", 1715 | "host": [ 1716 | "graph", 1717 | "threads", 1718 | "net" 1719 | ], 1720 | "path": [ 1721 | "me", 1722 | "threads" 1723 | ], 1724 | "query": [ 1725 | { 1726 | "key": "fields", 1727 | "value": "id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,has_replies,alt_text,link_attachment_url", 1728 | "description": "A comma-separated list of fields for media objects on Threads." 1729 | }, 1730 | { 1731 | "key": "since", 1732 | "value": null, 1733 | "description": "Query string parameter representing the start date for retrieval.", 1734 | "disabled": true 1735 | }, 1736 | { 1737 | "key": "until", 1738 | "value": null, 1739 | "description": "Query string parameter representing the end date for retrieval.", 1740 | "disabled": true 1741 | }, 1742 | { 1743 | "key": "limit", 1744 | "value": "50", 1745 | "description": "Query string parameter representing the maximum number of media objects or records to return." 1746 | }, 1747 | { 1748 | "key": "before", 1749 | "value": null, 1750 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 1751 | "disabled": true 1752 | }, 1753 | { 1754 | "key": "after", 1755 | "value": null, 1756 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 1757 | "disabled": true 1758 | } 1759 | ] 1760 | } 1761 | }, 1762 | "_postman_previewlanguage": "json", 1763 | "header": [ 1764 | { 1765 | "key": "Content-Type", 1766 | "value": "application/json", 1767 | "description": "", 1768 | "type": "text" 1769 | } 1770 | ], 1771 | "cookie": [], 1772 | "body": "{\n \"data\": [\n {\n \"id\": \"string\",\n \"media_product_type\": \"THREADS\",\n \"media_type\": \"TEXT_POST\",\n \"permalink\": \"string\",\n \"owner\": {\n \"id\": \"string\"\n },\n \"username\": \"string\",\n \"text\": \"This is a test post\",\n \"timestamp\": \"2024-09-12T23:17:39+0000\",\n \"shortcode\": \"string\",\n \"is_quote_post\": false,\n \"has_replies\": false,\n \"poll_attachment\": {\n \"option_a\": \"first\",\n \"option_b\": \"second\",\n \"option_c\": \"third\",\n \"option_d\": \"fourth\",\n \"option_a_votes_percentage\": 0.25,\n \"option_b_votes_percentage\": 0.25,\n \"option_c_votes_percentage\": 0.25,\n \"option_d_votes_percentage\": 0.25,\n \"expiration_timestamp\": \"2024-09-12T23:17:39+0000\"\n }\n }\n ],\n \"paging\": {\n \"cursors\": {\n \"before\": \"string\",\n \"after\": \"string\"\n }\n }\n}" 1773 | } 1774 | ] 1775 | } 1776 | ], 1777 | "description": "This folder will enable you to use the Threads API to retrieve details about posts." 1778 | }, 1779 | { 1780 | "name": "Read Threads Insights", 1781 | "item": [ 1782 | { 1783 | "name": "Get Post Insights", 1784 | "request": { 1785 | "method": "GET", 1786 | "header": [], 1787 | "url": { 1788 | "raw": "{{api_host}}/{{thread_id}}/insights?metric={{metrics_post}}", 1789 | "host": [ 1790 | "{{api_host}}" 1791 | ], 1792 | "path": [ 1793 | "{{thread_id}}", 1794 | "insights" 1795 | ], 1796 | "query": [ 1797 | { 1798 | "key": "metric", 1799 | "value": "{{metrics_post}}", 1800 | "description": "A comma-separated list of metrics for a post on Threads." 1801 | } 1802 | ] 1803 | }, 1804 | "description": "Use the `GET` /{threads-media-id}/insights [endpoint](https://developers.facebook.com/docs/threads/insights#media-insights) to return the insights of a given Threads post." 1805 | }, 1806 | "response": [ 1807 | { 1808 | "name": "Get Post Insights", 1809 | "originalRequest": { 1810 | "method": "GET", 1811 | "header": [], 1812 | "url": { 1813 | "raw": "{{api_host}}/{{thread_id}}/insights?metric={{metrics_post}}", 1814 | "host": [ 1815 | "{{api_host}}" 1816 | ], 1817 | "path": [ 1818 | "{{thread_id}}", 1819 | "insights" 1820 | ], 1821 | "query": [ 1822 | { 1823 | "key": "metric", 1824 | "value": "{{metrics_post}}", 1825 | "description": "A comma-separated list of metrics for a post on Threads." 1826 | } 1827 | ] 1828 | } 1829 | }, 1830 | "_postman_previewlanguage": null, 1831 | "header": null, 1832 | "cookie": [], 1833 | "body": "{\n \"data\": [\n {\n \"name\": \"views\",\n \"period\": \"lifetime\",\n \"values\": [\n {\n \"value\": 0\n }\n ],\n \"title\": \"Views\",\n \"description\": \"The number of times your post was viewed. This metric is in development (https://www.facebook.com/business/help/metrics-labeling)\",\n \"id\": \"{thread_id}/insights/views/lifetime\"\n },\n {\n \"name\": \"likes\",\n \"period\": \"lifetime\",\n \"values\": [\n {\n \"value\": 0\n }\n ],\n \"title\": \"Likes\",\n \"description\": \"The number of likes on your post.\",\n \"id\": \"{thread_id}/insights/likes/lifetime\"\n },\n {\n \"name\": \"replies\",\n \"period\": \"lifetime\",\n \"values\": [\n {\n \"value\": 0\n }\n ],\n \"title\": \"Replies\",\n \"description\": \"The number of replies on your post.\",\n \"id\": \"{thread_id}/insights/thread_replies/lifetime\"\n },\n {\n \"name\": \"reposts\",\n \"period\": \"lifetime\",\n \"values\": [\n {\n \"value\": 0\n }\n ],\n \"title\": \"Reposts\",\n \"description\": \"The number of times your post was reposted.\",\n \"id\": \"{thread_id}/insights/reposts/lifetime\"\n },\n {\n \"name\": \"quotes\",\n \"period\": \"lifetime\",\n \"values\": [\n {\n \"value\": 0\n }\n ],\n \"title\": \"Quotes\",\n \"description\": \"The number of times your post was quoted.\",\n \"id\": \"{thread_id}/insights/quotes/lifetime\"\n }\n ]\n}" 1834 | } 1835 | ] 1836 | }, 1837 | { 1838 | "name": "Get Account Insights", 1839 | "request": { 1840 | "method": "GET", 1841 | "header": [], 1842 | "url": { 1843 | "raw": "{{api_host}}/me/threads_insights?metric={{metrics_account}}", 1844 | "host": [ 1845 | "{{api_host}}" 1846 | ], 1847 | "path": [ 1848 | "me", 1849 | "threads_insights" 1850 | ], 1851 | "query": [ 1852 | { 1853 | "key": "metric", 1854 | "value": "{{metrics_account}}", 1855 | "description": "A comma-separated list of metrics for an account on Threads." 1856 | }, 1857 | { 1858 | "key": "breakdown", 1859 | "value": "{{follower_demographics_breakdown}}", 1860 | "description": "Used for categorizing follower demographics. Must be one of the following values: country, city, age, or gender.", 1861 | "disabled": true 1862 | } 1863 | ] 1864 | }, 1865 | "description": "Use the `GET` /{threads-user-id}/threads_insights [endpoint](https://developers.facebook.com/docs/threads/insights#user-insights) to return the insights of a given Threads user." 1866 | }, 1867 | "response": [ 1868 | { 1869 | "name": "Get Account Insights", 1870 | "originalRequest": { 1871 | "method": "GET", 1872 | "header": [], 1873 | "url": { 1874 | "raw": "{{api_host}}/me/threads_insights?metric={{metrics_account}}", 1875 | "host": [ 1876 | "{{api_host}}" 1877 | ], 1878 | "path": [ 1879 | "me", 1880 | "threads_insights" 1881 | ], 1882 | "query": [ 1883 | { 1884 | "key": "metric", 1885 | "value": "{{metrics_account}}", 1886 | "description": "A comma-separated list of metrics for an account on Threads." 1887 | }, 1888 | { 1889 | "key": "breakdown", 1890 | "value": "{{follower_demographics_breakdown}}", 1891 | "description": "Used for categorizing follower demographics. Must be one of the following values: country, city, age, or gender.", 1892 | "disabled": true 1893 | } 1894 | ] 1895 | } 1896 | }, 1897 | "_postman_previewlanguage": null, 1898 | "header": null, 1899 | "cookie": [], 1900 | "body": "{\n \"data\": [\n {\n \"name\": \"views\",\n \"period\": \"day\",\n \"values\": [\n {\n \"value\": 0,\n \"end_time\": \"2024-09-16T07:00:00+0000\"\n },\n {\n \"value\": 0,\n \"end_time\": \"2024-09-17T07:00:00+0000\"\n }\n ],\n \"title\": \"views\",\n \"description\": \"The number of times your profile was viewed.\",\n \"id\": \"{user_id}/insights/views/day\"\n },\n {\n \"name\": \"likes\",\n \"period\": \"day\",\n \"title\": \"likes\",\n \"description\": \"The number of likes on your posts.\",\n \"total_value\": {\n \"value\": 0\n },\n \"id\": \"{user_id}/insights/likes/day\"\n },\n {\n \"name\": \"replies\",\n \"period\": \"day\",\n \"title\": \"replies\",\n \"description\": \"The number of replies on your posts.\",\n \"total_value\": {\n \"value\": 0\n },\n \"id\": \"{user_id}/insights/replies/day\"\n },\n {\n \"name\": \"reposts\",\n \"period\": \"day\",\n \"title\": \"reposts\",\n \"description\": \"The number of times your posts were reposted.\",\n \"total_value\": {\n \"value\": 0\n },\n \"id\": \"{user_id}/insights/reposts/day\"\n },\n {\n \"name\": \"quotes\",\n \"period\": \"day\",\n \"title\": \"quotes\",\n \"description\": \"The number of times your posts were quoted.\",\n \"total_value\": {\n \"value\": 0\n },\n \"id\": \"{user_id}/insights/quotes/day\"\n },\n {\n \"name\": \"followers_count\",\n \"period\": \"day\",\n \"title\": \"followers_count\",\n \"description\": \"This is your total number of followers on Threads.\",\n \"total_value\": {\n \"value\": 0\n },\n \"id\": \"{user_id}/insights/followers_count/day\"\n }\n ],\n \"paging\": {\n \"previous\": \"string\",\n \"next\": \"string\"\n }\n}" 1901 | } 1902 | ] 1903 | } 1904 | ], 1905 | "description": "This folder will enable you to use the Threads API to retrieve insights about posts and accounts." 1906 | }, 1907 | { 1908 | "name": "Read and Manage Threads Replies", 1909 | "item": [ 1910 | { 1911 | "name": "Get Threads Replies", 1912 | "request": { 1913 | "method": "GET", 1914 | "header": [], 1915 | "url": { 1916 | "raw": "{{api_host}}/{{thread_id}}/replies?fields={{fields_replies}}&reverse=false", 1917 | "host": [ 1918 | "{{api_host}}" 1919 | ], 1920 | "path": [ 1921 | "{{thread_id}}", 1922 | "replies" 1923 | ], 1924 | "query": [ 1925 | { 1926 | "key": "fields", 1927 | "value": "{{fields_replies}}", 1928 | "description": "A comma-separated list of fields for replies on Threads." 1929 | }, 1930 | { 1931 | "key": "reverse", 1932 | "value": "false", 1933 | "description": "Whether or not replies should be sorted in reverse chronological order. The default is true if not specified." 1934 | } 1935 | ] 1936 | }, 1937 | "description": "Use `GET` /replies [endpoint](https://developers.facebook.com/docs/threads/reply-management#replies) to fetch a paginated list of all top-level replies." 1938 | }, 1939 | "response": [ 1940 | { 1941 | "name": "Get Threads Replies", 1942 | "originalRequest": { 1943 | "method": "GET", 1944 | "header": [], 1945 | "url": { 1946 | "raw": "{{api_host}}/{{thread_id}}/replies?fields={{fields_replies}}&reverse=false", 1947 | "host": [ 1948 | "{{api_host}}" 1949 | ], 1950 | "path": [ 1951 | "{{thread_id}}", 1952 | "replies" 1953 | ], 1954 | "query": [ 1955 | { 1956 | "key": "fields", 1957 | "value": "{{fields_replies}}", 1958 | "description": "A comma-separated list of fields for replies on Threads." 1959 | }, 1960 | { 1961 | "key": "reverse", 1962 | "value": "false", 1963 | "description": "Whether or not replies should be sorted in reverse chronological order. The default is true if not specified." 1964 | } 1965 | ] 1966 | } 1967 | }, 1968 | "_postman_previewlanguage": "json", 1969 | "header": [ 1970 | { 1971 | "key": "Content-Type", 1972 | "value": "application/json", 1973 | "description": "", 1974 | "type": "text" 1975 | } 1976 | ], 1977 | "cookie": [], 1978 | "body": "{\n \"data\": [\n {\n \"id\": \"string\",\n \"text\": \"reply\",\n \"timestamp\": \"2024-09-17T20:54:47+0000\",\n \"media_product_type\": \"THREADS\",\n \"media_type\": \"TEXT_POST\",\n \"permalink\": \"string\",\n \"shortcode\": \"string\",\n \"username\": \"string\",\n \"is_quote_post\": false,\n \"has_replies\": false,\n \"is_reply\": true,\n \"is_reply_owned_by_me\": false,\n \"root_post\": {\n \"id\": \"string\"\n },\n \"replied_to\": {\n \"id\": \"string\"\n }\n }\n ],\n \"paging\": {\n \"cursors\": {\n \"before\": \"string\",\n \"after\": \"string\"\n }\n }\n}" 1979 | } 1980 | ] 1981 | }, 1982 | { 1983 | "name": "Get Threads Conversations", 1984 | "request": { 1985 | "method": "GET", 1986 | "header": [], 1987 | "url": { 1988 | "raw": "{{api_host}}/{{thread_id}}/conversation?fields={{fields_replies}}&reverse=false", 1989 | "host": [ 1990 | "{{api_host}}" 1991 | ], 1992 | "path": [ 1993 | "{{thread_id}}", 1994 | "conversation" 1995 | ], 1996 | "query": [ 1997 | { 1998 | "key": "fields", 1999 | "value": "{{fields_replies}}", 2000 | "description": "A comma-separated list of fields for replies on Threads." 2001 | }, 2002 | { 2003 | "key": "reverse", 2004 | "value": "false", 2005 | "description": "Whether or not replies should be sorted in reverse chronological order. The default is true if not specified." 2006 | } 2007 | ] 2008 | }, 2009 | "description": "Use `GET` /conversation [endpoint](https://developers.facebook.com/docs/threads/reply-management#conversations) to fetch a paginated and flattened list of all top-level and nested replies." 2010 | }, 2011 | "response": [ 2012 | { 2013 | "name": "Get Threads Conversations", 2014 | "originalRequest": { 2015 | "method": "GET", 2016 | "header": [], 2017 | "url": { 2018 | "raw": "{{api_host}}/{{thread_id}}/conversation?fields={{fields_replies}}&reverse=false", 2019 | "host": [ 2020 | "{{api_host}}" 2021 | ], 2022 | "path": [ 2023 | "{{thread_id}}", 2024 | "conversation" 2025 | ], 2026 | "query": [ 2027 | { 2028 | "key": "fields", 2029 | "value": "{{fields_replies}}", 2030 | "description": "A comma-separated list of fields for replies on Threads." 2031 | }, 2032 | { 2033 | "key": "reverse", 2034 | "value": "false", 2035 | "description": "Whether or not replies should be sorted in reverse chronological order. The default is true if not specified." 2036 | } 2037 | ] 2038 | } 2039 | }, 2040 | "_postman_previewlanguage": "json", 2041 | "header": [ 2042 | { 2043 | "key": "Content-Type", 2044 | "value": "application/json", 2045 | "description": "", 2046 | "type": "text" 2047 | } 2048 | ], 2049 | "cookie": [], 2050 | "body": "{\n \"data\": [\n {\n \"id\": \"string\",\n \"text\": \"reply\",\n \"timestamp\": \"2024-09-17T20:54:47+0000\",\n \"media_product_type\": \"THREADS\",\n \"media_type\": \"TEXT_POST\",\n \"permalink\": \"string\",\n \"shortcode\": \"string\",\n \"username\": \"string\",\n \"is_quote_post\": false,\n \"has_replies\": false,\n \"is_reply\": true,\n \"is_reply_owned_by_me\": false,\n \"root_post\": {\n \"id\": \"string\"\n },\n \"replied_to\": {\n \"id\": \"string\"\n }\n }\n ],\n \"paging\": {\n \"cursors\": {\n \"before\": \"string\",\n \"after\": \"string\"\n }\n }\n}" 2051 | } 2052 | ] 2053 | }, 2054 | { 2055 | "name": "Hide Replies", 2056 | "request": { 2057 | "method": "POST", 2058 | "header": [], 2059 | "url": { 2060 | "raw": "{{api_host}}/{{reply_thread_id}}/manage_reply?hide=true", 2061 | "host": [ 2062 | "{{api_host}}" 2063 | ], 2064 | "path": [ 2065 | "{{reply_thread_id}}", 2066 | "manage_reply" 2067 | ], 2068 | "query": [ 2069 | { 2070 | "key": "hide", 2071 | "value": "true", 2072 | "description": "Set to true to hide a reply and set to false to unhide a reply." 2073 | } 2074 | ] 2075 | }, 2076 | "description": "Use `POST` /manage_reply [endpoint](https://developers.facebook.com/docs/threads/reply-management#hide-replies) to hide/unhide any top-level replies." 2077 | }, 2078 | "response": [ 2079 | { 2080 | "name": "Hide Replies", 2081 | "originalRequest": { 2082 | "method": "POST", 2083 | "header": [], 2084 | "url": { 2085 | "raw": "{{api_host}}/{{reply_thread_id}}/manage_reply?hide=true", 2086 | "host": [ 2087 | "{{api_host}}" 2088 | ], 2089 | "path": [ 2090 | "{{reply_thread_id}}", 2091 | "manage_reply" 2092 | ], 2093 | "query": [ 2094 | { 2095 | "key": "hide", 2096 | "value": "true", 2097 | "description": "Set to true to hide a reply and set to false to unhide a reply." 2098 | } 2099 | ] 2100 | } 2101 | }, 2102 | "_postman_previewlanguage": "json", 2103 | "header": [ 2104 | { 2105 | "key": "Content-Type", 2106 | "value": "application/json", 2107 | "description": "", 2108 | "type": "text" 2109 | } 2110 | ], 2111 | "cookie": [], 2112 | "body": "{\n \"success\": true\n}" 2113 | } 2114 | ] 2115 | }, 2116 | { 2117 | "name": "Respond to Replies", 2118 | "request": { 2119 | "method": "POST", 2120 | "header": [], 2121 | "url": { 2122 | "raw": "{{api_host}}/me/threads?media_type=TEXT&text=Text&reply_to_id={{reply_thread_id}}", 2123 | "host": [ 2124 | "{{api_host}}" 2125 | ], 2126 | "path": [ 2127 | "me", 2128 | "threads" 2129 | ], 2130 | "query": [ 2131 | { 2132 | "key": "media_type", 2133 | "value": "TEXT", 2134 | "description": "Indicates the current media type." 2135 | }, 2136 | { 2137 | "key": "text", 2138 | "value": "Text", 2139 | "description": "The text associated with the post." 2140 | }, 2141 | { 2142 | "key": "reply_to_id", 2143 | "value": "{{reply_thread_id}}", 2144 | "description": "Required if replying to a specific post." 2145 | } 2146 | ] 2147 | }, 2148 | "description": "Use the [reply_to_id ](https://developers.facebook.com/docs/threads/reply-management#respond-to-replies) parameter to reply to a specific reply under the root post." 2149 | }, 2150 | "response": [ 2151 | { 2152 | "name": "Respond to Replies", 2153 | "originalRequest": { 2154 | "method": "POST", 2155 | "header": [], 2156 | "url": { 2157 | "raw": "{{api_host}}/me/threads?media_type=TEXT&text=Text&reply_to_id={{reply_thread_id}}", 2158 | "host": [ 2159 | "{{api_host}}" 2160 | ], 2161 | "path": [ 2162 | "me", 2163 | "threads" 2164 | ], 2165 | "query": [ 2166 | { 2167 | "key": "media_type", 2168 | "value": "TEXT", 2169 | "description": "Indicates the current media type." 2170 | }, 2171 | { 2172 | "key": "text", 2173 | "value": "Text", 2174 | "description": "The text associated with the post." 2175 | }, 2176 | { 2177 | "key": "reply_to_id", 2178 | "value": "{{reply_thread_id}}", 2179 | "description": "Required if replying to a specific post." 2180 | } 2181 | ] 2182 | } 2183 | }, 2184 | "_postman_previewlanguage": "json", 2185 | "header": [ 2186 | { 2187 | "key": "Content-Type", 2188 | "value": "application/json", 2189 | "description": "", 2190 | "type": "text" 2191 | } 2192 | ], 2193 | "cookie": [], 2194 | "body": "{\n \"id\": \"string\"\n}" 2195 | } 2196 | ] 2197 | }, 2198 | { 2199 | "name": "Control Who Can Reply", 2200 | "request": { 2201 | "method": "POST", 2202 | "header": [], 2203 | "url": { 2204 | "raw": "{{api_host}}/me/threads?media_type=TEXT&text=Text&reply_control={{reply_control}}", 2205 | "host": [ 2206 | "{{api_host}}" 2207 | ], 2208 | "path": [ 2209 | "me", 2210 | "threads" 2211 | ], 2212 | "query": [ 2213 | { 2214 | "key": "media_type", 2215 | "value": "TEXT", 2216 | "description": "Indicates the current media type." 2217 | }, 2218 | { 2219 | "key": "text", 2220 | "value": "Text", 2221 | "description": "The text associated with the post." 2222 | }, 2223 | { 2224 | "key": "reply_control", 2225 | "value": "{{reply_control}}", 2226 | "description": "Can be used to specify who can reply to a post." 2227 | } 2228 | ] 2229 | }, 2230 | "description": "Use the [reply_control](https://developers.facebook.com/docs/threads/reply-management#control-who-can-reply) parameter to specify who can reply to a post being created for publishing." 2231 | }, 2232 | "response": [ 2233 | { 2234 | "name": "Control Who Can Reply", 2235 | "originalRequest": { 2236 | "method": "POST", 2237 | "header": [], 2238 | "url": { 2239 | "raw": "{{api_host}}/me/threads?media_type=TEXT&text=Text&reply_control={{reply_control}}", 2240 | "host": [ 2241 | "{{api_host}}" 2242 | ], 2243 | "path": [ 2244 | "me", 2245 | "threads" 2246 | ], 2247 | "query": [ 2248 | { 2249 | "key": "media_type", 2250 | "value": "TEXT", 2251 | "description": "Indicates the current media type." 2252 | }, 2253 | { 2254 | "key": "text", 2255 | "value": "Text", 2256 | "description": "The text associated with the post." 2257 | }, 2258 | { 2259 | "key": "reply_control", 2260 | "value": "{{reply_control}}", 2261 | "description": "Can be used to specify who can reply to a post." 2262 | } 2263 | ] 2264 | } 2265 | }, 2266 | "_postman_previewlanguage": null, 2267 | "header": null, 2268 | "cookie": [], 2269 | "body": "{\n \"id\": \"string\"\n}" 2270 | } 2271 | ] 2272 | } 2273 | ], 2274 | "description": "The Threads Reply Moderation API allows you to read and manage replies to users' own Threads." 2275 | }, 2276 | { 2277 | "name": "Read Replies Media Objects", 2278 | "item": [ 2279 | { 2280 | "name": "Get a List of All a User's Replies", 2281 | "request": { 2282 | "method": "GET", 2283 | "header": [], 2284 | "url": { 2285 | "raw": "{{api_host}}/me/replies?fields={{fields_replies}}&limit=50", 2286 | "host": [ 2287 | "{{api_host}}" 2288 | ], 2289 | "path": [ 2290 | "me", 2291 | "replies" 2292 | ], 2293 | "query": [ 2294 | { 2295 | "key": "fields", 2296 | "value": "{{fields_replies}}", 2297 | "description": "A comma-separated list of fields for replies on Threads." 2298 | }, 2299 | { 2300 | "key": "since", 2301 | "value": "", 2302 | "description": "Query string parameter representing the start date for retrieval.", 2303 | "disabled": true 2304 | }, 2305 | { 2306 | "key": "until", 2307 | "value": "", 2308 | "description": "Query string parameter representing the end date for retrieval.", 2309 | "disabled": true 2310 | }, 2311 | { 2312 | "key": "limit", 2313 | "value": "50", 2314 | "description": "Query string parameter representing the maximum number of media objects or records to return." 2315 | }, 2316 | { 2317 | "key": "before", 2318 | "value": "", 2319 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 2320 | "disabled": true 2321 | }, 2322 | { 2323 | "key": "after", 2324 | "value": "", 2325 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 2326 | "disabled": true 2327 | } 2328 | ] 2329 | }, 2330 | "description": "Use the `GET` /{threads-user-id}/threads [endpoint](https://developers.facebook.com/docs/threads/threads-media#retrieve-a-list-of-all-a-user-s-threads) to return a paginated list of all threads created by a user." 2331 | }, 2332 | "response": [ 2333 | { 2334 | "name": "Get a List of All a User's Replies", 2335 | "originalRequest": { 2336 | "method": "GET", 2337 | "header": [], 2338 | "url": { 2339 | "raw": "{{api_host}}/me/replies?fields={{fields_replies}}&limit=50", 2340 | "host": [ 2341 | "{{api_host}}" 2342 | ], 2343 | "path": [ 2344 | "me", 2345 | "replies" 2346 | ], 2347 | "query": [ 2348 | { 2349 | "key": "fields", 2350 | "value": "{{fields_replies}}", 2351 | "description": "A comma-separated list of fields for replies on Threads." 2352 | }, 2353 | { 2354 | "key": "since", 2355 | "value": null, 2356 | "description": "Query string parameter representing the start date for retrieval.", 2357 | "disabled": true 2358 | }, 2359 | { 2360 | "key": "until", 2361 | "value": null, 2362 | "description": "Query string parameter representing the end date for retrieval.", 2363 | "disabled": true 2364 | }, 2365 | { 2366 | "key": "limit", 2367 | "value": "50", 2368 | "description": "Query string parameter representing the maximum number of media objects or records to return." 2369 | }, 2370 | { 2371 | "key": "before", 2372 | "value": null, 2373 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 2374 | "disabled": true 2375 | }, 2376 | { 2377 | "key": "after", 2378 | "value": null, 2379 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 2380 | "disabled": true 2381 | } 2382 | ] 2383 | } 2384 | }, 2385 | "_postman_previewlanguage": "json", 2386 | "header": [ 2387 | { 2388 | "key": "Content-Type", 2389 | "value": "application/json", 2390 | "description": "", 2391 | "type": "text" 2392 | } 2393 | ], 2394 | "cookie": [], 2395 | "body": "{\n \"data\": [\n {\n \"id\": \"string\",\n \"media_product_type\": \"THREADS\",\n \"media_type\": \"TEXT_POST\",\n \"permalink\": \"string\",\n \"username\": \"string\",\n \"text\": \"REPLY\",\n \"timestamp\": \"2024-09-12T23:11:55+0000\",\n \"shortcode\": \"string\",\n \"is_quote_post\": false,\n \"has_replies\": false\n }\n ],\n \"paging\": {\n \"cursors\": {\n \"before\": \"string\",\n \"after\": \"string\"\n }\n }\n}" 2396 | } 2397 | ] 2398 | } 2399 | ], 2400 | "description": "This folder will enable you to use the Threads API to retrieve details about a user's own replies." 2401 | }, 2402 | { 2403 | "name": "Delete Threads Media Objects", 2404 | "item": [ 2405 | { 2406 | "name": "Delete Threads Post", 2407 | "request": { 2408 | "method": "DELETE", 2409 | "header": [], 2410 | "url": { 2411 | "raw": "{{api_host}}/{{thread_id}}", 2412 | "host": [ 2413 | "{{api_host}}" 2414 | ], 2415 | "path": [ 2416 | "{{thread_id}}" 2417 | ] 2418 | } 2419 | }, 2420 | "response": [ 2421 | { 2422 | "name": "Delete Threads Post", 2423 | "originalRequest": { 2424 | "method": "DELETE", 2425 | "header": [], 2426 | "url": { 2427 | "raw": "{{api_host}}/{{thread_id}}", 2428 | "host": [ 2429 | "{{api_host}}" 2430 | ], 2431 | "path": [ 2432 | "{{thread_id}}" 2433 | ] 2434 | } 2435 | }, 2436 | "_postman_previewlanguage": null, 2437 | "header": null, 2438 | "cookie": [], 2439 | "body": "{\n \"success\": true,\n \"deleted_id\": \"\"\n}" 2440 | } 2441 | ] 2442 | } 2443 | ], 2444 | "description": "This folder will enable you to use the Threads API to delete your own posts." 2445 | } 2446 | ], 2447 | "description": "This folder enables you to use the Threads API to retrieve details about profiles and media objects, reads insights and handle reply moderation." 2448 | }, 2449 | { 2450 | "name": "Discover Threads", 2451 | "item": [ 2452 | { 2453 | "name": "Search for Threads Posts", 2454 | "request": { 2455 | "method": "GET", 2456 | "header": [], 2457 | "url": { 2458 | "raw": "{{api_host}}/keyword_search?q=ThreadsAPI&search_type=TOP&fields={{fields_threads}}", 2459 | "host": [ 2460 | "{{api_host}}" 2461 | ], 2462 | "path": [ 2463 | "keyword_search" 2464 | ], 2465 | "query": [ 2466 | { 2467 | "key": "q", 2468 | "value": "ThreadsAPI", 2469 | "description": "This is the query to be searched for." 2470 | }, 2471 | { 2472 | "key": "search_type", 2473 | "value": "TOP", 2474 | "description": "This is the type of search to be performed. This can be either TOP or RECENT." 2475 | }, 2476 | { 2477 | "key": "limit", 2478 | "value": "", 2479 | "description": "Query string parameter representing the maximum number of media objects or records to return.", 2480 | "disabled": true 2481 | }, 2482 | { 2483 | "key": "fields", 2484 | "value": "{{fields_threads}}", 2485 | "description": "A comma-separated list of fields for media objects on Threads." 2486 | } 2487 | ] 2488 | } 2489 | }, 2490 | "response": [ 2491 | { 2492 | "name": "Search for Threads Posts", 2493 | "originalRequest": { 2494 | "method": "GET", 2495 | "header": [], 2496 | "url": { 2497 | "raw": "{{api_host}}/keyword_search?q=ThreadsAPI&search_type=TOP&fields={{fields_threads}}", 2498 | "host": [ 2499 | "{{api_host}}" 2500 | ], 2501 | "path": [ 2502 | "keyword_search" 2503 | ], 2504 | "query": [ 2505 | { 2506 | "key": "q", 2507 | "value": "ThreadsAPI", 2508 | "description": "This is the query to be searched for." 2509 | }, 2510 | { 2511 | "key": "search_type", 2512 | "value": "TOP", 2513 | "description": "This is the type of search to be performed. This can be either TOP or RECENT." 2514 | }, 2515 | { 2516 | "key": "limit", 2517 | "value": "", 2518 | "description": "Query string parameter representing the maximum number of media objects or records to return.", 2519 | "disabled": true 2520 | }, 2521 | { 2522 | "key": "fields", 2523 | "value": "{{fields_threads}}", 2524 | "description": "A comma-separated list of fields for media objects on Threads." 2525 | } 2526 | ] 2527 | } 2528 | }, 2529 | "_postman_previewlanguage": null, 2530 | "header": null, 2531 | "cookie": [], 2532 | "body": "{\n \"data\": [\n {\n \"id\": ,\n \"media_product_type\": \"THREADS\",\n \"media_type\": \"TEXT_POST\",\n \"permalink\": ,\n \"username\": ,\n \"text\": ,\n \"timestamp\": \"2025-01-01T00:00:00+0000\",\n \"shortcode\": ,\n \"is_quote_post\": false,\n \"has_replies\": false\n },\n ...\n ],\n \"paging\": {\n \"cursors\": {\n \"before\": ,\n \"after\": \n }\n }\n}" 2533 | } 2534 | ] 2535 | }, 2536 | { 2537 | "name": "Get Mentioned Threads Posts", 2538 | "request": { 2539 | "method": "GET", 2540 | "header": [], 2541 | "url": { 2542 | "raw": "{{api_host}}/me/mentions?fields={{fields_threads}}&limit=50", 2543 | "host": [ 2544 | "{{api_host}}" 2545 | ], 2546 | "path": [ 2547 | "me", 2548 | "mentions" 2549 | ], 2550 | "query": [ 2551 | { 2552 | "key": "fields", 2553 | "value": "{{fields_threads}}", 2554 | "description": "A comma-separated list of fields for media objects on Threads." 2555 | }, 2556 | { 2557 | "key": "since", 2558 | "value": "", 2559 | "description": "Query string parameter representing the start date for retrieval.", 2560 | "disabled": true 2561 | }, 2562 | { 2563 | "key": "until", 2564 | "value": "", 2565 | "description": "Query string parameter representing the end date for retrieval.", 2566 | "disabled": true 2567 | }, 2568 | { 2569 | "key": "limit", 2570 | "value": "50", 2571 | "description": "Query string parameter representing the maximum number of media objects or records to return." 2572 | }, 2573 | { 2574 | "key": "before", 2575 | "value": null, 2576 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 2577 | "disabled": true 2578 | }, 2579 | { 2580 | "key": "after", 2581 | "value": null, 2582 | "description": "Query string parameter representing a cursor that can be used for pagination. Both \"before\" and \"after\" cannot be sent in the same request.", 2583 | "disabled": true 2584 | } 2585 | ] 2586 | } 2587 | }, 2588 | "response": [ 2589 | { 2590 | "name": "Get Mentioned Threads Posts", 2591 | "originalRequest": { 2592 | "method": "GET", 2593 | "header": [], 2594 | "url": { 2595 | "raw": "{{api_host}}/me/mentions?fields={{fields_threads}}", 2596 | "host": [ 2597 | "{{api_host}}" 2598 | ], 2599 | "path": [ 2600 | "me", 2601 | "mentions" 2602 | ], 2603 | "query": [ 2604 | { 2605 | "key": "since", 2606 | "value": "", 2607 | "description": "Query string parameter representing the start date for retrieval.", 2608 | "disabled": true 2609 | }, 2610 | { 2611 | "key": "until", 2612 | "value": "", 2613 | "description": "Query string parameter representing the end date for retrieval.", 2614 | "disabled": true 2615 | }, 2616 | { 2617 | "key": "limit", 2618 | "value": "", 2619 | "description": "Query string parameter representing the maximum number of media objects or records to return.", 2620 | "disabled": true 2621 | }, 2622 | { 2623 | "key": "fields", 2624 | "value": "{{fields_threads}}", 2625 | "description": "A comma-separated list of fields for media objects on Threads." 2626 | } 2627 | ] 2628 | } 2629 | }, 2630 | "_postman_previewlanguage": null, 2631 | "header": null, 2632 | "cookie": [], 2633 | "body": "{\n \"data\": [\n {\n \"id\": ,\n \"media_product_type\": \"THREADS\",\n \"media_type\": \"TEXT_POST\",\n \"permalink\": ,\n \"username\": ,\n \"text\": ,\n \"timestamp\": \"2025-01-01T00:00:00+0000\",\n \"shortcode\": ,\n \"is_quote_post\": false,\n \"has_replies\": false\n },\n ...\n ],\n \"paging\": {\n \"cursors\": {\n \"before\": ,\n \"after\": \n }\n }\n}" 2634 | } 2635 | ] 2636 | } 2637 | ], 2638 | "description": "Discover Threads: This folder enables you to use the Threads API to search for public Threads posts and find posts in which the authenticated user is mentioned." 2639 | }, 2640 | { 2641 | "name": "Display Threads", 2642 | "item": [ 2643 | { 2644 | "name": "Embed a Threads Post", 2645 | "request": { 2646 | "auth": { 2647 | "type": "noauth" 2648 | }, 2649 | "method": "GET", 2650 | "header": [], 2651 | "url": { 2652 | "raw": "{{api_host}}/oembed?url=https://www.threads.net/@threads/post/DCkkKl_OGb1", 2653 | "host": [ 2654 | "{{api_host}}" 2655 | ], 2656 | "path": [ 2657 | "oembed" 2658 | ], 2659 | "query": [ 2660 | { 2661 | "key": "url", 2662 | "value": "https://www.threads.net/@threads/post/DCkkKl_OGb1", 2663 | "description": "This is the URL of the Threads post to be embedded. With standard access, you may embed posts from the @meta, @threads, @instagram, and @facebook accounts." 2664 | }, 2665 | { 2666 | "key": "access_token", 2667 | "value": "TH|{{app_id}}|{{app_secret}}", 2668 | "description": "This is your app access token. This consists of your app's ID and secret.", 2669 | "disabled": true 2670 | }, 2671 | { 2672 | "key": "access_token", 2673 | "value": "{{app_access_token}}", 2674 | "description": "This is your app access token received via the GET /oauth/access_token endpoint. See the Authorization folder for details.", 2675 | "disabled": true 2676 | } 2677 | ] 2678 | } 2679 | }, 2680 | "response": [ 2681 | { 2682 | "name": "Embed a Threads Post", 2683 | "originalRequest": { 2684 | "method": "GET", 2685 | "header": [], 2686 | "url": { 2687 | "raw": "{{api_host}}/oembed?url=https://www.threads.net/@threads/post/DCkkKl_OGb1", 2688 | "host": [ 2689 | "{{api_host}}" 2690 | ], 2691 | "path": [ 2692 | "oembed" 2693 | ], 2694 | "query": [ 2695 | { 2696 | "key": "url", 2697 | "value": "https://www.threads.net/@threads/post/DCkkKl_OGb1", 2698 | "description": "This is the URL of the Threads post to be embedded. With standard access, you may embed posts from the @meta, @threads, @instagram, and @facebook accounts." 2699 | }, 2700 | { 2701 | "key": "access_token", 2702 | "value": "TH|{{app_id}}|{{app_secret}}", 2703 | "description": "This is your app access token. This consists of your app's ID and secret.", 2704 | "disabled": true 2705 | }, 2706 | { 2707 | "key": "access_token", 2708 | "value": "{{app_access_token}}", 2709 | "description": "This is your app access token received via the GET /oauth/access_token endpoint. See the Authorization folder for details.", 2710 | "disabled": true 2711 | } 2712 | ] 2713 | } 2714 | }, 2715 | "_postman_previewlanguage": null, 2716 | "header": null, 2717 | "cookie": [], 2718 | "body": "{\n \"type\": \"rich\",\n \"version\": \"1.0\",\n \"html\": \"
View on Threads
\\n\",\n \"provider_name\": \"Threads\",\n \"provider_url\": \"https://www.threads.net/\",\n \"width\": 658\n}" 2719 | } 2720 | ] 2721 | } 2722 | ], 2723 | "description": "This folder enables you to use the Threads API to display Threads posts in other websites." 2724 | }, 2725 | { 2726 | "name": "Troubleshooting", 2727 | "item": [ 2728 | { 2729 | "name": "Check Container's Publishing Status", 2730 | "request": { 2731 | "method": "GET", 2732 | "header": [], 2733 | "url": { 2734 | "raw": "{{api_host}}/{{container_id}}/?fields={{fields_container}}", 2735 | "host": [ 2736 | "{{api_host}}" 2737 | ], 2738 | "path": [ 2739 | "{{container_id}}", 2740 | "" 2741 | ], 2742 | "query": [ 2743 | { 2744 | "key": "fields", 2745 | "value": "{{fields_container}}", 2746 | "description": "EXPIRED — The container was not published within 24 hours and has expired.\nERROR — The container failed to complete the publishing process.\nFINISHED — The container and its media object are ready to be published.\nIN_PROGRESS — The container is still in the publishing process.\nPUBLISHED — The container's media object has been published.\n" 2747 | } 2748 | ] 2749 | }, 2750 | "description": "Use `POST` /{threads-user-id}/threads_publish [endpoint](https://developers.facebook.com/docs/threads/troubleshooting#publishing-does-not-return-a-media-id) to check a container's publishing status." 2751 | }, 2752 | "response": [ 2753 | { 2754 | "name": "Check Container's Publishing Status", 2755 | "originalRequest": { 2756 | "method": "GET", 2757 | "header": [], 2758 | "url": { 2759 | "raw": "{{api_host}}/{{container_id}}/?fields={{fields_container}}", 2760 | "host": [ 2761 | "{{api_host}}" 2762 | ], 2763 | "path": [ 2764 | "{{container_id}}", 2765 | "" 2766 | ], 2767 | "query": [ 2768 | { 2769 | "key": "fields", 2770 | "value": "{{fields_container}}", 2771 | "description": "EXPIRED — The container was not published within 24 hours and has expired.\nERROR — The container failed to complete the publishing process.\nFINISHED — The container and its media object are ready to be published.\nIN_PROGRESS — The container is still in the publishing process.\nPUBLISHED — The container's media object has been published.\n" 2772 | } 2773 | ] 2774 | } 2775 | }, 2776 | "_postman_previewlanguage": null, 2777 | "header": null, 2778 | "cookie": [], 2779 | "body": "{\n \"id\": \"string\",\n \"status\": \"FINISHED\"\n}" 2780 | } 2781 | ] 2782 | }, 2783 | { 2784 | "name": "Retrieve Publishing Quota Limit", 2785 | "request": { 2786 | "method": "GET", 2787 | "header": [], 2788 | "url": { 2789 | "raw": "{{api_host}}/me/threads_publishing_limit?fields={{fields_quota}}", 2790 | "host": [ 2791 | "{{api_host}}" 2792 | ], 2793 | "path": [ 2794 | "me", 2795 | "threads_publishing_limit" 2796 | ], 2797 | "query": [ 2798 | { 2799 | "key": "fields", 2800 | "value": "{{fields_quota}}", 2801 | "description": "The user's current Threads API publishing usage total." 2802 | } 2803 | ] 2804 | }, 2805 | "description": "Use `GET` /threads_publishing_limit [endpoint](https://developers.facebook.com/docs/threads/troubleshooting#retrieve-publishing-quota-limit) to check a user's publishing API quota limit." 2806 | }, 2807 | "response": [ 2808 | { 2809 | "name": "Retrieve Publishing Quota Limit", 2810 | "originalRequest": { 2811 | "method": "GET", 2812 | "header": [], 2813 | "url": { 2814 | "raw": "{{api_host}}/me/threads_publishing_limit?fields={{fields_quota}}", 2815 | "host": [ 2816 | "{{api_host}}" 2817 | ], 2818 | "path": [ 2819 | "me", 2820 | "threads_publishing_limit" 2821 | ], 2822 | "query": [ 2823 | { 2824 | "key": "fields", 2825 | "value": "{{fields_quota}}", 2826 | "description": "The user's current Threads API publishing usage total." 2827 | } 2828 | ] 2829 | } 2830 | }, 2831 | "_postman_previewlanguage": null, 2832 | "header": null, 2833 | "cookie": [], 2834 | "body": "{\n \"data\": [\n {\n \"quota_usage\": 1,\n \"config\": {\n \"quota_total\": 250,\n \"quota_duration\": 86400\n },\n \"reply_quota_usage\": 0,\n \"reply_config\": {\n \"quota_total\": 1000,\n \"quota_duration\": 86400\n }\n }\n ]\n}" 2835 | } 2836 | ] 2837 | } 2838 | ], 2839 | "description": "This folder enables you to perform basic toubleshooting." 2840 | } 2841 | ], 2842 | "auth": { 2843 | "type": "oauth2", 2844 | "oauth2": [ 2845 | { 2846 | "key": "useBrowser", 2847 | "value": true, 2848 | "type": "boolean" 2849 | }, 2850 | { 2851 | "key": "authUrl", 2852 | "value": "{{authorization_host}}/oauth/authorize?response_type=code", 2853 | "type": "string" 2854 | }, 2855 | { 2856 | "key": "accessTokenUrl", 2857 | "value": "{{api_host}}/oauth/access_token", 2858 | "type": "string" 2859 | }, 2860 | { 2861 | "key": "client_authentication", 2862 | "value": "body", 2863 | "type": "string" 2864 | }, 2865 | { 2866 | "key": "tokenName", 2867 | "value": "MyToken", 2868 | "type": "string" 2869 | }, 2870 | { 2871 | "key": "scope", 2872 | "value": "{{scope}}", 2873 | "type": "string" 2874 | }, 2875 | { 2876 | "key": "redirect_uri", 2877 | "value": "", 2878 | "type": "string" 2879 | }, 2880 | { 2881 | "key": "grant_type", 2882 | "value": "authorization_code", 2883 | "type": "string" 2884 | }, 2885 | { 2886 | "key": "addTokenTo", 2887 | "value": "queryParams", 2888 | "type": "string" 2889 | }, 2890 | { 2891 | "key": "clientSecret", 2892 | "value": "{{app_secret}}", 2893 | "type": "string" 2894 | }, 2895 | { 2896 | "key": "clientId", 2897 | "value": "{{app_id}}", 2898 | "type": "string" 2899 | } 2900 | ] 2901 | }, 2902 | "event": [ 2903 | { 2904 | "listen": "prerequest", 2905 | "script": { 2906 | "type": "text/javascript", 2907 | "exec": [ 2908 | "" 2909 | ] 2910 | } 2911 | }, 2912 | { 2913 | "listen": "test", 2914 | "script": { 2915 | "type": "text/javascript", 2916 | "exec": [ 2917 | "" 2918 | ] 2919 | } 2920 | } 2921 | ], 2922 | "variable": [ 2923 | { 2924 | "key": "authorization_host", 2925 | "value": "https://www.threads.net", 2926 | "type": "string" 2927 | }, 2928 | { 2929 | "key": "api_host", 2930 | "value": "https://graph.threads.net", 2931 | "type": "string" 2932 | }, 2933 | { 2934 | "key": "app_id", 2935 | "value": "", 2936 | "type": "string" 2937 | }, 2938 | { 2939 | "key": "app_secret", 2940 | "value": "", 2941 | "type": "string" 2942 | }, 2943 | { 2944 | "key": "app_access_token", 2945 | "value": "", 2946 | "type": "string" 2947 | }, 2948 | { 2949 | "key": "user_id", 2950 | "value": "", 2951 | "type": "string" 2952 | }, 2953 | { 2954 | "key": "container_id", 2955 | "value": "", 2956 | "type": "string" 2957 | }, 2958 | { 2959 | "key": "thread_id", 2960 | "value": "", 2961 | "type": "string" 2962 | }, 2963 | { 2964 | "key": "reply_thread_id", 2965 | "value": "", 2966 | "type": "string" 2967 | }, 2968 | { 2969 | "key": "quote_post_id", 2970 | "value": "", 2971 | "type": "string" 2972 | }, 2973 | { 2974 | "key": "redirect_uri", 2975 | "value": "https://www.domain.com/login" 2976 | }, 2977 | { 2978 | "key": "code", 2979 | "value": "code" 2980 | }, 2981 | { 2982 | "key": "scope", 2983 | "value": "threads_basic,threads_content_publish,threads_manage_insights,threads_manage_replies,threads_read_replies,threads_keyword_search,threads_manage_mentions" 2984 | }, 2985 | { 2986 | "key": "media_type", 2987 | "value": "TEXT, IMAGE, VIDEO, CAROUSEL" 2988 | }, 2989 | { 2990 | "key": "reply_control", 2991 | "value": "everyone,accounts_you_follow,mentioned_only" 2992 | }, 2993 | { 2994 | "key": "metrics_post", 2995 | "value": "views,likes,replies,reposts,quotes,shares", 2996 | "type": "string" 2997 | }, 2998 | { 2999 | "key": "metrics_account", 3000 | "value": "views,likes,replies,reposts,quotes,followers_count,follower_demographics", 3001 | "type": "string" 3002 | }, 3003 | { 3004 | "key": "follower_demographics_breakdown", 3005 | "value": "country,city,age,gender", 3006 | "type": "string" 3007 | }, 3008 | { 3009 | "key": "fields_threads", 3010 | "value": "id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,quoted_post,reposted_post,has_replies,alt_text,link_attachment_url,poll_attachment{option_a,option_b,option_c,option_d,option_a_votes_percentage,option_b_votes_percentage,option_c_votes_percentage,option_d_votes_percentage,expiration_timestamp}" 3011 | }, 3012 | { 3013 | "key": "fields_replies", 3014 | "value": "id,text,timestamp,media_product_type,media_type,media_url,permalink,shortcode,thumbnail_url,username,children,is_quote_post,quoted_post,reposted_post,alt_text,link_attachment_url,has_replies,is_reply,is_reply_owned_by_me,root_post,replied_to,hide_status,reply_audience,poll_attachment{option_a,option_b,option_c,option_d,option_a_votes_percentage,option_b_votes_percentage,option_c_votes_percentage,option_d_votes_percentage,expiration_timestamp}" 3015 | }, 3016 | { 3017 | "key": "fields_profile", 3018 | "value": "id,username,name,threads_profile_picture_url,threads_biography" 3019 | }, 3020 | { 3021 | "key": "video_url", 3022 | "value": "video_url" 3023 | }, 3024 | { 3025 | "key": "image_url", 3026 | "value": "image_url" 3027 | }, 3028 | { 3029 | "key": "fields_quota", 3030 | "value": "quota_usage,config,reply_quota_usage,reply_config" 3031 | }, 3032 | { 3033 | "key": "fields_container", 3034 | "value": "id,status,error_message", 3035 | "type": "string" 3036 | }, 3037 | { 3038 | "key": "auto_save_variables", 3039 | "value": "false", 3040 | "type": "string" 3041 | }, 3042 | { 3043 | "key": "carousel_children_ids", 3044 | "value": "", 3045 | "type": "string" 3046 | } 3047 | ] 3048 | } 3049 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Copyright (c) Meta Platforms, Inc. and affiliates. 3 | // All rights reserved. 4 | // This source code is licensed under the license found in the 5 | // LICENSE file in the root directory of this source tree. 6 | */ 7 | * { 8 | font-family: sans-serif; 9 | } 10 | 11 | button { 12 | background-color: #2196F3; /* Green */ 13 | border: none; 14 | color: white; 15 | padding: 10px 20px; 16 | text-align: center; 17 | font-size: 15px; 18 | display: block; 19 | margin: 10px auto; 20 | width: 200px; 21 | } 22 | 23 | body { 24 | margin: 60px 40px; 25 | text-align: center; 26 | } 27 | 28 | input[type='submit']{ 29 | justify-content: center; 30 | width: 100px; 31 | background-color: #2196F3; /* Green */ 32 | border: none; 33 | color: white; 34 | padding: 10px 20px; 35 | text-align: center; 36 | font-size: 15px; 37 | display: block; 38 | margin: 10px auto; 39 | width: 200px; 40 | } 41 | 42 | input[type='submit']:disabled{ 43 | background-color: #cccccc; 44 | } 45 | 46 | input[type='text'] { 47 | width: 500px; 48 | height: 20px; 49 | } 50 | 51 | textarea { 52 | width: 500px; 53 | overflow: hidden; 54 | resize: none; 55 | white-space: pre-wrap; 56 | word-wrap: break-word; 57 | } 58 | 59 | form { 60 | flex-direction: column; 61 | display: flex; 62 | align-items: center; 63 | } 64 | 65 | label { 66 | text-align: center; 67 | } 68 | 69 | .btn-group { 70 | margin: 20px auto; 71 | width: 30%; 72 | } 73 | 74 | .title { 75 | display: inline-block; 76 | padding-bottom: -10px; 77 | text-align: center; 78 | margin: 20px; 79 | } 80 | 81 | .radio-group { 82 | margin: 20px auto; 83 | text-align: center; 84 | width: 500px; 85 | } 86 | 87 | .radio-child { 88 | text-align: left; 89 | } 90 | 91 | .alert { 92 | margin: 30px 10px; 93 | padding: 10px; 94 | background-color: #3b9610; 95 | color: white; 96 | } 97 | 98 | .alert-error { 99 | margin: 30px 10px; 100 | padding: 10px; 101 | background-color: #a51414; 102 | color: white; 103 | } 104 | 105 | .closebtn { 106 | margin-left: 15px; 107 | color: white; 108 | font-weight: bold; 109 | float: right; 110 | font-size: 22px; 111 | line-height: 20px; 112 | cursor: pointer; 113 | transition: 0.3s; 114 | } 115 | 116 | .closebtn:hover { 117 | color: black; 118 | } 119 | 120 | table { 121 | margin-left: auto; 122 | margin-right: auto; 123 | border: 1px solid black; 124 | table-layout: auto; 125 | width: 100%; 126 | } 127 | 128 | th, td { 129 | padding: 10px; 130 | text-align: left; 131 | border: 1px solid black; 132 | } 133 | 134 | th { 135 | padding-top: 12px; 136 | padding-bottom: 12px; 137 | text-align: left; 138 | background-color: #2196F3; 139 | color: white; 140 | } 141 | 142 | .attachments-area { 143 | width: 100%; 144 | padding-top: 12px; 145 | padding-bottom: 4px; 146 | } 147 | 148 | .attachment-controls { 149 | width: 42px; 150 | height: 42px; 151 | position: relative; 152 | cursor: pointer; 153 | } 154 | 155 | .profile-picture img { 156 | border-radius: 50%; 157 | } 158 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbsamples/threads_api/707f54a0c6d50932f1f3a0b0a34c7771022e79c4/public/favicon.ico -------------------------------------------------------------------------------- /public/img/attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbsamples/threads_api/707f54a0c6d50932f1f3a0b0a34c7771022e79c4/public/img/attachment.png -------------------------------------------------------------------------------- /public/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbsamples/threads_api/707f54a0c6d50932f1f3a0b0a34c7771022e79c4/public/img/loading.gif -------------------------------------------------------------------------------- /public/scripts/form.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * This source code is licensed under the license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | let loadingOverlay; 9 | 10 | document.addEventListener('DOMContentLoaded', () => { 11 | loadingOverlay = document.getElementById('loadingOverlay'); 12 | }); 13 | 14 | function onAsyncRequestStarting() { 15 | if (loadingOverlay) { 16 | loadingOverlay.style.display = 'block'; 17 | } 18 | } 19 | 20 | function onAsyncRequestEnded() { 21 | if (loadingOverlay) { 22 | loadingOverlay.style.display = 'none'; 23 | } 24 | } 25 | 26 | async function processFormAsync(urlGenerator) { 27 | const form = document.getElementById('form'); 28 | form.addEventListener('submit', async (e) => { 29 | e.preventDefault(); 30 | 31 | const button = document.getElementById('submit'); 32 | const formData = new FormData(e.target, button); 33 | 34 | onAsyncRequestStarting(); 35 | 36 | let id; 37 | try { 38 | let response = await fetch(e.target.getAttribute('action'), { 39 | method: 'POST', 40 | body: formData 41 | }); 42 | 43 | if(response.ok) { 44 | let jsonResponse = await response.json(); 45 | id = jsonResponse.id; 46 | } else { 47 | resultElem.textContent = 'Error submitting form'; 48 | } 49 | } catch (error) { 50 | console.error('There was an error:', error); 51 | resultElem.textContent = 'Error submitting form'; 52 | } finally { 53 | onAsyncRequestEnded(); 54 | } 55 | 56 | if (id) { 57 | window.location.href = urlGenerator(id); 58 | } 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /public/scripts/hide-reply.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * This source code is licensed under the license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | document.addEventListener('DOMContentLoaded', async () => { 9 | const hideForms = document.getElementsByClassName('hide-reply'); 10 | for (let i = 0; i < hideForms.length; i++) { 11 | const hideForm = hideForms[i]; 12 | hideForm.addEventListener('submit', async (e) => { 13 | e.preventDefault(); 14 | onAsyncRequestStarting(); 15 | 16 | const formData = new FormData(e.target); 17 | try { 18 | let response = await fetch(e.target.getAttribute('action'), { 19 | method: 'POST', 20 | body: formData 21 | }); 22 | 23 | if(response.ok) { 24 | const submitButton = e.target.querySelector('input[type="submit"]'); 25 | submitButton.value = submitButton.value === 'Hide' ? 'Unhide' : 'Hide'; 26 | } else { 27 | alert('An error occurred while hiding/unhiding the reply.'); 28 | } 29 | } catch (e) { 30 | console.error('There was an error:', error); 31 | alert('error while hiding reply') 32 | } finally { 33 | onAsyncRequestEnded(); 34 | } 35 | }); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /public/scripts/publish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * This source code is licensed under the license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const QUERYING_STATUS_TEXT = '...'; 9 | const QUERY_CONTAINER_STATUS_TIMEOUT_IN_SECONDS = 5; 10 | 11 | document.addEventListener('DOMContentLoaded', async () => { 12 | await processFormAsync((id) => `/threads/${id}`); 13 | 14 | queryContainerStatus(); 15 | }); 16 | 17 | async function queryContainerStatus() { 18 | const containerId = document.getElementById('container-id').getAttribute('value'); 19 | 20 | const statusDOMElement = document.getElementById('container-status'); 21 | statusDOMElement.innerText = QUERYING_STATUS_TEXT; 22 | 23 | onAsyncRequestStarting(); 24 | 25 | let jsonResponse; 26 | try { 27 | let response = await fetch(`/container/status/${containerId}`); 28 | 29 | if(response?.ok) { 30 | jsonResponse = await response.json(); 31 | } else { 32 | console.error(response); 33 | } 34 | } catch (e) { 35 | console.error(e); 36 | } finally { 37 | onAsyncRequestEnded(); 38 | } 39 | 40 | switch (jsonResponse?.status) { 41 | case 'FINISHED': 42 | // Enable publishing 43 | document.getElementById('submit').removeAttribute('disabled'); 44 | break; 45 | case 'IN_PROGRESS': 46 | // Retry 47 | setTimeout(queryContainerStatus, QUERY_CONTAINER_STATUS_TIMEOUT_IN_SECONDS * 1000); 48 | break; 49 | case 'ERROR': 50 | default: 51 | document.getElementById('error-message').textContent = jsonResponse?.error_message ?? "Unknown error"; 52 | break; 53 | } 54 | 55 | updateView(statusDOMElement, jsonResponse?.status); 56 | } 57 | 58 | function updateView(statusDOMElement, status) { 59 | statusDOMElement.innerText = status; 60 | 61 | const explanationTemplate = document.getElementById(`template-status-${status}`); 62 | if (explanationTemplate) { 63 | const explanationParentDOMElement = document.getElementById('status-explanation'); 64 | if (explanationParentDOMElement) { 65 | const explanationNode = explanationTemplate.content.cloneNode(true); 66 | explanationParentDOMElement.replaceChildren(explanationNode); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/scripts/upload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * This source code is licensed under the license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | async function updateMediaType(attachmentsCount, attachmentListElem) { 9 | const mediaTypeElem = document.getElementById('media-type'); 10 | 11 | let mediaTypeDesc; 12 | if (attachmentsCount === 0) { 13 | mediaTypeDesc = 'Text 📝'; 14 | } 15 | else if (attachmentsCount === 1) { 16 | const singleAttachmentType = attachmentListElem.querySelector('select').value; 17 | if (singleAttachmentType === 'Image') 18 | mediaTypeDesc = 'Image 🖼️'; 19 | else 20 | mediaTypeDesc = 'Video 🎬'; 21 | } 22 | else { 23 | mediaTypeDesc = 'Carousel 🎠'; 24 | } 25 | 26 | mediaTypeElem.innerText = mediaTypeDesc; 27 | } 28 | 29 | document.addEventListener('DOMContentLoaded', async () => { 30 | await processFormAsync((id) => `/publish/${id}`); 31 | 32 | const attachmentsButton = document.getElementById('attachments-button'); 33 | attachmentsButton.addEventListener('click', async (e) => { 34 | e.preventDefault(); 35 | 36 | const attachmentsList = document.getElementById('attachments-list'); 37 | const template = document.getElementById('attachment-template'); 38 | 39 | const div = document.createElement('div'); 40 | div.innerHTML = template.textContent; 41 | attachmentsList.appendChild(div); 42 | 43 | const deleteElem = div.querySelector('span.delete'); 44 | deleteElem.addEventListener('click', async (e) => { 45 | const parentDiv = e.target.parentNode.parentNode; 46 | parentDiv.remove(); 47 | 48 | await updateMediaType(attachmentsList.children.length, attachmentsList); 49 | }); 50 | 51 | const mediaTypeSelectElem = div.querySelector('select'); 52 | mediaTypeSelectElem.addEventListener('change', async (e) => { 53 | await updateMediaType(attachmentsList.children.length, attachmentsList); 54 | }); 55 | 56 | await updateMediaType(attachmentsList.children.length, attachmentsList); 57 | }); 58 | 59 | await updateMediaType(0, null); 60 | }); 61 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * This source code is licensed under the license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const express = require('express'); 9 | const session = require('express-session'); 10 | const bodyParser = require('body-parser'); 11 | const axios = require('axios'); 12 | const https = require('https'); 13 | const path = require('path'); 14 | const fs = require('fs'); 15 | const { URLSearchParams, URL } = require('url'); 16 | const multer = require('multer'); 17 | 18 | const app = express(); 19 | const upload = multer(); 20 | 21 | const DEFAULT_THREADS_QUERY_LIMIT = 10; 22 | 23 | const FIELD__ALT_TEXT = 'alt_text'; 24 | const FIELD__ERROR_MESSAGE = 'error_message'; 25 | const FIELD__FOLLOWERS_COUNT = 'followers_count'; 26 | const FIELD__HIDE_STATUS = 'hide_status'; 27 | const FIELD__ID = 'id'; 28 | const FIELD__IS_REPLY = 'is_reply'; 29 | const FIELD__LIKES = 'likes'; 30 | const FIELD__LINK_ATTACHMENT_URL = 'link_attachment_url'; 31 | const FIELD__MEDIA_TYPE = 'media_type'; 32 | const FIELD__MEDIA_URL = 'media_url'; 33 | const FIELD__PERMALINK = 'permalink'; 34 | const FIELD__REPLIES = 'replies'; 35 | const FIELD__REPOSTS = 'reposts'; 36 | const FIELD__QUOTES = 'quotes'; 37 | const FIELD__REPLY_AUDIENCE = 'reply_audience'; 38 | const FIELD__STATUS = 'status'; 39 | const FIELD__TEXT = 'text'; 40 | const FIELD__TIMESTAMP = 'timestamp'; 41 | const FIELD__THREADS_BIOGRAPHY = 'threads_biography'; 42 | const FIELD__THREADS_PROFILE_PICTURE_URL = 'threads_profile_picture_url'; 43 | const FIELD__USERNAME = 'username'; 44 | const FIELD__VIEWS = 'views'; 45 | 46 | const MEDIA_TYPE__CAROUSEL = 'CAROUSEL'; 47 | const MEDIA_TYPE__IMAGE = 'IMAGE'; 48 | const MEDIA_TYPE__TEXT = 'TEXT'; 49 | const MEDIA_TYPE__VIDEO = 'VIDEO'; 50 | 51 | const PARAMS__ACCESS_TOKEN = 'access_token'; 52 | const PARAMS__ALT_TEXT = 'alt_text'; 53 | const PARAMS__CLIENT_ID = 'client_id'; 54 | const PARAMS__CONFIG = 'config'; 55 | const PARAMS__FIELDS = 'fields'; 56 | const PARAMS__HIDE = 'hide'; 57 | const PARAMS__LINK_ATTACHMENT = 'link_attachment'; 58 | const PARAMS__METRIC = 'metric'; 59 | const PARAMS__Q = 'q'; 60 | const PARAMS__QUOTA_USAGE = 'quota_usage'; 61 | const PARAMS__QUOTE_POST_ID = 'quote_post_id'; 62 | const PARAMS__REDIRECT_URI = 'redirect_uri'; 63 | const PARAMS__REPLY_CONFIG = 'reply_config'; 64 | const PARAMS__REPLY_CONTROL = 'reply_control'; 65 | const PARAMS__REPLY_QUOTA_USAGE = 'reply_quota_usage'; 66 | const PARAMS__REPLY_TO_ID = 'reply_to_id'; 67 | const PARAMS__RESPONSE_TYPE = 'response_type'; 68 | const PARAMS__RETURN_URL = 'return_url'; 69 | const PARAMS__SCOPE = 'scope'; 70 | const PARAMS__SEARCH_TYPE = 'search_type'; 71 | const PARAMS__TEXT = 'text'; 72 | 73 | // Read variables from environment 74 | require('dotenv').config(); 75 | const { 76 | HOST, 77 | PORT, 78 | REDIRECT_URI, 79 | APP_ID, 80 | API_SECRET, 81 | GRAPH_API_VERSION, 82 | INITIAL_ACCESS_TOKEN, 83 | INITIAL_USER_ID, 84 | REJECT_UNAUTHORIZED, 85 | } = process.env; 86 | 87 | const agent = new https.Agent({ 88 | rejectUnauthorized: REJECT_UNAUTHORIZED !== 'false', 89 | }); 90 | 91 | const GRAPH_API_BASE_URL = 'https://graph.threads.net/' + 92 | (GRAPH_API_VERSION ? GRAPH_API_VERSION + '/' : ''); 93 | const AUTHORIZATION_BASE_URL = 'https://www.threads.net'; 94 | 95 | let initial_access_token = INITIAL_ACCESS_TOKEN; 96 | let initial_user_id = INITIAL_USER_ID; 97 | 98 | // Access scopes required for the token 99 | const SCOPES = [ 100 | 'threads_basic', 101 | 'threads_content_publish', 102 | 'threads_manage_insights', 103 | 'threads_manage_replies', 104 | 'threads_read_replies', 105 | 'threads_keyword_search', 106 | 'threads_manage_mentions', 107 | ]; 108 | 109 | app.use(express.static('public')); 110 | app.set('views', path.join(__dirname, '../views')); 111 | app.set('view engine', 'pug'); 112 | app.use(bodyParser.json()); 113 | app.use(bodyParser.urlencoded({ extended: true })); 114 | app.use( 115 | session({ 116 | secret: process.env.SESSION_SECRET, 117 | resave: false, 118 | saveUninitialized: true, 119 | cookie: { 120 | maxAge: 6000000, 121 | }, 122 | }) 123 | ); 124 | 125 | // Middleware to ensure the user is logged in 126 | const loggedInUserChecker = (req, res, next) => { 127 | if (req.session.access_token) { 128 | next(); 129 | } else if (initial_access_token && initial_user_id) { 130 | useInitialAuthenticationValues(req); 131 | next(); 132 | } else { 133 | const returnUrl = encodeURIComponent(req.originalUrl); 134 | res.redirect(`/?${PARAMS__RETURN_URL}=${returnUrl}`); 135 | } 136 | }; 137 | 138 | app.get('/', async (req, res) => { 139 | if (!(req.session.access_token) && 140 | (initial_access_token && initial_user_id)) { 141 | useInitialAuthenticationValues(req); 142 | res.redirect('/account'); 143 | } else { 144 | res.render('index', { 145 | title: 'Index', 146 | returnUrl: req.query[PARAMS__RETURN_URL], 147 | }); 148 | } 149 | }); 150 | 151 | // Login route using OAuth 152 | app.get('/login', (req, res) => { 153 | const url = buildGraphAPIURL('oauth/authorize', { 154 | [PARAMS__SCOPE]: SCOPES.join(','), 155 | [PARAMS__CLIENT_ID]: APP_ID, 156 | [PARAMS__REDIRECT_URI]: REDIRECT_URI, 157 | [PARAMS__RESPONSE_TYPE]: 'code', 158 | }, null, AUTHORIZATION_BASE_URL); 159 | 160 | res.redirect(url); 161 | }); 162 | 163 | // Callback route for OAuth user token And reroute to '/pages' 164 | app.get('/callback', async (req, res) => { 165 | const code = req.query.code; 166 | const uri = buildGraphAPIURL('oauth/access_token', {}, null, GRAPH_API_BASE_URL); 167 | 168 | try { 169 | const response = await axios.post(uri, new URLSearchParams({ 170 | client_id: APP_ID, 171 | client_secret: API_SECRET, 172 | grant_type: 'authorization_code', 173 | redirect_uri: REDIRECT_URI, 174 | code, 175 | }).toString(), { 176 | headers: { 177 | 'Content-Type': 'application/x-www-form-urlencoded', 178 | }, 179 | httpsAgent: agent, 180 | }); 181 | req.session.access_token = response.data.access_token; 182 | res.redirect('/account'); 183 | } catch (err) { 184 | console.error(err?.response?.data); 185 | res.render('index', { 186 | error: `There was an error with the request: ${err}`, 187 | }); 188 | } 189 | }); 190 | 191 | app.get('/account', loggedInUserChecker, async (req, res) => { 192 | const getUserDetailsUrl = buildGraphAPIURL('me', { 193 | [PARAMS__FIELDS]: [ 194 | FIELD__USERNAME, 195 | FIELD__THREADS_PROFILE_PICTURE_URL, 196 | FIELD__THREADS_BIOGRAPHY, 197 | ].join(','), 198 | }, req.session.access_token); 199 | 200 | let userDetails = {}; 201 | try { 202 | const response = await axios.get(getUserDetailsUrl, { httpsAgent: agent }); 203 | userDetails = response.data; 204 | 205 | // This value is not currently used but it may come handy in the future 206 | if (!req.session.user_id) 207 | req.session.user_id = response.data.id; 208 | 209 | userDetails.user_profile_url = `https://www.threads.net/@${userDetails.username}`; 210 | } catch (e) { 211 | console.error(e); 212 | } 213 | 214 | res.render('account', { 215 | title: 'Account', 216 | ...userDetails, 217 | }); 218 | }); 219 | 220 | app.get('/userInsights', loggedInUserChecker, async (req, res) => { 221 | const { since, until } = req.query; 222 | 223 | const params = { 224 | [PARAMS__METRIC]: [ 225 | FIELD__VIEWS, 226 | FIELD__LIKES, 227 | FIELD__REPLIES, 228 | FIELD__QUOTES, 229 | FIELD__REPOSTS, 230 | FIELD__FOLLOWERS_COUNT, 231 | ].join(',') 232 | }; 233 | if (since) { 234 | params.since = since; 235 | } 236 | if (until) { 237 | params.until = until; 238 | } 239 | 240 | const queryThreadUrl = buildGraphAPIURL(`me/threads_insights`, params, req.session.access_token); 241 | 242 | let data = []; 243 | try { 244 | const queryResponse = await axios.get(queryThreadUrl, { httpsAgent: agent }); 245 | data = queryResponse.data; 246 | } catch (e) { 247 | console.error(e?.response?.data?.error?.message ?? e.message); 248 | } 249 | 250 | const metrics = data?.data ?? []; 251 | for (const index in metrics) { 252 | const metric = metrics[index]; 253 | if (metric.name === FIELD__VIEWS) { 254 | // The "views" metric returns as a value for user insights 255 | getInsightsValue(metrics, index); 256 | } 257 | else { 258 | // All other metrics return as a total value 259 | getInsightsTotalValue(metrics, index); 260 | } 261 | } 262 | 263 | res.render('user_insights', { 264 | title: 'User Insights', 265 | metrics, 266 | since, 267 | until, 268 | }); 269 | }); 270 | 271 | app.get('/publishingLimit', loggedInUserChecker, async (req, res) => { 272 | const params = { 273 | [PARAMS__FIELDS]: [ 274 | PARAMS__QUOTA_USAGE, 275 | PARAMS__CONFIG, 276 | PARAMS__REPLY_QUOTA_USAGE, 277 | PARAMS__REPLY_CONFIG 278 | ].join(','), 279 | }; 280 | 281 | const publishingLimitUrl = buildGraphAPIURL(`me/threads_publishing_limit`, params, req.session.access_token); 282 | 283 | let data = []; 284 | try { 285 | const queryResponse = await axios.get(publishingLimitUrl, { httpsAgent: agent }); 286 | data = queryResponse.data; 287 | } catch (e) { 288 | console.error(e?.response?.data?.error?.message ?? e.message); 289 | } 290 | 291 | data = data.data?.[0] ?? {}; 292 | 293 | const quotaUsage = data[PARAMS__QUOTA_USAGE]; 294 | const config = data[PARAMS__CONFIG]; 295 | const replyQuotaUsage = data[PARAMS__REPLY_QUOTA_USAGE]; 296 | const replyConfig = data[PARAMS__REPLY_CONFIG]; 297 | 298 | res.render('publishing_limit', { 299 | title: 'Publishing Limit', 300 | quotaUsage, 301 | config, 302 | replyQuotaUsage, 303 | replyConfig, 304 | }); 305 | }); 306 | 307 | app.get('/upload', loggedInUserChecker, (req, res) => { 308 | const { replyToId, quotePostId } = req.query; 309 | const title = replyToId === undefined ? 'Upload' : 'Upload (Reply)'; 310 | res.render('upload', { 311 | title, 312 | replyToId, 313 | quotePostId, 314 | }); 315 | }); 316 | 317 | app.post('/repost', upload.array(), async (req, res) => { 318 | const { repostId } = req.body; 319 | 320 | const repostThreadsUrl = buildGraphAPIURL(`${repostId}/repost`, {}, req.session.access_token); 321 | try { 322 | const repostResponse = await axios.post(repostThreadsUrl, {}); 323 | const containerId = repostResponse.data.id; 324 | return res.redirect(`threads/${containerId}`); 325 | } 326 | catch (e) { 327 | console.error(e.message); 328 | return res.json({ 329 | error: true, 330 | message: `Error during repost: ${e}`, 331 | }); 332 | } 333 | }); 334 | 335 | app.post('/upload', upload.array(), async (req, res) => { 336 | const { text, attachmentType, attachmentUrl, attachmentAltText, replyControl, replyToId, linkAttachment, quotePostId } = req.body; 337 | const params = { 338 | [PARAMS__TEXT]: text, 339 | [PARAMS__REPLY_CONTROL]: replyControl, 340 | [PARAMS__REPLY_TO_ID]: replyToId, 341 | [PARAMS__LINK_ATTACHMENT]: linkAttachment, 342 | }; 343 | 344 | if (quotePostId) { 345 | params[PARAMS__QUOTE_POST_ID] = quotePostId; 346 | } 347 | 348 | // No attachments 349 | if (!attachmentType?.length) { 350 | params.media_type = MEDIA_TYPE__TEXT; 351 | } 352 | // Single attachment 353 | else if (attachmentType?.length === 1) { 354 | addAttachmentFields(params, attachmentType[0], attachmentUrl[0], attachmentAltText[0]); 355 | } 356 | // Multiple attachments 357 | else { 358 | params.media_type = MEDIA_TYPE__CAROUSEL; 359 | params.children = []; 360 | attachmentType.forEach((type, i) => { 361 | const child = { 362 | is_carousel_item: true, 363 | }; 364 | addAttachmentFields(child, type, attachmentUrl[i], attachmentAltText[i]); 365 | params.children.push(child); 366 | }); 367 | } 368 | 369 | if (params.media_type === MEDIA_TYPE__CAROUSEL) { 370 | const createChildPromises = params.children.map(child => ( 371 | axios.post( 372 | buildGraphAPIURL(`me/threads`, child, req.session.access_token), 373 | {}, 374 | ) 375 | )); 376 | try { 377 | const createCarouselItemResponse = await Promise.all(createChildPromises); 378 | // Replace children with the IDs 379 | params.children = createCarouselItemResponse 380 | .filter(response => response.status === 200) 381 | .map(response => response.data.id) 382 | .join(','); 383 | } catch (e) { 384 | console.error(e.message); 385 | res.json({ 386 | error: true, 387 | message: `Error creating child elements: ${e}`, 388 | }); 389 | return; 390 | } 391 | } 392 | 393 | const postThreadsUrl = buildGraphAPIURL(`me/threads`, params, req.session.access_token); 394 | try { 395 | const postResponse = await axios.post(postThreadsUrl, {}, { httpsAgent: agent }); 396 | const containerId = postResponse.data.id; 397 | res.json({ 398 | id: containerId, 399 | }); 400 | } 401 | catch (e) { 402 | console.error(e.message); 403 | res.json({ 404 | error: true, 405 | message: `Error during upload: ${e}`, 406 | }); 407 | } 408 | }); 409 | 410 | app.get('/publish/:containerId', loggedInUserChecker, async (req, res) => { 411 | const containerId = req.params.containerId; 412 | res.render('publish', { 413 | containerId, 414 | title: 'Publish', 415 | }); 416 | }); 417 | 418 | app.get('/container/status/:containerId', loggedInUserChecker, async (req, res) => { 419 | const { containerId } = req.params; 420 | const getContainerStatusUrl = buildGraphAPIURL(containerId, { 421 | [PARAMS__FIELDS]: [ 422 | FIELD__STATUS, 423 | FIELD__ERROR_MESSAGE 424 | ].join(','), 425 | }, req.session.access_token); 426 | 427 | try { 428 | const queryResponse = await axios.get(getContainerStatusUrl, { httpsAgent: agent }); 429 | res.json(queryResponse.data); 430 | } catch (e) { 431 | console.error(e.message); 432 | res.json({ 433 | error: true, 434 | message: `Error querying container status: ${e}`, 435 | }); 436 | } 437 | }); 438 | 439 | app.post('/publish', upload.array(), async (req, res) => { 440 | const { containerId } = req.body; 441 | const publishThreadsUrl = buildGraphAPIURL(`me/threads_publish`, { 442 | creation_id: containerId, 443 | }, req.session.access_token); 444 | 445 | try { 446 | const postResponse = await axios.post(publishThreadsUrl, { httpsAgent: agent }); 447 | const threadId = postResponse.data.id; 448 | res.json({ 449 | id: threadId, 450 | }); 451 | } 452 | catch (e) { 453 | console.error(e.message); 454 | res.json({ 455 | error: true, 456 | message: `Error during publishing: ${e}`, 457 | }); 458 | } 459 | }); 460 | 461 | app.get('/threads/:threadId', loggedInUserChecker, async (req, res) => { 462 | const { threadId } = req.params; 463 | let data = {}; 464 | const queryThreadUrl = buildGraphAPIURL(`${threadId}`, { 465 | [PARAMS__FIELDS]: [ 466 | FIELD__TEXT, 467 | FIELD__MEDIA_TYPE, 468 | FIELD__MEDIA_URL, 469 | FIELD__PERMALINK, 470 | FIELD__TIMESTAMP, 471 | FIELD__IS_REPLY, 472 | FIELD__USERNAME, 473 | FIELD__REPLY_AUDIENCE, 474 | FIELD__ALT_TEXT, 475 | FIELD__LINK_ATTACHMENT_URL, 476 | ].join(','), 477 | }, req.session.access_token); 478 | 479 | try { 480 | const queryResponse = await axios.get(queryThreadUrl, { httpsAgent: agent }); 481 | data = queryResponse.data; 482 | } catch (e) { 483 | console.error(e?.response?.data?.error?.message ?? e.message); 484 | } 485 | 486 | res.render('thread', { 487 | threadId, 488 | ...data, 489 | title: 'Thread', 490 | }); 491 | }); 492 | 493 | app.get('/threads', loggedInUserChecker, async (req, res) => { 494 | const { before, after, limit } = req.query; 495 | const params = { 496 | [PARAMS__FIELDS]: [ 497 | FIELD__TEXT, 498 | FIELD__MEDIA_TYPE, 499 | FIELD__MEDIA_URL, 500 | FIELD__PERMALINK, 501 | FIELD__TIMESTAMP, 502 | FIELD__REPLY_AUDIENCE, 503 | FIELD__ALT_TEXT, 504 | ].join(','), 505 | limit: limit ?? DEFAULT_THREADS_QUERY_LIMIT, 506 | }; 507 | if (before) { 508 | params.before = before; 509 | } 510 | if (after) { 511 | params.after = after; 512 | } 513 | 514 | let threads = []; 515 | let paging = {}; 516 | 517 | const queryThreadsUrl = buildGraphAPIURL(`me/threads`, params, req.session.access_token); 518 | 519 | try { 520 | const queryResponse = await axios.get(queryThreadsUrl, { httpsAgent: agent }); 521 | threads = queryResponse.data.data; 522 | 523 | if (queryResponse.data.paging) { 524 | const { next, previous } = queryResponse.data.paging; 525 | 526 | if (next) { 527 | paging.nextUrl = getCursorUrlFromGraphApiPagingUrl(req, next); 528 | } 529 | 530 | if (previous) { 531 | paging.previousUrl = getCursorUrlFromGraphApiPagingUrl(req, previous); 532 | } 533 | } 534 | } catch (e) { 535 | console.error(e?.response?.data?.error?.message ?? e.message); 536 | } 537 | 538 | res.render('threads', { 539 | paging, 540 | threads, 541 | title: 'Threads', 542 | }); 543 | }); 544 | 545 | app.get('/replies', loggedInUserChecker, async (req, res) => { 546 | const { before, after, limit } = req.query; 547 | const params = { 548 | [PARAMS__FIELDS]: [ 549 | FIELD__TEXT, 550 | FIELD__MEDIA_TYPE, 551 | FIELD__MEDIA_URL, 552 | FIELD__PERMALINK, 553 | FIELD__TIMESTAMP, 554 | FIELD__REPLY_AUDIENCE, 555 | ].join(','), 556 | limit: limit ?? DEFAULT_THREADS_QUERY_LIMIT, 557 | }; 558 | if (before) { 559 | params.before = before; 560 | } 561 | if (after) { 562 | params.after = after; 563 | } 564 | 565 | let threads = []; 566 | let paging = {}; 567 | 568 | const queryRepliesUrl = buildGraphAPIURL(`me/replies`, params, req.session.access_token); 569 | 570 | try { 571 | const queryResponse = await axios.get(queryRepliesUrl, { httpsAgent: agent }); 572 | threads = queryResponse.data.data; 573 | 574 | if (queryResponse.data.paging) { 575 | const { next, previous } = queryResponse.data.paging; 576 | 577 | if (next) { 578 | paging.nextUrl = getCursorUrlFromGraphApiPagingUrl(req, next); 579 | } 580 | 581 | if (previous) { 582 | paging.previousUrl = getCursorUrlFromGraphApiPagingUrl(req, previous); 583 | } 584 | } 585 | } catch (e) { 586 | console.error(e?.response?.data?.error?.message ?? e.message); 587 | } 588 | 589 | res.render('threads', { 590 | paging, 591 | threads, 592 | title: 'My Replies', 593 | }); 594 | }); 595 | 596 | app.get('/threads/:threadId/replies', loggedInUserChecker, (req, res) => { 597 | showReplies(req, res, true); 598 | }); 599 | 600 | app.get('/threads/:threadId/conversation', loggedInUserChecker, (req, res) => { 601 | showReplies(req, res, false); 602 | }); 603 | 604 | app.post('/manage_reply/:replyId', upload.array(), async (req, res) => { 605 | const { replyId } = req.params; 606 | const { hide } = req.query; 607 | 608 | const params = {}; 609 | if (hide) { 610 | params[PARAMS__HIDE] = hide === 'true'; 611 | } 612 | 613 | const hideReplyUrl = buildGraphAPIURL(`${replyId}/manage_reply`, {}, req.session.access_token); 614 | 615 | try { 616 | response = await axios.post(hideReplyUrl, params, { httpsAgent: agent }); 617 | } 618 | catch (e) { 619 | console.error(e?.message); 620 | return res.status(e?.response?.status ?? 500).json({ 621 | error: true, 622 | message: `Error while hiding reply: ${e}`, 623 | }); 624 | } 625 | 626 | return res.sendStatus(200); 627 | }); 628 | 629 | app.get('/threads/:threadId/insights', loggedInUserChecker, async (req, res) => { 630 | const { threadId } = req.params; 631 | const { since, until } = req.query; 632 | 633 | const params = { 634 | [PARAMS__METRIC]: [ 635 | FIELD__VIEWS, 636 | FIELD__LIKES, 637 | FIELD__REPLIES, 638 | FIELD__REPOSTS, 639 | FIELD__QUOTES, 640 | ].join(',') 641 | }; 642 | if (since) { 643 | params.since = since; 644 | } 645 | if (until) { 646 | params.until = until; 647 | } 648 | 649 | const queryThreadUrl = buildGraphAPIURL(`${threadId}/insights`, params, req.session.access_token); 650 | 651 | let data = []; 652 | try { 653 | const queryResponse = await axios.get(queryThreadUrl, { httpsAgent: agent }); 654 | data = queryResponse.data; 655 | } catch (e) { 656 | console.error(e?.response?.data?.error?.message ?? e.message); 657 | } 658 | 659 | const metrics = data?.data ?? []; 660 | for (const index in metrics) { 661 | // All metrics return as a value (rather than total value) for media insights 662 | getInsightsValue(metrics, index); 663 | } 664 | 665 | res.render('thread_insights', { 666 | title: 'Thread Insights', 667 | threadId, 668 | metrics, 669 | since, 670 | until, 671 | }); 672 | }); 673 | 674 | app.get('/mentions', loggedInUserChecker, async (req, res) => { 675 | const { before, after, limit } = req.query; 676 | const params = { 677 | [PARAMS__FIELDS]: [ 678 | FIELD__USERNAME, 679 | FIELD__TEXT, 680 | FIELD__MEDIA_TYPE, 681 | FIELD__MEDIA_URL, 682 | FIELD__PERMALINK, 683 | FIELD__TIMESTAMP, 684 | FIELD__REPLY_AUDIENCE, 685 | FIELD__ALT_TEXT, 686 | ].join(','), 687 | limit: limit ?? DEFAULT_THREADS_QUERY_LIMIT, 688 | }; 689 | if (before) { 690 | params.before = before; 691 | } 692 | if (after) { 693 | params.after = after; 694 | } 695 | 696 | const queryMentionsUrl = buildGraphAPIURL(`me/mentions`, params, req.session.access_token); 697 | 698 | let threads = []; 699 | let paging = {}; 700 | 701 | try { 702 | const queryResponse = await axios.get(queryMentionsUrl, { httpsAgent: agent }); 703 | threads = queryResponse.data.data; 704 | 705 | if (queryResponse.data.paging) { 706 | const { next, previous } = queryResponse.data.paging; 707 | 708 | if (next) { 709 | paging.nextUrl = getCursorUrlFromGraphApiPagingUrl(req, next); 710 | } 711 | 712 | if (previous) { 713 | paging.previousUrl = getCursorUrlFromGraphApiPagingUrl(req, previous); 714 | } 715 | } 716 | } catch (e) { 717 | console.error(e?.response?.data?.error?.message ?? e.message); 718 | } 719 | 720 | res.render('mentions', { 721 | title: 'Mentions', 722 | threads, 723 | paging, 724 | }); 725 | }); 726 | 727 | app.get('/keywordSearch', loggedInUserChecker, async (req, res) => { 728 | const { keyword, searchType } = req.query; 729 | 730 | if (!keyword) { 731 | return res.render('keyword_search', { 732 | title: 'Search for Threads', 733 | }); 734 | } 735 | 736 | const params = { 737 | [PARAMS__Q]: keyword, 738 | [PARAMS__SEARCH_TYPE]: searchType, 739 | [PARAMS__FIELDS]: [ 740 | FIELD__USERNAME, 741 | FIELD__ID, 742 | FIELD__TIMESTAMP, 743 | FIELD__MEDIA_TYPE, 744 | FIELD__TEXT, 745 | FIELD__PERMALINK, 746 | FIELD__REPLY_AUDIENCE, 747 | ].join(',') 748 | }; 749 | 750 | const keywordSearchUrl = buildGraphAPIURL(`keyword_search`, params, req.session.access_token); 751 | 752 | let threads = []; 753 | let paging = {}; 754 | 755 | try { 756 | const response = await axios.get(keywordSearchUrl, { httpsAgent: agent }); 757 | threads = response.data.data; 758 | 759 | if (response.data.paging) { 760 | const { next, previous } = response.data.paging; 761 | 762 | if (next) { 763 | paging.nextUrl = getCursorUrlFromGraphApiPagingUrl(req, next); 764 | } 765 | } 766 | } catch (e) { 767 | console.error(e?.response?.data?.error?.message ?? e.message); 768 | } 769 | 770 | return res.render('keyword_search', { 771 | title: 'Search for Threads', 772 | threads, 773 | paging, 774 | resultsTitle: `${searchType} results for '${keyword}'`, 775 | }); 776 | }); 777 | 778 | // Logout route to kill the session 779 | app.get('/logout', (req, res) => { 780 | if (req.session) { 781 | req.session.destroy((err) => { 782 | if (err) { 783 | res.render('index', { error: 'Unable to log out' }); 784 | } else { 785 | res.render('index', { response: 'Logout successful!' }); 786 | } 787 | }); 788 | } else { 789 | res.render('index', { response: 'Token not stored in session' }); 790 | } 791 | }); 792 | 793 | app.get('/oEmbed', async (req, res) => { 794 | const { url } = req.query; 795 | if (!url) { 796 | return res.render('oembed', { 797 | title: 'Embed Threads', 798 | }); 799 | } 800 | 801 | const oEmbedUrl = buildGraphAPIURL(`oembed`, { 802 | url, 803 | }, `TH|${APP_ID}|${API_SECRET}`); 804 | 805 | let html = '

Unable to embed

'; 806 | try { 807 | const response = await axios.get(oEmbedUrl, { httpsAgent: agent }); 808 | if (response.data?.html) { 809 | html = response.data.html; 810 | } 811 | } catch (e) { 812 | console.error(e?.response?.data?.error?.message ?? e.message); 813 | } 814 | 815 | return res.render('oembed', { 816 | title: 'Embed Threads', 817 | html, 818 | url, 819 | }); 820 | }); 821 | 822 | https 823 | .createServer({ 824 | key: fs.readFileSync(path.join(__dirname, '../'+ HOST +'-key.pem')), 825 | cert: fs.readFileSync(path.join(__dirname, '../'+ HOST +'.pem')), 826 | }, app) 827 | .listen(PORT, HOST, (err) => { 828 | if (err) { 829 | console.error(`Error: ${err}`); 830 | } 831 | console.log(`listening on port ${PORT}!`); 832 | }); 833 | 834 | /** 835 | * @param {string} path 836 | * @param {URLSearchParams} searchParams 837 | * @param {string} accessToken 838 | * @param {string} base_url 839 | */ 840 | function buildGraphAPIURL(path, searchParams, accessToken, base_url) { 841 | const url = new URL(path, base_url ?? GRAPH_API_BASE_URL); 842 | 843 | url.search = new URLSearchParams(searchParams); 844 | if (accessToken) { 845 | url.searchParams.append(PARAMS__ACCESS_TOKEN, accessToken); 846 | } 847 | 848 | return url.toString(); 849 | } 850 | /** 851 | * @param {Request} req 852 | */ 853 | function useInitialAuthenticationValues(req) { 854 | // Use initial values 855 | req.session.access_token = initial_access_token; 856 | req.session.user_id = initial_user_id; 857 | // Clear initial values to enable signing out 858 | initial_access_token = undefined; 859 | initial_user_id = undefined; 860 | } 861 | 862 | /** 863 | * @param {{ value?: number, values: { value: number }[] }[]} metrics 864 | * @param {*} index 865 | */ 866 | function getInsightsValue(metrics, index) { 867 | if (metrics[index]) { 868 | metrics[index].value = metrics[index].values?.[0]?.value; 869 | } 870 | } 871 | 872 | /** 873 | * @param {{ value?: number, total_value: { value: number } }[]} metrics 874 | * @param {number} index 875 | */ 876 | function getInsightsTotalValue(metrics, index) { 877 | if (metrics[index]) { 878 | metrics[index].value = metrics[index].total_value?.value; 879 | } 880 | } 881 | 882 | /** 883 | * @param {object} target 884 | * @param {string} attachmentType 885 | * @param {string} url 886 | */ 887 | function addAttachmentFields(target, attachmentType, url, altText) { 888 | if (attachmentType === 'Image') { 889 | target.media_type = MEDIA_TYPE__IMAGE; 890 | target.image_url = url; 891 | target.alt_text = altText; 892 | } else if (attachmentType === 'Video') { 893 | target.media_type = MEDIA_TYPE__VIDEO; 894 | target.video_url = url; 895 | target.alt_text = altText; 896 | } 897 | } 898 | 899 | /** 900 | * @param {URL} sourceUrl 901 | * @param {URL} destinationUrl 902 | * @param {string} paramName 903 | */ 904 | function setUrlParamIfPresent(sourceUrl, destinationUrl, paramName) { 905 | const paramValue = sourceUrl.searchParams.get(paramName); 906 | if (paramValue) { 907 | destinationUrl.searchParams.set(paramName, paramValue); 908 | } 909 | } 910 | 911 | /** 912 | * @param {Request} req 913 | * @param {string} graphApiPagingUrl 914 | */ 915 | function getCursorUrlFromGraphApiPagingUrl(req, graphApiPagingUrl) { 916 | const graphUrl = new URL(graphApiPagingUrl); 917 | 918 | const cursorUrl = new URL(req.protocol + '://' + req.get('host') + req.originalUrl); 919 | cursorUrl.search = ''; 920 | 921 | setUrlParamIfPresent(graphUrl, cursorUrl, 'limit'); 922 | setUrlParamIfPresent(graphUrl, cursorUrl, 'before'); 923 | setUrlParamIfPresent(graphUrl, cursorUrl, 'after'); 924 | 925 | return cursorUrl.href; 926 | } 927 | 928 | /** 929 | * @param {Request} req 930 | * @param {Response} res 931 | * @param {boolean} [isTopLevel] 932 | */ 933 | async function showReplies(req, res, isTopLevel) { 934 | const { threadId } = req.params; 935 | const { username, before, after, limit } = req.query; 936 | 937 | const params = { 938 | [PARAMS__FIELDS]: [ 939 | FIELD__TEXT, 940 | FIELD__MEDIA_TYPE, 941 | FIELD__MEDIA_URL, 942 | FIELD__PERMALINK, 943 | FIELD__TIMESTAMP, 944 | FIELD__USERNAME, 945 | FIELD__HIDE_STATUS, 946 | FIELD__ALT_TEXT, 947 | ].join(','), 948 | limit: limit ?? DEFAULT_THREADS_QUERY_LIMIT, 949 | }; 950 | if (before) { 951 | params.before = before; 952 | } 953 | if (after) { 954 | params.after = after; 955 | } 956 | 957 | let replies = []; 958 | let paging = {}; 959 | 960 | const repliesOrConversation = isTopLevel ? 'replies' : 'conversation'; 961 | const queryThreadsUrl = buildGraphAPIURL(`${threadId}/${repliesOrConversation}`, params, req.session.access_token); 962 | 963 | try { 964 | const queryResponse = await axios.get(queryThreadsUrl, { httpsAgent: agent }); 965 | replies = queryResponse.data.data; 966 | 967 | if (queryResponse.data.paging) { 968 | const { next, previous } = queryResponse.data.paging; 969 | 970 | if (next) 971 | paging.nextUrl = getCursorUrlFromGraphApiPagingUrl(req, next); 972 | 973 | if (previous) 974 | paging.previousUrl = getCursorUrlFromGraphApiPagingUrl(req, previous); 975 | } 976 | } catch (e) { 977 | console.error(e?.response?.data?.error?.message ?? e.message); 978 | } 979 | 980 | res.render(isTopLevel ? 'thread_replies' : 'thread_conversation', { 981 | threadId, 982 | username, 983 | paging, 984 | replies, 985 | manage: isTopLevel ? true : false, 986 | title: 'Replies', 987 | }); 988 | } 989 | -------------------------------------------------------------------------------- /test_data/test-video-threads-bad-audio-bit-rate.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbsamples/threads_api/707f54a0c6d50932f1f3a0b0a34c7771022e79c4/test_data/test-video-threads-bad-audio-bit-rate.mp4 -------------------------------------------------------------------------------- /test_data/test-video-threads-bad-frame-rate.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbsamples/threads_api/707f54a0c6d50932f1f3a0b0a34c7771022e79c4/test_data/test-video-threads-bad-frame-rate.mp4 -------------------------------------------------------------------------------- /test_data/test-video-threads-bad-video-bit-rate.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbsamples/threads_api/707f54a0c6d50932f1f3a0b0a34c7771022e79c4/test_data/test-video-threads-bad-video-bit-rate.mp4 -------------------------------------------------------------------------------- /test_data/test-video-threads.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbsamples/threads_api/707f54a0c6d50932f1f3a0b0a34c7771022e79c4/test_data/test-video-threads.mp4 -------------------------------------------------------------------------------- /views/account.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | div 5 | .profile-picture 6 | img(src=threads_profile_picture_url alt='User\'s profile picture' width=36 height=36) 7 | .username 8 | span 9 | a(href=user_profile_url target='_blank') @#{username} 10 | .user-bio 11 | p #{threads_biography} 12 | 13 | .button-group 14 | button(onclick="location.href='/upload'") Publish 15 | button(onclick="location.href='/threads'") My Threads 16 | button(onclick="location.href='/replies'") My Replies 17 | button(onclick="location.href='/mentions'") My Mentions 18 | button(onclick="location.href='/keywordSearch'") Search for Threads 19 | button(onclick="location.href='/userInsights'") My Insights 20 | button(onclick="location.href='/publishingLimit'") Publishing Limit 21 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | style(type="text/css"). 5 | #oembed { 6 | margin-top: 40px; 7 | } 8 | .button-group 9 | button(onclick="location.href='/login'") Log In 10 | button(onclick="location.href='/logout'") Log Out 11 | 12 | button#oembed(onclick="location.href='/oEmbed'") Embed Threads 13 | -------------------------------------------------------------------------------- /views/insights.pug: -------------------------------------------------------------------------------- 1 | table 2 | thead 3 | tr 4 | th Metric 5 | th Value 6 | th Description 7 | th Id 8 | tbody 9 | each metric in metrics 10 | tr 11 | td #{metric.name} 12 | td #{metric.value} 13 | td #{metric.description} 14 | td #{metric.id} 15 | -------------------------------------------------------------------------------- /views/keyword_search.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | form(action='/keywordSearch' id='form' method='GET') 5 | textarea(placeholder='Enter a search query' name='keyword' autocomplete='off') 6 | 7 | label(for="search-type") Search for top or recent Threads? 8 | select#search-type(name='searchType') 9 | option(value="TOP" selected) Top 10 | option(value="RECENT") Recent 11 | 12 | input(type='submit' value='Search') 13 | 14 | if threads 15 | h2=resultsTitle 16 | table.threads-list 17 | thead 18 | tr 19 | th Username 20 | th ID 21 | th Created On 22 | th Media Type 23 | th Text 24 | th Permalink 25 | th Reply Audience 26 | tbody 27 | each thread in threads 28 | tr.threads-list-item 29 | td.thread-username=thread.username 30 | td.thread-id 31 | a(href=`/threads/${thread.id}`)=thread.id 32 | td.thread-timestamp=thread.timestamp 33 | td.thread-type=thread.media_type 34 | td.thread-text=thread.text 35 | td.thread-permalink 36 | a(href=thread.permalink target='_blank') View on Threads 37 | td.thread-reply-audience=thread.reply_audience 38 | 39 | div.paging 40 | if paging.nextUrl 41 | div.paging-next 42 | a(href=paging.nextUrl) Next 43 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype 2 | html 3 | head 4 | title Threads API Sample App - #{title} 5 | link(rel='stylesheet', href='/css/style.css') 6 | body 7 | header 8 | h1= title 9 | 10 | block content 11 | 12 | div#loadingOverlay(style='display: none; position: fixed; top: 0; left: 0; height: 100vh; width: 100%; background-color: rgba(255,255,255,0.8); z-index: 9999;') 13 | div(style='position: relative; top: 50%; left: 50%; transform: translate(-50%, -50%);') 14 | img(src='/img/loading.gif' alt='Loading...') 15 | 16 | 17 | if (message) 18 | .alert 19 | span.closebtn(onclick="this.parentElement.style.display='none';") × 20 | | #{message} 21 | 22 | if(error) 23 | .alert-error 24 | span.closebtn(onclick="this.parentElement.style.display='none';") × 25 | | Error Occured - #{error}! 26 | 27 | block scripts -------------------------------------------------------------------------------- /views/layout_with_account.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | append content 4 | style(type="text/css"). 5 | a.account-link { 6 | position: absolute; 7 | left: 10px; 8 | top: 10px; 9 | } 10 | 11 | a.account-link(href='/account') Go to Account 12 | 13 | block content 14 | -------------------------------------------------------------------------------- /views/mentions.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | table.threads-list 5 | thead 6 | tr 7 | th Username 8 | th ID 9 | th Created On 10 | th Media Type 11 | th Text 12 | th Permalink 13 | th Reply Audience 14 | tbody 15 | each thread in threads 16 | tr.threads-list-item 17 | td.thread-username=thread.username 18 | td.thread-id 19 | a(href=`/threads/${thread.id}`)=thread.id 20 | td.thread-timestamp=thread.timestamp 21 | td.thread-type=thread.media_type 22 | td.thread-text=thread.text 23 | td.thread-permalink 24 | a(href=thread.permalink target='_blank') View on Threads 25 | td.thread-reply-audience=thread.reply_audience 26 | 27 | div.paging 28 | if paging.nextUrl 29 | div.paging-next 30 | a(href=paging.nextUrl) Next 31 | 32 | if paging.previousUrl 33 | div.paging-previous 34 | a(href=paging.previousUrl) Previous 35 | -------------------------------------------------------------------------------- /views/oembed.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | style(type="text/css"). 5 | #example-accounts { 6 | margin: 0 auto; 7 | width: 100px; 8 | text-align: left; 9 | } 10 | form.oembed { 11 | margin-top: 50px; 12 | } 13 | 14 | p Try embedding a post from one of these accounts: 15 | ul#example-accounts 16 | li 17 | a(href='https://www.threads.net/@meta') @meta 18 | li 19 | a(href='https://www.threads.net/@threads') @threads 20 | li 21 | a(href='https://www.threads.net/@instagram') @instagram 22 | li 23 | a(href='https://www.threads.net/@facebook') @facebook 24 | 25 | form.oembed(action='/oEmbed' id='form' method='GET') 26 | input(type='text' placeholder='Enter a URL of a Threads post to embed' name='url' autocomplete='off') 27 | 28 | input(type='submit' value='Embed') 29 | 30 | if html 31 | div !{html} 32 | -------------------------------------------------------------------------------- /views/publish.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | p Your container was successfully created. 5 | p You will be able to publish it once it finishes processing the content. 6 | 7 | div 8 | p Current status:  9 | span#container-status Unknown 10 | 11 | div#status-explanation 12 | 13 | p#error-message 14 | 15 | form#form(action='/publish' method='POST') 16 | input#container-id(type='hidden' name='containerId' value=containerId) 17 | input#submit.threads-button(type='submit' value='Publish' disabled='disabled') 18 | 19 | template#template-status-FINISHED 20 | p All set! You can publish your thread now. 🧵 21 | template#template-status-IN_PROGRESS 22 | p Your content is still processing. The status will be refreshed automatically. ⏳ 23 | template#template-status-ERROR 24 | p Publish failed. ❌ 25 | 26 | block scripts 27 | script(src='/scripts/form.js' type='text/javascript') 28 | script(src='/scripts/publish.js', type='text/javascript') 29 | -------------------------------------------------------------------------------- /views/publishing_limit.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | table 5 | tbody 6 | tr 7 | th(colspan=2) Quota Usage 8 | td(colspan=2)=quotaUsage 9 | tr 10 | th(rowspan=2) Config 11 | th Quota Total 12 | td=config.quota_total 13 | tr 14 | th Quota Duration 15 | td=config.quota_duration 16 | tr 17 | th(colspan=2) Reply Quota Usage 18 | td(colspan=2)=replyQuotaUsage 19 | tr 20 | th(rowspan=2) Reply Config 21 | th Reply Quota Total 22 | td=replyConfig.quota_total 23 | tr 24 | th Reply Quota Duration 25 | td=replyConfig.quota_duration 26 | -------------------------------------------------------------------------------- /views/thread.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | table 5 | thead 6 | tr 7 | th Field 8 | th Value 9 | tbody 10 | tr 11 | td Media Type 12 | td #{media_type} 13 | tr 14 | td Text 15 | td #{text} 16 | tr 17 | td Created On 18 | td #{timestamp} 19 | tr 20 | td Media URL 21 | td 22 | a(href=media_url target='_blank') #{media_url} 23 | tr 24 | td Alt Text 25 | td #{alt_text} 26 | tr 27 | td Link Attachment URL 28 | td #{link_attachment_url} 29 | tr 30 | td Permalink 31 | td 32 | a(href=permalink target='_blank') #{permalink} 33 | tr 34 | td Replies 35 | td 36 | a(href=`/threads/${threadId}/replies?username=${username}`) View Direct Replies 37 | br 38 | if (!is_reply) 39 | a(href=`/threads/${threadId}/conversation?username=${username}`) View Conversation 40 | tr 41 | td Reply Audience 42 | td #{reply_audience} 43 | tr 44 | td Insights 45 | td 46 | a(href=`/threads/${threadId}/insights`) View Insights 47 | tr 48 | td Quote 49 | td 50 | button(onclick=`location.href='/upload?quotePostId=${threadId}'`) Quote 51 | tr 52 | td Repost 53 | td 54 | form(action='/repost' method='post') 55 | input(type='hidden' name='repostId' value=threadId) 56 | button(type='submit') Repost 57 | -------------------------------------------------------------------------------- /views/thread_conversation.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | p Conversation for thread #{threadId} 5 | 6 | include thread_replies_layout.pug 7 | -------------------------------------------------------------------------------- /views/thread_insights.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | include insights 5 | br 6 | form.insights(action=`/threads/${threadId}/insights` method='GET') 7 | span 8 | label(for='since') Since 9 | input#since(type='date' name='since' value=since) 10 | span 11 | label(for='until') Until 12 | input#until(type='date' name='until' value=until) 13 | input#submit.threads-button(type='submit' value='Refine') 14 | -------------------------------------------------------------------------------- /views/thread_replies.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | p Top-level replies to thread #{threadId} 5 | 6 | include thread_replies_layout.pug 7 | 8 | block scripts 9 | script(src='/scripts/form.js' type='text/javascript') 10 | script(src='/scripts/hide-reply.js', type='text/javascript') 11 | -------------------------------------------------------------------------------- /views/thread_replies_layout.pug: -------------------------------------------------------------------------------- 1 | table.thread-replies 2 | thead 3 | tr 4 | th ID 5 | th Created On 6 | th Media Type 7 | th Text 8 | th Permalink 9 | if manage 10 | th Manage 11 | th Reply 12 | tbody 13 | each reply in replies 14 | tr.thread-replies-list-item 15 | td.reply-id 16 | a(href=`/threads/${reply.id}`)=reply.id 17 | td.reply-timestamp=reply.timestamp 18 | td.reply-type=reply.media_type 19 | td.reply-text=reply.text 20 | td.reply-permalink 21 | a(href=reply.permalink target='_blank') View on Threads 22 | if manage 23 | td.manage-reply 24 | if username !== reply.username 25 | form.hide-reply(action=`/manage_reply/${reply.id}?hide=${reply.hide_status!=='HIDDEN'}&username=${username}`, method='POST') 26 | input(type='submit', value=`${reply.hide_status==='HIDDEN' ? 'Unhide' : 'Hide'}`) 27 | else 28 | p(title='Cannot hide your own replies.') ⓘ 29 | td 30 | button(onclick=`location.href='/upload?replyToId=${reply.id}'`) Reply 31 | 32 | 33 | 34 | div.paging 35 | if paging.nextUrl 36 | div.paging-next 37 | a(href=paging.nextUrl) Next 38 | 39 | if paging.previousUrl 40 | div.paging-previous 41 | a(href=paging.previousUrl) Previous 42 | -------------------------------------------------------------------------------- /views/threads.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | table.threads-list 5 | thead 6 | tr 7 | th ID 8 | th Created On 9 | th Media Type 10 | th Text 11 | th Permalink 12 | th Reply Audience 13 | tbody 14 | each thread in threads 15 | tr.threads-list-item 16 | td.thread-id 17 | a(href=`/threads/${thread.id}`)=thread.id 18 | td.thread-timestamp=thread.timestamp 19 | td.thread-type=thread.media_type 20 | td.thread-text=thread.text 21 | td.thread-permalink 22 | a(href=thread.permalink target='_blank') View on Threads 23 | td.thread-reply-audience=thread.reply_audience 24 | 25 | div.paging 26 | if paging.nextUrl 27 | div.paging-next 28 | a(href=paging.nextUrl) Next 29 | 30 | if paging.previousUrl 31 | div.paging-previous 32 | a(href=paging.previousUrl) Previous 33 | -------------------------------------------------------------------------------- /views/upload.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | style(type="text/css"). 5 | div.attachments-area { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | flex-direction: column; 10 | } 11 | div.attachment-controls { 12 | height: 100%; 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: center; 16 | } 17 | div.attachment-item { 18 | display: flex; 19 | flex-direction: row; 20 | justify-content: center; 21 | margin-bottom: 12px; 22 | } 23 | div.attachment-item-details { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | div.attachment-item-type-and-url { 28 | display: flex; 29 | flex-direction: row; 30 | } 31 | textarea::placeholder { 32 | text-align: center; 33 | line-height: 15px; 34 | } 35 | textarea { 36 | text-align: center; 37 | padding: 5px 0; 38 | } 39 | img { 40 | margin-bottom: 15px; 41 | } 42 | #reply-control { 43 | margin-bottom: 15px; 44 | } 45 | #link-attachment { 46 | width: 200px; 47 | margin-bottom: 15px; 48 | } 49 | #quote-post-id { 50 | width: 200px; 51 | } 52 | 53 | if (replyToId !== undefined) 54 | p(style='color: gray') Replying to #{replyToId} 55 | 56 | form(action='/upload' id='form' method='POST') 57 | label(for='text') 58 | | Text: 59 | textarea(placeholder='Start a thread...' id='text' name='text' autocomplete='off') 60 | br 61 | | To attach an image or video, click the image below 62 | div.attachments-area 63 | img#attachments-button(src='/img/attachment.png') 64 | div.attachment-controls 65 | div#attachments-list.attachments 66 | 67 | label(for="reply-control") Who Can Reply 68 | select#reply-control(name='replyControl' hint="Reply Control") 69 | option(value="" selected) 70 | option(value="everyone") Everyone 71 | option(value="accounts_you_follow") Accounts You Follow 72 | option(value="mentioned_only") Mentioned Only 73 | 74 | label(for='linkAttachment') 75 | | Link Attachment 76 | input#link-attachment(type='text' name='linkAttachment' value='') 77 | 78 | if quotePostId 79 | a(href=`/threads/${quotePostId}`) Quoting #{quotePostId} 80 | input#quote-post-id(type='hidden' name='quotePostId' value=quotePostId) 81 | 82 | input(type='hidden' name='replyToId' value=replyToId) 83 | input.threads-button(type='submit' id='submit' value='Post') 84 | 85 | p#media-type-explanation Media Type:  86 | span#media-type 87 | 88 | script#attachment-template(type='text/template') 89 | div.attachment-item 90 | div.attachment-item-details 91 | div.attachment-item-type-and-url 92 | label(for='attachmentType') 93 | | Type    94 | select(name='attachmentType[]') 95 | option(value='Image') Image 96 | option(value='Video') Video 97 | label(for='attachmentUrl') 98 | | URL    99 | input(type='text' name='attachmentUrl[]' autocomplete='off') 100 | div.attachment-item-alt-text 101 | label(for='attachmentAltText') 102 | | Alt Text    103 | input(type='text' name='attachmentAltText[]' value='') 104 | 105 | // The parent of this node will be removed from the DOM 106 | span.delete ❌ 107 | br 108 | 109 | block scripts 110 | script(src='/scripts/form.js' type='text/javascript') 111 | script(src='/scripts/upload.js', type='text/javascript') 112 | -------------------------------------------------------------------------------- /views/user_insights.pug: -------------------------------------------------------------------------------- 1 | extends layout_with_account 2 | 3 | block content 4 | include insights 5 | br 6 | form.insights(action='/userInsights' method='GET') 7 | span 8 | label(for='since') Since 9 | input#since(type='date' name='since' value=since) 10 | span 11 | label(for='until') Until 12 | input#until(type='date' name='until' value=until) 13 | input#submit.threads-button(type='submit' value='Refine') 14 | --------------------------------------------------------------------------------