curl --request POST \
--url https://api.reply.io/v3/ai-sdr/learnings \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"text": "Keep subject lines under six words.",
"categories": [
"personalizedMessage"
],
"scope": "sequence",
"sequenceIds": [
42,
57
]
}
'import requests
url = "https://api.reply.io/v3/ai-sdr/learnings"
payload = {
"text": "Keep subject lines under six words.",
"categories": ["personalizedMessage"],
"scope": "sequence",
"sequenceIds": [42, 57]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
text: 'Keep subject lines under six words.',
categories: ['personalizedMessage'],
scope: 'sequence',
sequenceIds: [42, 57]
})
};
fetch('https://api.reply.io/v3/ai-sdr/learnings', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.reply.io/v3/ai-sdr/learnings",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'text' => 'Keep subject lines under six words.',
'categories' => [
'personalizedMessage'
],
'scope' => 'sequence',
'sequenceIds' => [
42,
57
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.reply.io/v3/ai-sdr/learnings"
payload := strings.NewReader("{\n \"text\": \"Keep subject lines under six words.\",\n \"categories\": [\n \"personalizedMessage\"\n ],\n \"scope\": \"sequence\",\n \"sequenceIds\": [\n 42,\n 57\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.reply.io/v3/ai-sdr/learnings")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"text\": \"Keep subject lines under six words.\",\n \"categories\": [\n \"personalizedMessage\"\n ],\n \"scope\": \"sequence\",\n \"sequenceIds\": [\n 42,\n 57\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.reply.io/v3/ai-sdr/learnings")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"text\": \"Keep subject lines under six words.\",\n \"categories\": [\n \"personalizedMessage\"\n ],\n \"scope\": \"sequence\",\n \"sequenceIds\": [\n 42,\n 57\n ]\n}"
response = http.request(request)
puts response.read_body{
"id": 5498,
"text": "Never open a message with \"Hello\".",
"categories": [
"personalizedMessage"
],
"source": "manual",
"scope": "user",
"sequenceIds": [],
"isEnabled": true,
"createdAt": "2026-09-02T09:14:07Z",
"updatedAt": "2026-09-11T16:41:55Z",
"ownerUserId": 1234,
"isOwn": true,
"conflictState": "takesPrecedence",
"conflicts": [
{
"otherLearningId": 5512,
"otherText": "Always open with a short greeting.",
"categories": [
"personalizedMessage"
],
"state": "takesPrecedence",
"reason": "One learning forbids a greeting the other requires."
}
]
}Create an AI learning
Coming soon. This endpoint will be available by early October 2026.
ai-sdr:write scope (or a broader one that includes it).
Use this endpoint when you need a new standing rule for how the AI writes. text is required and limited to 1000 characters, and categories says where the rule applies — personalizedMessage for outbound drafting, replyHandling for answering incoming mail, or both. scope decides who the rule binds: user for yourself across all your sequences, sequence for named sequences only, workspace for everyone in your team, or organization for every workspace in the org. sequenceIds names the sequences — at most 50 and no repeats, each one you can see — and is required for the sequence scope and rejected for every other one. Creating a workspace or organization learning needs the matching permission, and an organization learning needs you to belong to one — without either the call is refused and nothing is written. A learning created here is always enabled and counts as manually authored, so it can be refused with a conflict when those categories and scope already hold as many manually authored learnings as they may; the message says which budget is full. The created learning is returned in full with its assigned id — unless the service cannot be reached to read it straight back, in which case the response carries the assigned id and the fields you sent rather than the stored learning. Contradictions with your other learnings are looked for after the learning is stored, so conflictState comes back as none here — read the learning again a moment later to see its conflict standing. The AI Learnings feature must be enabled for your team.
curl --request POST \
--url https://api.reply.io/v3/ai-sdr/learnings \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"text": "Keep subject lines under six words.",
"categories": [
"personalizedMessage"
],
"scope": "sequence",
"sequenceIds": [
42,
57
]
}
'import requests
url = "https://api.reply.io/v3/ai-sdr/learnings"
payload = {
"text": "Keep subject lines under six words.",
"categories": ["personalizedMessage"],
"scope": "sequence",
"sequenceIds": [42, 57]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
text: 'Keep subject lines under six words.',
categories: ['personalizedMessage'],
scope: 'sequence',
sequenceIds: [42, 57]
})
};
fetch('https://api.reply.io/v3/ai-sdr/learnings', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.reply.io/v3/ai-sdr/learnings",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'text' => 'Keep subject lines under six words.',
'categories' => [
'personalizedMessage'
],
'scope' => 'sequence',
'sequenceIds' => [
42,
57
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.reply.io/v3/ai-sdr/learnings"
payload := strings.NewReader("{\n \"text\": \"Keep subject lines under six words.\",\n \"categories\": [\n \"personalizedMessage\"\n ],\n \"scope\": \"sequence\",\n \"sequenceIds\": [\n 42,\n 57\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.reply.io/v3/ai-sdr/learnings")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"text\": \"Keep subject lines under six words.\",\n \"categories\": [\n \"personalizedMessage\"\n ],\n \"scope\": \"sequence\",\n \"sequenceIds\": [\n 42,\n 57\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.reply.io/v3/ai-sdr/learnings")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"text\": \"Keep subject lines under six words.\",\n \"categories\": [\n \"personalizedMessage\"\n ],\n \"scope\": \"sequence\",\n \"sequenceIds\": [\n 42,\n 57\n ]\n}"
response = http.request(request)
puts response.read_body{
"id": 5498,
"text": "Never open a message with \"Hello\".",
"categories": [
"personalizedMessage"
],
"source": "manual",
"scope": "user",
"sequenceIds": [],
"isEnabled": true,
"createdAt": "2026-09-02T09:14:07Z",
"updatedAt": "2026-09-11T16:41:55Z",
"ownerUserId": 1234,
"isOwn": true,
"conflictState": "takesPrecedence",
"conflicts": [
{
"otherLearningId": 5512,
"otherText": "Always open with a short greeting.",
"categories": [
"personalizedMessage"
],
"state": "takesPrecedence",
"reason": "One learning forbids a greeting the other requires."
}
]
}Authorizations
Authenticate every request with a Bearer token. Pass your Reply API key in the
Authorization header:
Authorization: Bearer <your-api-key>
Get your API key from the Reply dashboard: Settings → API Key.
Body
Request body for creating an AI learning. A learning created through the API is always enabled and counts as manually authored.
The standing rule the AI should follow
1 - 1000Where the rule applies. Send one or both values, without repeats.
1Where a learning applies. personalizedMessage shapes outbound message drafting, replyHandling
shapes answers to incoming mail. A learning can carry both.
personalizedMessage, replyHandling Who the learning binds. workspace and organization need the matching permission, and
organization also needs you to belong to an organization.
organization, workspace, user, sequence Sequences the learning is limited to, each one a sequence you can see. Required when scope
is sequence and rejected for every other scope.
50x >= 1Response
The learning was created
A standing rule that shapes how the AI writes, together with the learnings it contradicts.
Unique identifier of the learning
The standing rule the AI follows
Where the rule applies
Where a learning applies. personalizedMessage shapes outbound message drafting, replyHandling
shapes answers to incoming mail. A learning can carry both.
personalizedMessage, replyHandling Whether the AI derived the learning or a person wrote it
aiGenerated, manual Who the learning binds
organization, workspace, user, sequence Sequences the learning is limited to. Empty unless scope is sequence.
Whether the learning is switched on
When the learning was created (UTC)
When the learning was last changed (UTC). null if it has never been changed.
User ID of the learning's owner. null when the owner is no longer a Reply user.
Whether the learning is yours. false for a learning a team or organization admin set for everyone.
How the learning stands against the learnings it contradicts. An unresolved learning is not applied.
none, overridden, takesPrecedence, unresolved Every other learning this one contradicts. Empty when conflictState is none.
Show child attributes
Show child attributes