Triggering Copilot Studio Agents with HTTP Calls
Triggering Copilot Studio Agents with HTTP Calls
Sometimes you want to trigger an autonomous Copilot Studio agent with HTTP requests from an external source. This post shows two complete paths to integrate your agent with external APIs.
Overview
There are two main approaches to trigger your agent:
- Cloud Flow path: Using Power Automate to call the agent
- Direct HTTP path: Calling the agent directly with REST and a Direct Line secret
We’ll explore both paths through three practical examples:
- Cloud Flow integration in Power Platform
- API call collection with Hoppscotch
- Python demo script
Path 1: Cloud Flow Integration
Cloud Flows provide a clean way to trigger a Copilot Studio agent with a fire-and-forget mechanism. This approach is particularly efficient if you’re already working within the Power Platform ecosystem, and you can even set up this cloud flow using Copilot Studio credits.
Required Steps
- Trigger: Configure “When an HTTP request is received”
- Action: Set up “Execute Copilot”
- Response: Return the agent reply (just the conversationID since this is a fire-and-forget mechanism)
Path 2: Direct HTTP Integration
Direct HTTP calls with a Direct Line secret allow you to trigger a Copilot Studio agent, send messages, and receive responses directly over REST. This approach requires four distinct API calls:
- Generate a Direct Line token using your secret
- Start a conversation
- Send a text message to the agent
- Pull the conversation activities (without watermark)
This path is ideal if you want a raw API integration without the Power Platform wrapper.
Important Security Notes
- Never hard code the secret - use Azure Key Vault or another secure store
- Implement proper access control for the endpoint
- Rotate the secret on a regular schedule
Preparing Your Agent
Before implementing either approach, you need to:
- Publish your agent
- Go to settings and the “Security” tab
- Select “No Authentication”
- Navigate to “Web channel security” in the “Security” tab
- Copy the secret for later use
- Activate the “Require secured access” toggle
This configuration ensures the agent can only be triggered when including the proper secret.
Implementation Examples
Example 1: Native Cloud Flow
Step 1: Configure the Trigger
Create a new Cloud Flow and select “When an HTTP request is received” as your trigger (note: this is a premium trigger).
Step 2: Execute Copilot
Add the “Execute Copilot” action and pass the trigger body to the agent. Here’s an example of the body structure:
1
2
3
4
5
6
7
8
{
"type": "message",
"from": {
"id": "dl_7f8c4d42-3f1b-42a2-92a3-84f879a546e3"
},
"text": "CAT calling this agent, with secret phrase Meow",
"locale": "en-EN"
}
Step 3: Return Response
Add a “Response” action that returns the agent’s conversationId with a 200 status code to the HTTP sender.
Note that this is a fire-and-forget mechanism - you’ll receive the response from the triggered Copilot Agent separately.
Example 2: Direct HTTP Calls Using Direct Line
Step 1: Generate Direct Line Token
Send a POST request to generate a token:
Endpoint: https://directline.botframework.com/v3/directline/tokens/generate
Headers:
1
Authorization: Bearer <DIRECT_LINE_SECRET>
Body:
1
2
3
4
5
6
{
"user": {
"id": "dl_7f8c4d42-3f1b-42a2-92a3-84f879a546e3",
"name": "Copilot CAT Direct Line tester"
}
}
Make sure to capture the conversationId and token from the response.
Step 2: Start Conversation
Send a POST request to initialize the conversation:
Endpoint: https://directline.botframework.com/v3/directline/conversations
Headers:
1
Authorization: Bearer <TOKEN_FROM_STEP_1>
Step 3: Send Message to Bot
Send a POST request with your prompt:
Endpoint: https://directline.botframework.com/v3/directline/conversations/<CONVERSATION_ID>/activities
Headers:
1
Authorization: Bearer <TOKEN_FROM_STEP_1>
Body:
1
2
3
4
5
6
7
8
{
"type": "message",
"from": {
"id": "dl_7f8c4d42-3f1b-42a2-92a3-84f879a546e3"
},
"text": "CAT calling this agent, with secret phrase Meow",
"locale": "en-EN"
}
Step 4: Fetch Conversation Activities
Send a GET request to retrieve the conversation:
Endpoint: https://directline.botframework.com/v3/directline/conversations/<CONVERSATION_ID>/activities
Headers:
1
Authorization: Bearer <TOKEN_FROM_STEP_1>
The response will include all activities in the conversation, similar to:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"activities": [
{
"type": "message",
"id": "53orLbCv2GM7jGfhlyeaUO-eu|0000000",
"timestamp": "2025-09-25T19:04:48.4092092Z",
"serviceUrl": "https://directline.botframework.com/",
"channelId": "directline",
"from": {
"id": "dl_7f8c4d42-3f1b-42a2-92a3-84f879a546e3"
},
"conversation": {
"id": "53orLbCv2GM7jGfhlyeaUO-eu"
},
"locale": "en-EN",
"text": "CAT calling this agent, with secret phrase Meow"
},
{
"type": "message",
"id": "53orLbCv2GM7jGfhlyeaUO-eu|0000001",
"timestamp": "2025-09-25T19:04:55.1522894Z",
"channelId": "directline",
"from": {
"id": "487381d2-438d-49f6-0a31-e856dfc3459d",
"name": "Directline test agent",
"role": "bot"
},
"conversation": {
"id": "53orLbCv2GM7jGfhlyeaUO-eu"
},
"text": "success, you've been able to connect with the Directline test agent.",
"inputHint": "acceptingInput"
}
],
"watermark": "1"
}
Example 3: Python Implementation
You can also use a Python script to call the agent. This approach is ideal for testing purposes but should be enhanced for production use.
Development Notes
- Store the Direct Line secret in environment variables
- Add proper error handling with retries and timeouts
- Implement comprehensive logging of status, headers, and response bodies
- Use Azure Key Vault or similar for secrets management
Check the appendix for the complete Python implementation and Hoppscotch collection.
Conclusion
You now have two complete paths to trigger your Copilot Studio agent: Cloud Flow for a native Power Platform integration, or Direct HTTP for a lean REST-based approach. With the provided examples (Cloud Flow, Hoppscotch, and Python), you can trigger an agent, POST messages, and integrate responses into your own API flow.
We’re excited to see what you’ll build with these integration patterns. Share your implementations and use cases in the comments below!
Appendix
Hoppscotch Collection
Save the following JSON as “sample-trigger-copilot-studio-agent-with-api.json” and import it into Hoppscotch:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
{
"v": 10,
"name": "Example to trigger an agent using Direct Line agent using HTTP calls",
"folders": [],
"requests": [
{
"v": "15",
"name": "1: Generate Direct Line token (using Direct Line secret)",
"method": "POST",
"endpoint": "https://directline.botframework.com/v3/directline/tokens/generate",
"params": [],
"headers": [
{
"key": "Authorization",
"value": "Bearer <<DIRECT_LINE_SECRET>>",
"active": true
}
],
"body": {
"contentType": "application/json",
"body": {
"user": {
"id": "dl_7f8c4d42-3f1b-42a2-92a3-84f879a546e3",
"name": "Copilot CAT Direct Line tester"
}
}
}
},
{
"v": "15",
"name": "2: Start conversation",
"method": "POST",
"endpoint": "https://directline.botframework.com/v3/directline/conversations",
"headers": [
{
"key": "Authorization",
"value": "Bearer <<DIRECT_LINE_TOKEN>>",
"active": true
}
]
},
{
"v": "15",
"name": "3: Send text message to the bot",
"method": "POST",
"endpoint": "https://directline.botframework.com/v3/directline/conversations/<<conversationId>>/activities",
"headers": [
{
"key": "Authorization",
"value": "Bearer <<DIRECT_LINE_TOKEN>>",
"active": true
}
],
"body": {
"contentType": "application/json",
"body": {
"type": "message",
"from": {
"id": "dl_7f8c4d42-3f1b-42a2-92a3-84f879a546e3"
},
"text": "CAT calling this agent, with secret phrase Meow",
"locale": "en-EN"
}
}
},
{
"v": "15",
"name": "4: Immediately fetch conversation activities without watermark",
"method": "GET",
"endpoint": "https://directline.botframework.com/v3/directline/conversations/<<conversationId>>/activities",
"headers": [
{
"key": "Authorization",
"value": "Bearer <<DIRECT_LINE_TOKEN>>",
"active": true
}
]
}
]
}
Python Implementation
Here’s a complete Python script that demonstrates the Direct Line integration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""
Direct Line token + GET activities poll
- Generate token
- Start conversation
- Send text
- Poll /activities with watermark to read bot reply
"""
import json
import time
import uuid
import requests
# Config
DIRECT_LINE_BASE = "https://directline.botframework.com/v3/directline"
# Create a fake test user with a random unique ID (must start with "dl_")
TEST_USER_ID = f"dl_{uuid.uuid4()}"
TEST_USER_NAME = "Copilot CAT Direct Line tester"
# Default test message and language
DEFAULT_TEXT = "CAT calling this agent, with secret phrase Meow"
DEFAULT_LOCALE = "en-EN"
def _auth_secret(secret: str):
# Used when sending your Direct Line *secret* (to get a token)
return {"Authorization": f"Bearer {secret}"}
def _auth_token(token: str):
# Used once we already have a short-lived Direct Line *token*
return {"Authorization": f"Bearer {token}"}
def _json_headers():
# Tells the server we are sending JSON
return {"Content-Type": "application/json"}
def generate_token_directline(secret: str, user_id: str, user_name: str):
"""
Exchange the Direct Line secret for a short-lived token.
Tokens are safer to use in apps than secrets.
"""
url = f"{DIRECT_LINE_BASE}/tokens/generate"
payload = {"user": {"id": user_id, "name": user_name}}
resp = requests.post(
url,
headers={**_auth_secret(secret), **_json_headers()},
data=json.dumps(payload)
)
resp.raise_for_status()
return resp.json()
def start_conversation(token: str):
"""
Start a new conversation with the bot.
This gives us a conversationId we'll use in later calls.
"""
url = f"{DIRECT_LINE_BASE}/conversations"
resp = requests.post(url, headers=_auth_token(token))
if resp.status_code not in (200, 201):
resp.raise_for_status()
return resp.json()
def send_text(conversation_id: str, token: str, user_id: str, text: str):
"""
Send a text message into the bot conversation.
"""
url = f"{DIRECT_LINE_BASE}/conversations/{conversation_id}/activities"
payload = {
"type": "message",
"from": {"id": user_id},
"text": text,
"locale": DEFAULT_LOCALE
}
resp = requests.post(
url,
headers={**_auth_token(token), **_json_headers()},
data=json.dumps(payload)
)
resp.raise_for_status()
return resp.json()
def get_activities(conversation_id: str, token: str, watermark: str | None = None):
"""
Get all activities (messages, events, etc.) in the conversation.
We pass a 'watermark' so we only get *new* activities since last check.
"""
url = f"{DIRECT_LINE_BASE}/conversations/{conversation_id}/activities"
params = {}
if watermark:
params["watermark"] = watermark
resp = requests.get(url, headers=_auth_token(token), params=params)
resp.raise_for_status()
return resp.json()
def poll_bot_replies(conversation_id: str, token: str, user_id: str,
start_watermark: str | None,
timeout_sec: float = 15.0,
interval_sec: float = 1.0):
"""
Keep polling the bot for new messages until we get a reply
or until the timeout runs out.
"""
seen_ids = set() # keep track of messages we already saw
wm = start_watermark # start from the given watermark
deadline = time.time() + timeout_sec
bot_texts = []
while time.time() < deadline:
# Get activities (messages/events) from bot
data = get_activities(conversation_id, token, wm)
wm = data.get("watermark", wm)
for act in data.get("activities", []):
act_id = act.get("id")
if not act_id or act_id in seen_ids:
continue
seen_ids.add(act_id)
# If it's a bot message (not from our user)
if act.get("type") == "message" and act.get("from", {}).get("id") != user_id:
text = act.get("text") or ""
if text.strip():
bot_texts.append(text)
if bot_texts: # stop early if we already got something
break
time.sleep(interval_sec) # wait before asking again
return bot_texts, wm
def main():
# Get your Direct Line secret from environment or configuration
direct_line_secret = input("Enter your Direct Line secret: ")
print("== Direct Line GET activities demo ==")
# 1) Generate Direct Line token
print("[1] Generating token via Direct Line...")
gen = generate_token_directline(direct_line_secret, TEST_USER_ID, TEST_USER_NAME)
token = gen["token"]
print(f"Token acquired, expires in {gen['expires_in']}s")
# 2) Start a new conversation
print("[2] Starting conversation...")
conv = start_conversation(token)
conversation_id = conv["conversationId"]
print(f"Conversation started: {conversation_id}")
# 3) Get the initial watermark
print("[3] Priming watermark with an initial GET...")
first = get_activities(conversation_id, token, None)
watermark = first.get("watermark")
print(f"Initial watermark: {watermark}")
# 4) Send a test message to the bot
print("[4] Sending message...")
res = send_text(conversation_id, token, TEST_USER_ID, DEFAULT_TEXT)
print(f"Message sent, activity ID: {res.get('id')}")
# Short wait so bot has time to reply
time.sleep(0.3)
# 5) Poll the bot until we get a reply or timeout
print("[5] Polling for bot reply via GET /activities...")
replies, watermark = poll_bot_replies(conversation_id, token, TEST_USER_ID, watermark)
if replies:
for i, t in enumerate(replies, 1):
print(f"[Bot {i}] {t}")
else:
print("No bot reply in time window.")
print(f"Done. Last watermark: {watermark}")
if __name__ == "__main__":
main()
Note: Remember to replace the Direct Line secret with your own when testing these examples.