Introduction 👋
Our AI agent integrates with many popular customer support platforms out of the box, but if you have an in-house support tool or need more control, you can build your own integration using our RESTful HTTP API.
Our API is accessible at https://api.gradient-labs.ai
. There are client
libraries available for Python
and Go.
Request and response bodies are encoded as JSON, with timestamps encoded as RFC 3339 strings.
We also send webhooks to let you know when our agent wants to reply to the customer, hand-off to a human agent, or bring the conversation to a close.
Authentication 🔒
Requests must be authenticated by providing your API Key in the Authorization
header, in the form: Bearer <your api key>
.
curl https://api.gradient-labs.ai \
--header "Authorization: Bearer $api_key"
import os
from gradient_labs import Client
client = Client(os.environ["api_key"])
package main
import (
"log"
"os"
glabs "github.com/gradientlabs-ai/go-client"
)
func main() {
client, err := glabs.NewClient(
glabs.WithAPIKey(os.Getenv("api_key")),
)
if err != nil {
log.Fatal(err)
}
}
Idempotence 🔁
Unless stated otherwise, all endpoints are idempotent and requests can be safely retried.
Conversations 💬
Start a conversation
To begin a conversation, make a POST
request to the /conversations
endpoint.
curl https://api.gradient-labs.ai/conversations \
--request POST \
--header "Authorization: Bearer $api_key" \
--header 'Content-Type: application/json' \
--data @- \
<<BODY
{
"id": "conversation-1234",
"customer_id": "user-1234",
"channel": "web",
"created": "2023-10-23T17:24:50.804952Z"
"metadata": {
"chat_entrypoint": "home-page"
}
}
BODY
conv = client.start_conversation(
conversation_id="conversation-1234",
customer_id="user-1234",
channel="web",
metadata={
"chat_entrypoint": "home-page",
},
)
conv, err := client.StartConversation(ctx, glabs.StartConversationParams{
ID: "conversation-1234",
CustomerID: "user-1234",
Channel: glabs.ChannelWeb,
Metadata: map[string]string{"chat_entrypoint": "home-page"},
})
{
"id": "conversation-1234",
"customer_id": "user-1234",
"channel": "web",
"metadata": {
"chat_entrypoint": "home-page"
},
"created": "2023-10-23T17:24:50.804952Z",
"updated": "2023-10-23T17:24:50.804952Z",
"status": "active"
}
Body Parameters
Parameter | Type | Required | Description |
---|---|---|---|
id |
String | ✅ | Unique identifier for this conversation. Can be anything consisting of letters, numbers, or any of the following characters: _ - + = . Tip: use something meaningful to your business (e.g. a ticket number). |
customer_id |
String | ✅ | Unique identifier for the customer. Used to build historical context of conversations the agent has had with this customer. |
channel |
String | ✅ | Identifier for the way the customer is getting in touch: one of web or email . |
created |
Timestamp | The time at which the conversation started (defaults to the current time). | |
metadata |
Any | Arbitrary metadata to attach to this conversation. Will be passed along with webhooks so can be used as action parameters. | |
assignee_id |
String | Unique identifier for the human agent or bot that is assigned to the conversation. Used to render assignment changes in the web app. | |
assignee_type |
String | Identifies the type of assignee (must be one of Agent , Bot , or AI Agent ). |
Add a message
To record a message sent by the customer or a human agent, make a POST
request
to the /conversations/:conversation_id/messages
endpoint.
curl "https://api.gradient-labs.ai/conversations/$conversation_id/messages" \
--request POST \
--header "Authorization: Bearer $api_key" \
--header 'Content-Type: application/json' \
--data @- \
<<BODY
{
"id": "message-1234",
"body": "Hello! I need some help setting up my toaster oven, please.",
"participant_id": "user-1234",
"participant_type": "Customer",
"created": "2023-10-23T17:24:50.804952Z",
"metadata": {
"device_os": "iOS 17"
}
}
BODY
from datetime import datetime
from gradient_labs import ParticipantType
msg = client.add_message(
conversation_id="conversation-1234",
message_id="message-1234",
body="Hello! I need some help setting up my toaster oven, please.",
participant_id="user-1234",
participant_type=ParticipantType.CUSTOMER,
metadata={
"device_os": "iOS 17",
},
attachments=[
Attachment(
type=AttachmentType.IMAGE,
file_name="toaster.jpg",
)
],
)
msg, err := client.AddMessage(ctx, "conversation-1234", glabs.AddMessageParams{
ID: "message-1234",
Body: "Hello! I need some help setting up my toaster oven, please.",
ParticipantID: "user-1234",
ParticipantType: glabs.ParticipantTypeCustomer,
Metadata: map[string]string{"device_os": "iOS 17"},
Attachments: []*glabs.Attachment{
{
Type: glabs.AttachmentTypeImage,
FileName: "toaster.jpg",
},
},
})
{
"id": "message-1234",
"body": "Hello! I need some help setting up my toaster oven, please.",
"participant_id": "user-1234",
"participant_type": "Customer",
"created": "2023-10-23T17:24:50.804952Z",
"metadata": {
"device_os": "iOS 17"
}
}
Body Parameters
Parameter | Type | Required | Description |
---|---|---|---|
id |
String | ✅ | Unique identifier for this message. Can be anything consisting of letters, numbers, or any of the following characters: _ - + = . Tip: use something meaningful to your business. |
body |
String | The message text. Required if there are no attachments. | |
attachments |
List | An optional list of attachments that were sent with the message. Currently, our AI agent does not read attachments and will therefore hand-off any conversation where an attachment is uploaded. | |
participant_id |
String | ✅ | Identifies the message sender. |
participant_type |
String | ✅ | Identifies the type of sender (must be one of Customer , Agent , or Bot ). |
created |
Timestamp | The time at which the message was sent (defaults to the current time). | |
metadata |
Any | Arbitrary metadata to attach to this message. |
Assign the conversation
Assignment controls who is currently responsible for responding to the customer, it's how you activate and deactivate the AI agent.
To assign the conversation, make a PUT
request to the
/conversations/:conversation_id/assignee
endpoint.
To assign the conversation to the AI agent, use the assignee type "AI Agent"
.
Note: This will fail if you have not configured a webhook endpoint that we
can use to deliver replies to the customer.
If you need to stop the AI agent from responding in a conversation (e.g. because a human agent has taken over), assign the conversation away from the AI agent by using one of the other available assignee types.
By default, conversations will start off without an assignment, unless you
pass the assignee_id
and assignee_type
parameters to the
/start
endpoint.
Note: a conversation cannot be assigned to a customer.
curl "https://api.gradient-labs.ai/conversations/$conversation_id/assignee" \
--request PUT \
--header "Authorization: Bearer $api_key" \
--header 'Content-Type: application/json' \
--data @- \
<<BODY
{
"assignee_id": "agent-1234",
"assignee_type": "AI Agent",
"timestamp": "2023-10-23T17:24:50.804952Z",
}
BODY
from datetime import datetime
from gradient_labs import ParticipantType
client.assign_conversation(
conversation_id=conv.id,
participant_type=ParticipantType.AI_AGENT,
)
err = client.AssignConversation(ctx, conv.ID, &glabs.AssignmentParams{
AssigneeType: glabs.ParticipantTypeAIAgent,
})
if err != nil {
return err
}
{
"assignee_id": "agent-1234",
"assignee_type": "AI Agent",
}
Body Parameters
Parameter | Type | Required | Description |
---|---|---|---|
assignee_id |
String | Unique identifier for the human agent or bot that is assigned to the conversation. Used to render assignment changes in the web app. | |
assignee_type |
String | ✅ | Identifies the type of assignee (must be one of Agent , Bot , or AI Agent ). |
timestamp |
Timestamp | The time at which the conversation was assigned (defaults to the current time). |
Read a conversation
To retrieve the latest status of a conversation, make a GET
request to the /conversations/:conversation_id
endpoint.
curl "https://api.gradient-labs.ai/conversations/$conversation_id" \
--request GET \
--header "Authorization: Bearer $api_key"
client.read_conversation(conversation_id="conversation-1234")
err := client.ReadConversation(ctx, "conversation-1234")
End a conversation
When the conversation is finished (e.g., because the customer issue has been resolved, or the chat is being closed after the customer has abandoned it), make a PUT
request to the
/conversations/:conversation_id/end
endpoint.
curl "https://api.gradient-labs.ai/conversations/$conversation_id/end" \
--request PUT \
--header "Authorization: Bearer $api_key"
--data @- \
<<BODY
{
"timestamp": "2023-10-23T17:24:50.804952Z",
}
BODY
client.end_conversation(conversation_id="conversation-1234")
err := client.EndConversation(ctx, "conversation-1234")
Body Parameters
| Parameter | Type | Required | Description |
| timestamp
| Timestamp | | The time at which the conversation was ended (defaults to the current time). |
Webhooks 🪝
We deliver webhooks by making an HTTP POST
request to your configured endpoint.
Request format
PUT /your-endpoint HTTP/1.1
Host: your-api.com
Content-Type: application/json
User-Agent: GradientLabs/1.0
X-GradientLabs-Signature: t=1698953919,v1=9f16975886154e02fb0bb34694baf87b83d385d7941310c55d2021e3f5cdf780
{
"id": "webhook_01he8rwyp4fn3v870d925kz2nf",
"type": "agent.message",
"sequence_number": 5,
"timestamp": "2023-10-23T17:24:50.804952Z",
"data": {
"conversation": {
"id": "conversation-1234",
"customer_id": "user-1234",
"metadata": {
"chat_entrypoint": "home-page"
},
"body": "Sure, I can help you set up your toaster oven!"
}
}
}
The request body is a JSON object with the following structure.
Field | Type | Description |
---|---|---|
id |
String | Unique identifier for this event. |
type |
String | Type of event (documented below). |
sequence_number |
Integer | Sequential index of this event within the conversation (see below). |
timestamp |
Timestamp | Time at which this event was generated. |
data |
Object | Event data. |
The data
object for all conversation-related webhooks contains a
conversation
object with the following fields.
Field | Type | Description |
---|---|---|
id |
String | Your chosen unique identifier for this conversation. |
customer_id |
String | Your chosen identifier for the customer. |
metadata |
Any | Metadata you attached to the conversation. |
Sequence numbers
Each of the webhooks about a particular conversation includes an incrementing
number called the sequence_number
, which can be used to establish a
total order of events.
You can use this number to detect when events are missed or delivered in the
wrong order (e.g. to prevent sending a message with sequence_number = 5
before
an earlier message with sequence_number = 4
).
Signature verification
import os
from flask import Flask, request
from gradient_labs import Webhook, SignatureVerificationError
app = Flask(__name__)
signing_key = os.environ["webhook_signing_key"]
@app.route("/webhook", methods=["POST"])
def handle_webhook():
payload = request.data.decode("utf-8")
signature_header = request.headers.get("X-GradientLabs-Signature", None)
try:
event = Webhook.parse_event(
payload=payload,
signature_header=signature_header,
signing_key=signing_key,
)
except SignatureVerificationError:
print("Invalid signature!")
return "Bad signature", 401
print(
"Received event: id={id}, type={type}".format(
id=event.event_id, type=event.event_type
)
)
return "", 200
if __name__ == "__main__":
app.run(port=int(os.environ.get("PORT", 5000)))
package main
import (
"errors"
"log"
"net/http"
"os"
glabs "github.com/gradientlabs-ai/go-client"
)
func main() {
client, err := glabs.NewClient(
glabs.WithAPIKey(os.Getenv("api_key")),
glabs.WithWebhookSigningKey(os.Getenv("webhook_signing_key")),
)
if err != nil {
log.Fatal(err)
}
http.ListenAndServe(":4321", webhookHandler(client))
}
func webhookHandler(client *glabs.Client) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
webhook, err := client.ParseWebhook(r)
switch {
case errors.Is(err, glabs.ErrInvalidWebhookSignature):
w.WriteHeader(http.StatusUnauthorized)
return
case errors.Is(err, glabs.ErrUnknownWebhookType):
log.Printf("unknown webhook type: %q", webhook.Type)
return
case err != nil:
w.WriteHeader(http.StatusInternalServerError)
log.Printf("failed to parse webhook: %v", err)
return
}
// Handle the webhook...
})
}
We sign webhooks using HMAC-SHA256, so you can tell they're really from us.
This signature is in the X-GradientLabs-Signature
request header. Our client
libraries can check it for you, but to do it yourself:
- Split on the
,
character - Split each item by the
=
character to get a list of key/value pairs - Prevent replay attacks:
- Take the item with the key
t
and parse its value as a UNIX timestamp (the number of seconds since January 1st 1970) - Check the timestamp is less than 5 minutes old
- Take the item with the key
- Concatenate the UNIX timestamp, the
.
character, and the raw request body - Create an HMAC-SHA256 hash of that string, using your webhook signing key
- Hex-encode the hash and compare it to each item with the key
v1
, if one of them is a match, the request is legitimate! 🎉
Failure, timeouts, and retries
If your endpoint does not respond with a 2XX
status code within 10 seconds,
we'll consider webhook delivery failed, and begin retrying with exponential
back-off and jitter.
Critical webhooks (agent.message
, conversation.hand_off
and
conversation.finished
) will be retried 36 times over a 24 hour period.
If delivery has still not succeeded, the agent will mark the conversation as
failed
and stop processing it.
Action webhooks (action.execute
) will be retried for up to 1 minute, and if
they still have not succeeded, the conversation will be handed off to a human
agent.
agent.message
We send this event when the agent wants to send a message to the customer.
{
"id": "webhook_01he8rwyp4fn3v870d925kz2nf",
"type": "agent.message",
"sequence_number": 5,
"timestamp": "2023-10-23T17:24:50.804952Z",
"data": {
"conversation": {
"id": "conversation-1234",
"customer_id": "user-1234",
"metadata": {
"chat_entrypoint": "home-page"
}
},
"body": "Sure, I can help you set up your toaster oven!"
}
}
Payload Fields
Field | Type | Description |
---|---|---|
body |
String | Text of the message the agent wants to send to the customer. |
conversation.hand_off
{
"id": "webhook_01he8rwyp4fn3v870d925kz2nf",
"type": "conversation.hand_off",
"sequence_number": 5,
"timestamp": "2023-10-23T17:24:50.804952Z",
"data": {
"conversation": {
"id": "conversation-1234",
"customer_id": "user-1234",
"metadata": {
"chat_entrypoint": "home-page"
}
}
}
}
We send this event when the agent is escalating or handing the conversation off to a human agent.
conversation.finished
{
"id": "webhook_01he8rwyp4fn3v870d925kz2nf",
"type": "conversation.finished",
"sequence_number": 5,
"timestamp": "2023-10-23T17:24:50.804952Z",
"data": {
"conversation": {
"id": "conversation-1234",
"customer_id": "user-1234",
"metadata": {
"chat_entrypoint": "home-page"
}
}
}
}
We send this event when the agent has concluded the conversation with the customer and you should close any corresponding ticket, etc.
action.execute
We send this event when the agent wants to execute one of your configured webhook-backed actions.
For data-retrieval actions, you should respond with a JSON object containing the data. Note: don't have to tell us the response schema up-front, the agent is able to extract data from any well-formed JSON object using AI. 🪄
{
"id": "webhook_01he8rwyp4fn3v870d925kz2nf",
"type": "action.execute",
"sequence_number": 5,
"timestamp": "2023-10-23T17:24:50.804952Z",
"data": {
"conversation": {
"id": "conversation-1234",
"customer_id": "user-1234",
"metadata": {
"chat_entrypoint": "home-page"
}
},
"action": "random-dog-fact",
"params": {
"breed": "Golden Retriever"
}
}
}
Payload Fields
Field | Type | Description |
---|---|---|
action |
String | Your given identifier for this action. |
params |
Object | Parameters the agent has generated for this action. |