Skip to content

Commit

Permalink
docs(api): 📝 Simplified API endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Apr 15, 2022
1 parent 281fddc commit 29254f6
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 40 deletions.
40 changes: 14 additions & 26 deletions apps/docs/docs/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,54 +45,42 @@ curl -i -X GET https://typebot.io/api/typebots \
}
```

### <Tag color="green">GET</Tag> /api/typebots/<Tag>typebotId</Tag>/webhookSteps
### <Tag color="green">GET</Tag> /api/typebots/<Tag>typebotId</Tag>/webhookBlocks

List webhook steps in a typebot. These are the steps you can, later on, register your Webhook URL:
List webhook blocks in a typebot. These are the blocks you can register a Webhook URL:

```bash title="Try it yourself"
curl -i -X GET https://typebot.io/api/typebots/$TYPEBOT_ID/webhookSteps \
curl -i -X GET https://typebot.io/api/typebots/$TYPEBOT_ID/webhookBlocks \
-H 'Authorization: Bearer ${TOKEN}'
```

```json title="Response 200 OK"
{
"steps": [
"blocks": [
{
"blockId": "blockId",
"id": "stepId",
"name": "Block #2 > stepId"
"name": "Group #2 > blockId",
"url": "https://my-webhook.com/webhook"
}
]
}
```

### <Tag color="green">GET</Tag> /api/typebots/<Tag>typebotId</Tag>/blocks/<Tag>blockId</Tag>/steps/<Tag>stepId</Tag>/sampleResult
### <Tag color="green">GET</Tag> /api/typebots/<Tag>typebotId</Tag>/blocks/<Tag>blockId</Tag>/sampleResult

Get a sample of what the webhook body will look like when triggered

```bash title="Try it yourself"
curl -i -X GET https://typebot.io/api/typebots/$TYPEBOT_ID/blocks/$BLOCK_ID/steps/$STEP_ID/sampleResult \
curl -i -X GET https://typebot.io/api/typebots/$TYPEBOT_ID/blocks/$BLOCK_ID/sampleResult \
-H 'Authorization: Bearer ${TOKEN}'
```

```json title="Response 200 OK"
{
"steps": [
{
"blockId": "blockId",
"id": "stepId",
"name": "Block #2 > stepId"
}
]
}
```

### <Tag color="orange">POST</Tag> /api/typebots/<Tag>typebotId</Tag>/blocks/<Tag>blockId</Tag>/steps/<Tag>stepId</Tag>/subscribeWebhook
### <Tag color="orange">POST</Tag> /api/typebots/<Tag>typebotId</Tag>/blocks/<Tag>blockId</Tag>/subscribeWebhook

Subscribe the step to a specified webhook URL
Subscribe the block to a specified webhook URL

```bash title="Try it yourself"
curl -i -X POST https://typebot.io/api/typebots/$TYPEBOT_ID/webhookSteps \
curl -i -X POST https://typebot.io/api/typebots/$TYPEBOT_ID/blocks/$BLOCK_ID/subscribeWebhook \
-H 'Authorization: Bearer ${TOKEN}'\
--header 'Content-Type: application/json' \
--data '{"url": "https://domain.com/my-webhook"}'
Expand All @@ -114,12 +102,12 @@ The url you want to subscribe to.

<hr />

### <Tag color="orange">POST</Tag> /api/typebots/<Tag>typebotId</Tag>/blocks/<Tag>blockId</Tag>/steps/<Tag>stepId</Tag>/unsubscribeWebhook
### <Tag color="orange">POST</Tag> /api/typebots/<Tag>typebotId</Tag>/blocks/<Tag>blockId</Tag>/unsubscribeWebhook

Unsubscribe the current webhook on step
Unsubscribe the current webhook on block

```bash title="Try it yourself"
curl -i -X POST https://typebot.io/api/typebots/$TYPEBOT_ID/webhookSteps \
curl -i -X POST https://typebot.io/api/typebots/$TYPEBOT_ID/blocks/$BLOCK_ID/unsubscribeWebhook \
-H 'Authorization: Bearer ${TOKEN}'\
```

Expand Down
4 changes: 3 additions & 1 deletion apps/docs/src/js/api-helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Taken from https://github.com/plausible/docs/blob/master/src/js/api-helpers.js 💙
import React from 'react'
import { useColorMode } from '@docusaurus/theme-common'

export const Required = () => (
<span
Expand Down Expand Up @@ -30,7 +31,8 @@ export const Optional = () => (
)

export const Tag = ({ children, color }) => {
let backgroundColor = '#CBD5E0'
const { isDarkTheme } = useColorMode()
let backgroundColor = isDarkTheme ? '#2d60b4' : '#CBD5E0'
switch (color) {
case 'green':
backgroundColor = '#68D391'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import prisma from 'libs/prisma'
import {
defaultWebhookAttributes,
HttpMethod,
KeyValue,
PublicTypebot,
ResultValues,
Typebot,
Variable,
Webhook,
WebhookOptions,
WebhookResponse,
WebhookStep,
} from 'models'
import { parseVariables } from 'bot-engine'
import { NextApiRequest, NextApiResponse } from 'next'
import got, { Method, Headers, HTTPError } from 'got'
import {
byId,
initMiddleware,
methodNotAllowed,
notFound,
parseAnswers,
} from 'utils'
import { stringify } from 'qs'
import { withSentry } from '@sentry/nextjs'
import Cors from 'cors'
import { parseSampleResult } from 'services/api/webhooks'

const cors = initMiddleware(Cors())
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res)
if (req.method === 'POST') {
const typebotId = req.query.typebotId.toString()
const stepId = req.query.blockId.toString()
const { resultValues, variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as {
resultValues: ResultValues | undefined
variables: Variable[]
}
const typebot = (await prisma.typebot.findUnique({
where: { id: typebotId },
include: { webhooks: true },
})) as unknown as (Typebot & { webhooks: Webhook[] }) | null
if (!typebot) return notFound(res)
const step = typebot.blocks
.flatMap((b) => b.steps)
.find(byId(stepId)) as WebhookStep
const webhook = typebot.webhooks.find(byId(step.webhookId))
if (!webhook)
return res
.status(404)
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
const preparedWebhook = prepareWebhookAttributes(webhook, step.options)
const result = await executeWebhook(typebot)(
preparedWebhook,
variables,
step.blockId,
resultValues
)
return res.status(200).send(result)
}
return methodNotAllowed(res)
}

const prepareWebhookAttributes = (
webhook: Webhook,
options: WebhookOptions
): Webhook => {
if (options.isAdvancedConfig === false) {
return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes }
} else if (options.isCustomBody === false) {
return { ...webhook, body: '{{state}}' }
}
return webhook
}

const executeWebhook =
(typebot: Typebot) =>
async (
webhook: Webhook,
variables: Variable[],
blockId: string,
resultValues?: ResultValues
): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method)
return {
statusCode: 400,
data: { message: `Webhook doesn't have url or method` },
}
const basicAuth: { username?: string; password?: string } = {}
const basicAuthHeaderIdx = webhook.headers.findIndex(
(h) =>
h.key?.toLowerCase() === 'authorization' &&
h.value?.toLowerCase()?.includes('basic')
)
const isUsernamePasswordBasicAuth =
basicAuthHeaderIdx !== -1 &&
webhook.headers[basicAuthHeaderIdx].value?.includes(':')
if (isUsernamePasswordBasicAuth) {
const [username, password] =
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
basicAuth.username = username
basicAuth.password = password
webhook.headers.splice(basicAuthHeaderIdx, 1)
}
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
| Headers
| undefined
const queryParams = stringify(
convertKeyValueTableToObject(webhook.queryParams, variables)
)
const contentType = headers ? headers['Content-Type'] : undefined
const body =
webhook.method !== HttpMethod.GET
? getBodyContent(typebot)({
body: webhook.body,
resultValues,
blockId,
})
: undefined
try {
const response = await got(
parseVariables(variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
),
{
method: webhook.method as Method,
headers,
...basicAuth,
json:
contentType !== 'x-www-form-urlencoded' && body
? JSON.parse(parseVariables(variables)(body))
: undefined,
form:
contentType === 'x-www-form-urlencoded' && body
? JSON.parse(parseVariables(variables)(body))
: undefined,
}
)
return {
statusCode: response.statusCode,
data: parseBody(response.body),
}
} catch (error) {
if (error instanceof HTTPError) {
return {
statusCode: error.response.statusCode,
data: parseBody(error.response.body as string),
}
}
console.error(error)
return {
statusCode: 500,
data: { message: `Error from Typebot server: ${error}` },
}
}
}

