How-To Guides

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

  1. Create an email draft — compose the message and receive a mail_id
  2. Attach a file — initiate an upload session scoped to the draft, then transfer the file content
  3. Send the email — promote the draft to sent by setting draft to false
Sending without an attachment? Set "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.
All requests require a valid Bearer token in the 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.

POST /rest/mail/actions/sendFile

Query parameters

ParameterTypeDescription
returnEntitybooleanWhen true, the created email object — including its id — is returned in the response body. Required for this workflow.

Request body

FieldTypeRequiredDescription
subjectstringRequiredThe email subject line.
bodystringRequiredThe email body. Supports plain text and HTML.
toarray of stringsRequiredList of recipient email addresses.
ccarray of stringsOptionalList of CC recipient email addresses.
bccarray of stringsOptionalList of BCC recipient email addresses.
draftstringRequiredSet to "true" to save as a draft. Set to "false" to send immediately (skipping steps 2 and 3).
secureBodystringOptionalSet to "true" to encrypt the message body. Defaults to "false".
create-draft.sh
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"
  }'
create_draft.py
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.

201 Created
{
  "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.

POST /rest/mail/{mail_id}/actions/initiateUpload

Request body

FieldTypeRequiredDescription
filenamestringRequiredThe name the attachment will have on the email.
totalSizeintegerRequiredTotal size of the file in bytes.
totalChunksintegerRequiredNumber of chunks the file will be split into. Use 1 for single-chunk uploads.
initiate-attachment.sh
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.

Mail attachments use the 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.
201 Created
{
  "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.

POST /{uri}

Request fields (multipart/form-data)

FieldTypeRequiredDescription
compressionModestringRequiredCompression mode. Only NORMAL is supported.
compressionSizeintegerRequiredCompressed size in bytes. When using NORMAL, set to the same value as originalSize.
originalSizeintegerRequiredOriginal size of the chunk in bytes.
contentfileRequiredBinary content of this chunk.
indexintegerOptionalChunk index, starting from 1. Required when uploading multiple chunks.
lastChunkintegerOptionalSet to 1 on the final chunk to complete the upload.
upload-attachment.sh
# 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.

201 Created — attachment complete
{
  "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.

PUT /rest/mail/{mail_id}/actions/sendFile

Request body

FieldTypeRequiredDescription
filesarrayRequiredArray containing the file_id returned by the upload in step 2b.
draftstringRequiredMust be "false" to send. Setting this to "false" triggers immediate dispatch.
uploadingintegerRequiredSet to 0 to indicate the upload is complete.
You can also update subject, body, or to in this same request before sending — but be aware the email dispatches as soon as "draft": "false" is set.
send-email.sh
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
  }'
send_mail.py
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: