Manage Mail with the Kiteworks REST API
How to compose, attach a file to, and send mail using the Kiteworks API. This guide covers the full draft-based workflow: create a message as a draft, attach a file to it, then dispatch it — all via three sequential API calls.
In this guide
- Create an email draft — compose the message and receive a
mail_id - Attach a file — initiate an upload session scoped to the draft, then transfer the file content
- Send the email — promote the draft to sent by setting
drafttofalse
"draft": "false" in the
step 1 request body and the message is sent immediately — no upload steps needed.
The remaining steps in this guide only apply when attaching a file.
Authorization header and the
X-Accellion-Version: 28 header. The Python sample imports KWOAuthClient
from a get_access_token module — see
Authentication
for the implementation.
1. Create an Email Draft
Creates the email message. Setting draft to true saves it without
sending, giving you a mail_id you'll use in the next two steps. The
id field in the response is that mail_id.
/rest/mail/actions/sendFile
Query parameters
| Parameter | Type | Description |
|---|---|---|
returnEntity | boolean | When true, the created email object — including its id — is returned in the response body. Required for this workflow. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | Required | The email subject line. |
body | string | Required | The email body. Supports plain text and HTML. |
to | array of strings | Required | List of recipient email addresses. |
cc | array of strings | Optional | List of CC recipient email addresses. |
bcc | array of strings | Optional | List of BCC recipient email addresses. |
draft | string | Required | Set to "true" to save as a draft. Set to "false" to send immediately (skipping steps 2 and 3). |
secureBody | string | Optional | Set to "true" to encrypt the message body. Defaults to "false". |
curl -X POST "{base_url}/rest/mail/actions/sendFile?returnEntity=true" \
-H "X-Accellion-Version: 28" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": "Test mail subject",
"body": "Test mail body",
"to": [
"recipient1@example.com",
"recipient2@example.com"
],
"cc": [],
"bcc": [],
"draft": "true",
"secureBody": "false"
}'
import requests
from get_access_token import KWOAuthClient
def create_email_draft(base_url, access_token, subject, body, recipient):
"""Create an email draft and return its mail_id."""
url = f"{base_url}/rest/mail/actions/sendFile?returnEntity=true"
headers = {
"X-Accellion-Version": "28",
"Authorization": f"Bearer {access_token}",
}
payload = {
"subject": subject,
"body": body,
"to": [recipient],
"cc": [],
"bcc": [],
"draft": "true",
"secureBody": "false",
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()["id"]
Example response
Returns 201 Created. The id field is your mail_id — keep it for steps 2 and 3.
{
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"status": "draft",
"bucket": "draft",
"subject": "Test email subject",
"body": "<p style=\"margin: 0px;\">Test email body</p>",
"rawBody": "Test email body",
"secureBody": true,
"emailFrom": "user@example.com",
"senderId": "a111b222-c333-d444-e555-f666777888aaa",
"recipients": [
{
"type": 0,
"email": "recipient1@example.com",
"userId": "f9e8d7c6-b5a4-3f2e-1d0c-9b8a7f6e5d4c",
"isDistributionList": false
},
{
"type": 0,
"email": "recipient2@example.com",
"userId": "e8d7c6b5-a4f3-2e1d-0c9b-8a7f6e5d4c3b",
"isDistributionList": false
}
],
"attachmentCount": 0,
"date": "2026-02-04T14:15:04+0000",
"modifiedDate": "2026-02-04T14:15:04+0000",
"expirationDate": "2026-03-06T23:59:59+0000",
"isRead": false,
"deleted": false
}
2. Attach a File
Attaching a file uses the same chunked upload protocol as
uploading a file to a folder,
with one important difference: the upload session is scoped to the email draft, not a
folder. The initiate endpoint uses /rest/mail/{mail_id}/, and the upload
URL is constructed from the uri field in the response — not the id.
Step 2a — Initiate the attachment upload
Creates an upload session tied to the email draft. The uri in the response
is the path you'll POST the file content to in step 2b.
/rest/mail/{mail_id}/actions/initiateUpload
Request body
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | Required | The name the attachment will have on the email. |
totalSize | integer | Required | Total size of the file in bytes. |
totalChunks | integer | Required | Number of chunks the file will be split into. Use 1 for single-chunk uploads. |
curl -X POST "{base_url}/rest/mail/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d/actions/initiateUpload?returnEntity=true" \
-H "X-Accellion-Version: 28" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filename": "MyNewFile.txt",
"totalSize": 2048,
"totalChunks": 1
}'
Example response
Returns 201 Created. Note the uri field — you'll prepend your
base_url to it to form the upload URL for step 2b. This is different from
the file upload workflow, which uses the id field instead.
uri field to construct the upload URL
(e.g. {base_url}/dacfs_upload1/rest/uploads/7890). This differs from
file uploads to folders,
which use the numeric id field with the standard
/rest/uploads/{id} path.
{
"id": 7890,
"uri": "dacfs_upload1/rest/uploads/7890",
"error": "OK",
"totalSize": 2048,
"totalChunks": 1,
"uploadedSize": 0,
"uploadedChunks": 0,
"completeOk": 0,
"timestamp": "2020-05-15T08:11:07Z"
}
Step 2b — Upload the attachment content
POST the file content to the URL formed by prepending your base_url to the
uri from step 2a. The id in the final response is the
file_id you'll include when sending the email in step 3.
/{uri}
Request fields (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
compressionMode | string | Required | Compression mode. Only NORMAL is supported. |
compressionSize | integer | Required | Compressed size in bytes. When using NORMAL, set to the same value as originalSize. |
originalSize | integer | Required | Original size of the chunk in bytes. |
content | file | Required | Binary content of this chunk. |
index | integer | Optional | Chunk index, starting from 1. Required when uploading multiple chunks. |
lastChunk | integer | Optional | Set to 1 on the final chunk to complete the upload. |
# Upload URL is base_url + uri from the initiate response
curl -X POST "{base_url}/dacfs_upload1/rest/uploads/7890?returnEntity=true" \
-H "X-Accellion-Version: 28" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-F "compressionMode=NORMAL" \
-F "compressionSize=2048" \
-F "originalSize=2048" \
-F "index=1" \
-F "lastChunk=1" \
-F "content=@/tmp/MyNewFile.txt"
Example response
The final chunk returns 201 Created with the uploaded file object.
The id field is the file_id you'll pass in the send request.
{
"id": 1240,
"name": "MyNewFile.txt",
"type": "f",
"mime": "text/plain",
"size": 2048,
"parentId": 1234,
"locked": false,
"deleted": false,
"fingerprint": "Generating...",
"created": "2020-05-15T06:25:16Z",
"modified": "2020-05-15T09:30:14Z"
}
3. Send the Email
Promotes the draft to sent by updating it with "draft": "false" and the
file_id from the upload. The email is dispatched immediately when this
request completes.
/rest/mail/{mail_id}/actions/sendFile
Request body
| Field | Type | Required | Description |
|---|---|---|---|
files | array | Required | Array containing the file_id returned by the upload in step 2b. |
draft | string | Required | Must be "false" to send. Setting this to "false" triggers immediate dispatch. |
uploading | integer | Required | Set to 0 to indicate the upload is complete. |
subject, body, or to in this
same request before sending — but be aware the email dispatches as soon as
"draft": "false" is set.
curl -X PUT "https://{base_url}/rest/mail/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d/actions/sendFile?returnEntity=true" \
-H "X-Accellion-Version: 28" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"files": ["1240"],
"draft": "false",
"uploading": 0
}'
import os
import requests
from get_access_token import KWOAuthClient
def send_mail(base_url, access_token, file_name, file_path, subject, body, recipient):
"""
Compose an email, attach a file, and send it.
file_name: Name the attachment will have on the email
file_path: Local path to the file, e.g. "/tmp/report.pdf"
subject: Email subject line
body: Email message body
recipient: Recipient email address
"""
headers = {
"X-Accellion-Version": "28",
"Authorization": f"Bearer {access_token}",
}
# Step 1: Create the email draft
create_url = f"{base_url}/rest/mail/actions/sendFile?returnEntity=true"
create_payload = {
"subject": subject,
"body": body,
"to": [recipient],
"draft": "true",
"secureBody": "false",
}
create_response = requests.post(create_url, headers=headers, json=create_payload)
create_response.raise_for_status()
mail_id = create_response.json()["id"]
print(f"Draft created. mail_id: {mail_id}")
# Step 2a: Initiate the attachment upload
file_size = os.path.getsize(file_path)
initiate_url = f"{base_url}/rest/mail/{mail_id}/actions/initiateUpload?returnEntity=true"
initiate_payload = {
"filename": file_name,
"totalSize": file_size,
"totalChunks": 1,
}
initiate_response = requests.post(initiate_url, headers=headers, json=initiate_payload)
initiate_response.raise_for_status()
# Mail attachment uploads use the uri field, not the id field
upload_uri = initiate_response.json()["uri"]
# Step 2b: Upload the attachment content
upload_url = f"{base_url}/{upload_uri}?returnEntity=true"
with open(file_path, "rb") as f:
payload = {
"compressionMode": (None, "NORMAL"),
"compressionSize": (None, str(file_size)),
"originalSize": (None, str(file_size)),
"index": (None, "1"),
"lastChunk": (None, "1"),
"content": (file_name, f),
}
upload_response = requests.post(upload_url, headers=headers, files=payload)
if upload_response.status_code != 201:
print(f"Attachment upload failed: {upload_response.status_code} — {upload_response.text}")
return
file_id = upload_response.json()["id"]
print(f"Attachment uploaded. file_id: {file_id}")
# Step 3: Send the email
send_url = f"{base_url}/rest/mail/{mail_id}/actions/sendFile?returnEntity=true"
send_payload = {
"files": [file_id],
"draft": "false",
"uploading": 0,
}
send_response = requests.put(send_url, headers=headers, json=send_payload)
if send_response.status_code == 200:
print("Email sent successfully.")
else:
print(f"Failed to send email: {send_response.status_code} — {send_response.text}")
def main():
base_url = input("Enter Base URL for Kiteworks instance: ")
client = KWOAuthClient(base_url)
access_token = client.get_access_token()
file_path = input("Enter local file path: ")
file_name = input("Enter attachment name: ")
subject = input("Enter email subject: ")
body = input("Enter email body: ")
recipient = input("Enter recipient email address: ")
send_mail(base_url, access_token, file_name, file_path, subject, body, recipient)
if __name__ == "__main__":
main()
Frequently Asked Questions
Why do I use the URI instead of the ID for mail attachments?
When you initiate a file upload for a mail attachment, the API returns both an id and a uri. You must use the uri field — not the id — as the endpoint for posting the file data. The URI is a pre-authorized upload URL that routes the file correctly within the mail context.
What is the difference between sending a mail draft and sending directly?
Creating a draft (draft=true) gives you the mail ID you need to attach files before the message is sent. Once attachments are in place, a second call with draft=false sends the message. Skipping the draft step means you cannot attach files before sending.
How do I specify recipients for a Kiteworks mail message?
Recipients are specified in the request body of the mail creation call as a JSON array of email address objects in the recipients field. Both internal Kiteworks users and external email addresses are supported. External recipients receive a secure notification link rather than a direct attachment.
Can I attach multiple files to a single Kiteworks mail message?
Yes. Repeat the attach file workflow (initiateUpload + POST data) for each file you want to attach to the same mail ID. All attachments are associated with the draft until you send it. There is no limit imposed by the API protocol itself, though your Kiteworks instance configuration may set a maximum attachment count or total size.
Next Steps
You've now covered the three core How-To workflows. Here's where to explore further:
- Kiteworks MCP → Integrate Kiteworks with AI tools using the Model Context Protocol
- Manage Files → Upload and download files directly to folders
- API Reference → Full
/mailendpoint specification