const getBodyContent =
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
({
body,
resultValues,
blockId,
}: {
body?: string | null
resultValues?: ResultValues
blockId: string
}): string | undefined => {
if (!body) return
return body === '{{state}}'
? JSON.stringify(
resultValues
? parseAnswers(typebot)(resultValues)
: parseSampleResult(typebot)(blockId)
)
: body
}

const parseBody = (body: string) => {
try {
return JSON.parse(body)
} catch (err) {
return body
}
}

const convertKeyValueTableToObject = (
keyValues: KeyValue[] | undefined,
variables: Variable[]
) => {
if (!keyValues) return
return keyValues.reduce((object, item) => {
if (!item.key) return {}
return {
...object,
[item.key]: parseVariables(variables)(item.value ?? ''),
}
}, {})
}

export default withSentry(handler)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import prisma from 'libs/prisma'
import { Typebot } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { authenticateUser } from 'services/api/utils'
import { parseSampleResult } from 'services/api/webhooks'
import { methodNotAllowed } from 'utils'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebotId = req.query.typebotId.toString()
const stepId = req.query.blockId.toString()
const typebot = (await prisma.typebot.findUnique({
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
const step = typebot.blocks
.flatMap((b) => b.steps)
.find((s) => s.id === stepId)
if (!step) return res.status(404).send({ message: 'Block not found' })
return res.send(parseSampleResult(typebot)(step.blockId))
}
methodNotAllowed(res)
}

export default handler
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { withSentry } from '@sentry/nextjs'
import prisma from 'libs/prisma'
import { Typebot, WebhookStep } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { authenticateUser } from 'services/api/utils'
import { byId, methodNotAllowed } from 'utils'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
const body = req.body as Record<string, string>
if (!('url' in body))
return res.status(403).send({ message: 'url is missing in body' })
const { url } = body
const typebotId = req.query.typebotId.toString()
const stepId = req.query.blockId.toString()
const typebot = (await prisma.typebot.findUnique({
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
const { webhookId } = typebot.blocks
.flatMap((b) => b.steps)
.find(byId(stepId)) as WebhookStep
await prisma.webhook.upsert({
where: { id: webhookId },
update: { url, body: '{{state}}', method: 'POST' },
create: { url, body: '{{state}}', method: 'POST', typebotId },
})

return res.send({ message: 'success' })
} catch (err) {
return res
.status(400)
.send({ message: "blockId doesn't point to a Webhook step" })
}
}
return methodNotAllowed(res)
}

export default withSentry(handler)
Loading

5 comments on commit 29254f6

@vercel
Copy link

@vercel vercel bot commented on 29254f6 Apr 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 29254f6 Apr 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 29254f6 Apr 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-git-main-typebot-io.vercel.app
builder-v2-typebot-io.vercel.app
app.typebot.io

@vercel
Copy link

@vercel vercel bot commented on 29254f6 Apr 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./apps/docs

docs-git-main-typebot-io.vercel.app
docs-typebot-io.vercel.app
docs.typebot.io

Please sign in to comment.