Our next job is to enable OAuth 2.0 for the app. Head to the OAuth tab in your app's settings and
enable OAuth 2.0 via the toggle at the top of the page.
Next, configure the redirect URIs for your app. For our tutorial, we'll use the following URL:
```
http://localhost:3050/integrations/attio/callback
```
Of course, for a real app, you'd also include a publicly available URL such as
`https://my-app.com/integrations/attio/callback`.
Lastly, we need to configure the app's scopes. Heads to the scopes tab to enable these. For our
demonstration app, we'll set tasks, user management, object configuration and records to "read" so
we can fetch a list of tasks and which users they are assigned to.
Make a new directory and setup your new Node.js project inside it:
```bash theme={"system"}
mkdir my-app
cd my-app
npm init -y
npm install express dotenv sqlite3 bcrypt express-session
```
Create a new file called `server.js` and add the following code:
```js theme={"system"}
require("dotenv").config()
const express = require("express")
const session = require("express-session")
const sqlite3 = require("sqlite3").verbose()
const bcrypt = require("bcrypt")
const app = express()
const PORT = 3050
// NOTE: The following code is heavily simplified for educational purposes and should not be copied
// for production use without careful consideration of security implications.
// 1) Setup a database
// Use ":memory:" for a temporary database in RAM. A real app should use a persistent database.
const db = new sqlite3.Database(":memory:")
// Seed a "users" table if it doesn't exist
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
passwordHash TEXT NOT NULL
)
`)
// 2) Middleware
// Enable parsing of form data
app.use(express.urlencoded({extended: false}))
// Setup session middleware
app.use(
session({
secret: "mysecret", // Replace with a strong secret in production
resave: false,
saveUninitialized: false,
})
)
// 3) Page routes
app.get("/", (req, res) => {
if (req.session.userId) {
return res.send(`
Welcome to Taskr!
You are logged in as ${req.session.username}.
Logout
`)
} else {
return res.send(`
You are not logged in
Sign Up
`)
}
})
app.get("/signup", (req, res) => {
if (req.session.userId) {
return res.redirect("/")
}
res.send(`
Sign Up
`)
})
// 4) Endpoint routes
app.post("/signup", async (req, res) => {
const {username, password} = req.body
try {
const passwordHash = await bcrypt.hash(password, 10) // Hash the password for secure storage
// Create a new user in the database
db.run(
`INSERT INTO users (username, passwordHash) VALUES (?, ?)`,
[username, passwordHash],
function (err) {
if (err) {
// If username is taken, sqlite typically throws a UNIQUE constraint error
if (err.message.includes("UNIQUE constraint failed")) {
return res.send(`
Username already taken
Try another username
`)
}
return res.send("An error occurred. Please try again.")
}
// If insert succeeds, log the user in
req.session.userId = this.lastID // ID of the newly created user
req.session.username = username
// Show the home page
return res.redirect("/")
}
)
} catch (error) {
console.error("Error hashing password:", error)
res.send("An error occurred. Please try again.")
}
})
app.get("/logout", (req, res) => {
req.session.destroy(() => {
res.redirect("/")
})
})
// 4) Start the server
app.listen(PORT, () => {
console.log(`App running at http://localhost:${PORT}`)
})
```
You should now be able to run your app from the command line and visit it in your browser at
`http://localhost:3050`:
```bash theme={"system"}
node server.js
```
Run through the signup flow to ensure everything works as expected.
To add support for OAuth, we need to ensure that our Node.js code has access to the OAuth client ID
and client secret.
Create a new file called `.env` and add your app's client ID and client secret. You can find these
in the OAuth tab in your app's settings.
```
ATTIO_CLIENT_ID=your-client-id
ATTIO_CLIENT_SECRET=your-client-secret
```
When we complete the OAuth flow, we'll need a place to store the OAuth access token for each user.
Modify the code that creates the `users` table as follows:
```js theme={"system"}
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
passwordHash TEXT NOT NULL,
attio_access_token TEXT
)
`)
```
In a real app, you should encrypt these values before storing them. Access tokens are highly
sensitive data and should be stored securely.
Next, we need to implement the OAuth flow itself. An OAuth flow consists of the following steps:
1. Redirect to Attio's OAuth authorization page when prompted by the user
2. Handle the redirect back from Attio
3. Exchange the authorization code for an access token
4. Persist the access token
5. Make API requests using the access token
```mermaid theme={"system"}
sequenceDiagram
participant User
participant Your App
participant Attio
User->>Your App: Click "Connect to Attio"
Your App->>Attio: Redirect to OAuth authorization page
Attio->>Your App: Redirect to app with authorization code
Your App->>Attio: Exchange authorization code for access token securely
Attio->>Your App: Return access token
Your App->>Your App: Store access token
Your App->>Attio: Make API request with access token
```
We'll start by adding a new route to our app that redirects the user to the OAuth authorization
page.
```js theme={"system"}
const crypto = require("crypto")
app.get("/integrations/attio/connect", (req, res) => {
if (!req.session.userId) {
return res.redirect("/signup") // Must be logged in
}
// Generate a secure random state parameter and store it in the user's session
const state = crypto.randomBytes(16).toString("hex")
req.session.oauthState = state
const authUrl = `https://app.attio.com/authorize?response_type=code&client_id=${process.env.ATTIO_CLIENT_ID}&redirect_uri=http://localhost:3050/integrations/attio/callback&state=${state}`
res.redirect(authUrl)
})
```
A second route will handle the redirect back from Attio.
```js theme={"system"}
const ATTIO_TOKEN_URL = "https://app.attio.com/oauth/token"
app.get("/integrations/attio/callback", async (req, res) => {
if (!req.session.userId) {
return res.redirect("/signup")
}
const {code, state} = req.query
// Verify the state parameter
if (!state || state !== req.session.oauthState) {
return res.status(403).send("Invalid state parameter. Possible CSRF attack detected.")
}
if (!code) {
return res.status(400).send("Missing authorization code from Attio")
}
try {
// Exchange authorization code for an access token using fetch
const tokenResponse = await fetch(ATTIO_TOKEN_URL, {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: "http://localhost:3050/integrations/attio/callback",
client_id: process.env.ATTIO_CLIENT_ID,
client_secret: process.env.ATTIO_CLIENT_SECRET,
}),
})
if (!tokenResponse.ok) {
console.error("Attio token exchange failed with status:", tokenResponse.status)
return res.status(500).send("Error exchanging code for token")
}
const tokenData = await tokenResponse.json()
const attioAccessToken = tokenData.access_token
// Update the user's record with the access token
db.run(
`UPDATE users SET attio_access_token = ? WHERE id = ?`,
[attioAccessToken, req.session.userId],
(err) => {
if (err) {
console.error("Failed to store Attio token in DB:", err)
return res.status(500).send("Database error storing Attio token")
}
// Redirect back to home
res.redirect("/")
}
)
} catch (err) {
console.error("Error fetching token from Attio:", err)
res.status(500).send("Internal error")
}
})
```
Last, we need to ensure the user can navigate to the start of this flow. Let's add a button to the
home page that redirects to the `/integrations/attio/connect` route.
```js theme={"system"}
app.get("/", async (req, res) => {
if (req.session.userId) {
await db.get("SELECT * FROM users WHERE id = ?", [req.session.userId], async (err, user) => {
if (err) {
console.error("Error fetching users:", err)
return res.status(500).send("Error fetching users")
}
const hasAttioConnection = user !== null && user.attio_access_token !== null
return res.send(`
Welcome to Taskr!
You are logged in as ${req.session.username}.
Logout
${
hasAttioConnection
? `Todo: render tasks
`
: `Connect Attio
`
}
`)
})
} else {
// ...
}
})
```
Please note, the example above stores a raw access token in the database. The access tokens that
we grant to your app are highly sensitive data and should be stored securely. Please ensure any
production apps you build encrypt the token before storing it.
Now we have a token, all that remains is to make a request to the Attio API and render the results.
To make a request to the Attio API, we need to call the right endpoint and pass in our new oauth
token in the `Authorization` header like so.
```js theme={"system"}
app.get("/", async (req, res) => {
if (req.session.userId) {
await db.get("SELECT * FROM users WHERE id = ?", [req.session.userId], async (err, user) => {
if (err) {
console.error("Error fetching users:", err)
return res.status(500).send("Error fetching users")
}
const hasAttioConnection = user !== null && user.attio_access_token !== null
if (hasAttioConnection) {
const fetchResult = await fetch(`https://api.attio.com/v2/tasks?limit=10`, {
headers: {
Authorization: `Bearer ${user.attio_access_token}`, // Pass in the token here
},
})
const data = await fetchResult.json()
const taskItems = data.data.map((task) => {
return `${task.content_plaintext}`
})
const taskList = ``
return res.send(`
Welcome to Taskr!
You are logged in as ${req.session.username}.
Logout
${taskList}
`)
}
return res.send(`
Welcome to Taskr!
You are logged in as ${req.session.username}.
Logout
Connect Attio
`)
})
} else {
return res.send(`
You are not logged in
Sign Up
`)
}
})
```
All that remains is to spin up your app and test it out!
Run your app from the command line and visit it in your browser at `http://localhost:3050`.
```bash theme={"system"}
node server.js
```
# Call recording created
Source: https://docs.attio.com/rest-api/webhook-reference/call-recording-events/call-recordingcreated
https://api.attio.com/openapi/webhooks webhook call-recording.created
This event fires after a call recording has finished and its media upload is complete.
# Comment created
Source: https://docs.attio.com/rest-api/webhook-reference/comment-events/commentcreated
https://api.attio.com/openapi/webhooks webhook comment.created
This event is fired whenever a comment is created.
# Comment deleted
Source: https://docs.attio.com/rest-api/webhook-reference/comment-events/commentdeleted
https://api.attio.com/openapi/webhooks webhook comment.deleted
This event is fired whenever a comment is deleted.
# Comment resolved
Source: https://docs.attio.com/rest-api/webhook-reference/comment-events/commentresolved
https://api.attio.com/openapi/webhooks webhook comment.resolved
This event is fired whenever a comment is resolved.
# Comment unresolved
Source: https://docs.attio.com/rest-api/webhook-reference/comment-events/commentunresolved
https://api.attio.com/openapi/webhooks webhook comment.unresolved
This event is fired whenever a comment is un-resolved.
# List attribute created
Source: https://docs.attio.com/rest-api/webhook-reference/list-attribute-events/list-attributecreated
https://api.attio.com/openapi/webhooks webhook list-attribute.created
This event is fired whenever a list attribute is created (e.g. adding an "Owner" attribute).
# List attribute updated
Source: https://docs.attio.com/rest-api/webhook-reference/list-attribute-events/list-attributeupdated
https://api.attio.com/openapi/webhooks webhook list-attribute.updated
This event is fired whenever a list attribute is updated (e.g. when changing the name of the "Owner" attribute to "Proprietor").
# List entry created
Source: https://docs.attio.com/rest-api/webhook-reference/list-entry-events/list-entrycreated
https://api.attio.com/openapi/webhooks webhook list-entry.created
This event is fired whenever a list entry is created (i.e. when a record is added to a list).
# List entry deleted
Source: https://docs.attio.com/rest-api/webhook-reference/list-entry-events/list-entrydeleted
https://api.attio.com/openapi/webhooks webhook list-entry.deleted
This event is fired whenever a list entry is deleted (i.e. when a record is removed from a list).
# List entry updated
Source: https://docs.attio.com/rest-api/webhook-reference/list-entry-events/list-entryupdated
https://api.attio.com/openapi/webhooks webhook list-entry.updated
This event is fired whenever an existing list entry is updated (i.e. when a list attribute is changed for a specific list entry, e.g. when setting "Owner").
# List created
Source: https://docs.attio.com/rest-api/webhook-reference/list-events/listcreated
https://api.attio.com/openapi/webhooks webhook list.created
This event is fired whenever a list is created.
# List deleted
Source: https://docs.attio.com/rest-api/webhook-reference/list-events/listdeleted
https://api.attio.com/openapi/webhooks webhook list.deleted
This event is fired whenever a list is deleted.
# List updated
Source: https://docs.attio.com/rest-api/webhook-reference/list-events/listupdated
https://api.attio.com/openapi/webhooks webhook list.updated
This event is fired whenever a list is updated (e.g. when changing the name or icon of the list).
# Note content updated
Source: https://docs.attio.com/rest-api/webhook-reference/note-content-events/note-contentupdated
https://api.attio.com/openapi/webhooks webhook note-content.updated
This event is fired whenever the content (body) of a note is updated. The `parent_object_id` refers to the object that the note references (e.g. the person object), and the `parent_record_id` refers to the record that the note references.
# Note created
Source: https://docs.attio.com/rest-api/webhook-reference/note-events/notecreated
https://api.attio.com/openapi/webhooks webhook note.created
This event is fired whenever a note is created. The `parent_object_id` refers to the object that the note references (e.g. the person object), and the `parent_record_id` refers to the record that the note references.
# Note deleted
Source: https://docs.attio.com/rest-api/webhook-reference/note-events/notedeleted
https://api.attio.com/openapi/webhooks webhook note.deleted
This event is fired whenever a note is deleted.
# Note updated
Source: https://docs.attio.com/rest-api/webhook-reference/note-events/noteupdated
https://api.attio.com/openapi/webhooks webhook note.updated
This event is fired whenever the title of a note is modified. Body updates do not currently trigger webhooks.
# Object attribute created
Source: https://docs.attio.com/rest-api/webhook-reference/object-attribute-events/object-attributecreated
https://api.attio.com/openapi/webhooks webhook object-attribute.created
This event is fired whenever an object attribute is created (e.g. when defining a new attribute "Rating" on the company object).
# Object attribute updated
Source: https://docs.attio.com/rest-api/webhook-reference/object-attribute-events/object-attributeupdated
https://api.attio.com/openapi/webhooks webhook object-attribute.updated
This event is fired whenever an object attribute is updated (e.g. when renaming the "Rating" attribute to "Score" on the company object).
# Record created
Source: https://docs.attio.com/rest-api/webhook-reference/record-events/recordcreated
https://api.attio.com/openapi/webhooks webhook record.created
This event is fired whenever a record is created.
# Record deleted
Source: https://docs.attio.com/rest-api/webhook-reference/record-events/recorddeleted
https://api.attio.com/openapi/webhooks webhook record.deleted
This event is fired whenever a record is deleted.
# Record merged
Source: https://docs.attio.com/rest-api/webhook-reference/record-events/recordmerged
https://api.attio.com/openapi/webhooks webhook record.merged
This event is fired whenever two records are merged together. Merging copies properties from the "duplicate" record into the original record, so that the original record has the properties of both, and the duplicate record is deleted.
# Record updated
Source: https://docs.attio.com/rest-api/webhook-reference/record-events/recordupdated
https://api.attio.com/openapi/webhooks webhook record.updated
This event is fired whenever an attribute on a record is updated (e.g. changing the "name" field on a record).
# Task created
Source: https://docs.attio.com/rest-api/webhook-reference/task-events/taskcreated
https://api.attio.com/openapi/webhooks webhook task.created
This event is fired whenever a task is created.
# Task deleted
Source: https://docs.attio.com/rest-api/webhook-reference/task-events/taskdeleted
https://api.attio.com/openapi/webhooks webhook task.deleted
This event is fired whenever a task is deleted.
# Task updated
Source: https://docs.attio.com/rest-api/webhook-reference/task-events/taskupdated
https://api.attio.com/openapi/webhooks webhook task.updated
This event is fired whenever a task is updated (e.g. the assignees or deadline are changed).
# Workspace member created
Source: https://docs.attio.com/rest-api/webhook-reference/workspace-member-events/workspace-membercreated
https://api.attio.com/openapi/webhooks webhook workspace-member.created
This event is fired whenever a workspace member is added to the workspace.
#