Skip to main content
12-Amethyst
July 20, 2026
Solved

Create WTDoc with content with REST API

  • July 20, 2026
  • 1 reply
  • 96 views
I am using Windchill PDMLink Release 12.1 and Datecode with CPS 12.1.2.1 I want to create a Document using REST API, while adding a PDF as primary content and another file as secondary content. I know how to create iteration 0.1 of the Document, then add the files to iteration 0.2. How can I create everything in iteration 0.1? Here are the errors that I faced self.url + "/servlet/odata/DocMgmt/UploadStage1Action with data { "NoOfFiles": 1} gives error: ApiError NOT FOUND 404 self.url + "/servlet/odata/DocMgmt/PTC.DocMgmt.UploadStage1Action with data { "NoOfFiles": 1} gives error: UploadStage1Action must not follow a collection
Best answer by dgarcia

I was able to solve this. Here is what I did:

  1. Create a new Document without primary content nor attachments.
  2. Rename and checkout
  3. Follow the 3 stages upload content procedure but:
    • In stage 2 use encode_multipart_formdata function from  urlib3
    • In stage 3 use the 0.1 iteration, not to the checked out one
  4. Undo the checkout

Here is the (edited) code I used         

    # get product_id and folder_id first

dataDocument = {
"Name": name,
"Description": name,
# "Number": number, # rename later
"Revision": revision,
"Folder@odata.bind": f"Folders('{folder_id}')",
"Context@odata.bind": f"Containers('{product_id}')"
}

# create empty Design document 0.1
full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents"
response = self.callWindchillPOST(full_url, dataDocument)

if response == None:
print("Error creating document")
return None

# rename document
doc_id = response['ID']

full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UpdateCommonProperties"

# The payload specifies the updates for common attributes.
payload = {
"Updates": {
"Name": newName,
"Number": newNumber
}
}
data = self.callWindchillPOST(full_url, payload) # Rename

# now use the 3 stage upload

# fill file_paths[] with the file paths to the attachemnts.
fill_paths[0] = "C:\\ ... \\primary_content.pdf"
fill_paths[1] = "C:\\ ... \\attachement1.pdf"
fill_paths[2] = "C:\\ ... \\attachement2.pdf"

# keep basenames, sizes, contents and mimetypes
file_basenames = []
file_sizes = []
file_contents = []
file_mimetypes = []
for path in file_paths:
if not os.path.exists(path):
print(f"Error: Local file '{path}' not found.")
return None
else:
file_basenames.append(os.path.basename(path))
file_sizes.append(os.path.getsize(path))
file_contents.append(open(path, "rb").read())
file_mimetypes.append(mimetypes.guess_type(path)[0])


# Checkout Document
documentCheckOut = self.documentCheckOut(doc_id)
if documentCheckOut == None:
print(f"Error checking out document {number}")
return None

# ------------------------------------------------------------------
# STAGE 1: Request upload URL and StreamID from Windchill
# ------------------------------------------------------------------
print("Stage 1: Initializing content upload session...")
stage1_url = self.url + f"/servlet/odata/v7/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UploadStage1Action"
stage1_payload = {
"NoOfFiles": len(file_paths) # total number of files
}
stage1_resp = self.callWindchillPOST(stage1_url, stage1_payload)
if not stage1_resp or 'value' not in stage1_resp or len(stage1_resp['value']) == 0:
print("Failed to initialize Stage 1 upload.")
self.documentUndoCheckOut(documentCheckOut['ID'])
return None
print("Stage 1 initialized successfully.")

# ------------------------------------------------------------------
# STAGE 2: Upload primary file bytes to the target URL
# ------------------------------------------------------------------
print("Stage 2: Uploading primary file content...")

stage2_url = stage1_resp['value'][0]['ReplicaUrl']
master_url = stage1_resp['value'][0]['MasterUrl']
stream_ids = stage1_resp['value'][0]['StreamIds']
filename_ids = stage1_resp['value'][0]['FileNames']

# create multipart request
boundary = "99--77665544332211" # any separator will do
stage2_headers = {
'CSRF_NONCE': self.headers['CSRF_NONCE'], # required!
'Content-Type': f"multipart/form-data; boundary={boundary}" # boundary
}

# fields must be filled in this order precisely !!

# 1. Master_URL
fields = {
"Master_URL": master_url
}

# 2. CacheDescriptor_array
cacheDescriptor_array = []
for i in range(len(file_basenames)):
cacheDescriptor_array.append(f"{stream_ids[i]}:{filename_ids[i]}:{stream_ids[i]}:{file_sizes[i]}") # file_sizes is optional
fields["CacheDescriptor_array"] = ";".join(cacheDescriptor_array)

# 3. Files
for i in range(len(file_basenames)):
fields[filename_ids[i]] = (f"{file_basenames[i]}", file_contents[i], file_mimetypes[i])

body, header = encode_multipart_formdata(fields, boundary) # header not used
stage2_resp = self.callWindchillPOSTraw(stage2_url, body, stage2_headers)

if stage2_resp == None:
print(f"Stage 2 upload failed.")
self.documentUndoCheckOut(documentCheckOut['ID'])
return None

print("Stage 2 succeded.")

# ------------------------------------------------------------------
# STAGE 3: Create Document with Primary Content and attachments
# ------------------------------------------------------------------
print("Stage 3: Creating Document with contents...")
create_doc_url = self.url + "/servlet/odata/DocMgmt/Documents"

contentInfos = stage2_resp["contentInfos"]

data3 = {
"ContentInfo": [
# {
# "StreamId" : stream_id,
# "EncodedInfo" : encodedInfo,
# "FileName" : basefilename,
# "PrimaryContent" : True,
# "MimeType" : "application/pdf",
# "FileSize" : fileSize
# }
]
}

primaryContent = True # first one is primary
for i in range(len(contentInfos)):
data3["ContentInfo"].append({
"StreamId" : contentInfos[i]["streamId"],
"EncodedInfo" : contentInfos[i]["encodedInfo"],
"FileName" : file_basenames[i],
"PrimaryContent" : primaryContent,
"MimeType" : file_mimetypes[i],
"FileSize" : contentInfos[i]["fileSize"]
})
primaryContent = False # the others are attachemnts

# Now modify the document doc_id (not the checked out one)!!!!
stage3_url = self.url + f"/servlet/odata/v7/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UploadStage3Action"
response3 = self.callWindchillPOST(stage3_url, data3)

# undo the checkout to remove the working copy
full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents('{id}')/PTC.DocMgmt.UndoCheckOut"
responseUndoCheckout = self.callWindchillPATCH(full_url, data)

if response3 == None:
print(f"Stage 3 upload failed.")
return None

print(f"Stage 3 upload succeded.")


 

1 reply

dgarcia12-AmethystAuthorAnswer
12-Amethyst
July 28, 2026

I was able to solve this. Here is what I did:

  1. Create a new Document without primary content nor attachments.
  2. Rename and checkout
  3. Follow the 3 stages upload content procedure but:
    • In stage 2 use encode_multipart_formdata function from  urlib3
    • In stage 3 use the 0.1 iteration, not to the checked out one
  4. Undo the checkout

Here is the (edited) code I used         

    # get product_id and folder_id first

dataDocument = {
"Name": name,
"Description": name,
# "Number": number, # rename later
"Revision": revision,
"Folder@odata.bind": f"Folders('{folder_id}')",
"Context@odata.bind": f"Containers('{product_id}')"
}

# create empty Design document 0.1
full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents"
response = self.callWindchillPOST(full_url, dataDocument)

if response == None:
print("Error creating document")
return None

# rename document
doc_id = response['ID']

full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UpdateCommonProperties"

# The payload specifies the updates for common attributes.
payload = {
"Updates": {
"Name": newName,
"Number": newNumber
}
}
data = self.callWindchillPOST(full_url, payload) # Rename

# now use the 3 stage upload

# fill file_paths[] with the file paths to the attachemnts.
fill_paths[0] = "C:\\ ... \\primary_content.pdf"
fill_paths[1] = "C:\\ ... \\attachement1.pdf"
fill_paths[2] = "C:\\ ... \\attachement2.pdf"

# keep basenames, sizes, contents and mimetypes
file_basenames = []
file_sizes = []
file_contents = []
file_mimetypes = []
for path in file_paths:
if not os.path.exists(path):
print(f"Error: Local file '{path}' not found.")
return None
else:
file_basenames.append(os.path.basename(path))
file_sizes.append(os.path.getsize(path))
file_contents.append(open(path, "rb").read())
file_mimetypes.append(mimetypes.guess_type(path)[0])


# Checkout Document
documentCheckOut = self.documentCheckOut(doc_id)
if documentCheckOut == None:
print(f"Error checking out document {number}")
return None

# ------------------------------------------------------------------
# STAGE 1: Request upload URL and StreamID from Windchill
# ------------------------------------------------------------------
print("Stage 1: Initializing content upload session...")
stage1_url = self.url + f"/servlet/odata/v7/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UploadStage1Action"
stage1_payload = {
"NoOfFiles": len(file_paths) # total number of files
}
stage1_resp = self.callWindchillPOST(stage1_url, stage1_payload)
if not stage1_resp or 'value' not in stage1_resp or len(stage1_resp['value']) == 0:
print("Failed to initialize Stage 1 upload.")
self.documentUndoCheckOut(documentCheckOut['ID'])
return None
print("Stage 1 initialized successfully.")

# ------------------------------------------------------------------
# STAGE 2: Upload primary file bytes to the target URL
# ------------------------------------------------------------------
print("Stage 2: Uploading primary file content...")

stage2_url = stage1_resp['value'][0]['ReplicaUrl']
master_url = stage1_resp['value'][0]['MasterUrl']
stream_ids = stage1_resp['value'][0]['StreamIds']
filename_ids = stage1_resp['value'][0]['FileNames']

# create multipart request
boundary = "99--77665544332211" # any separator will do
stage2_headers = {
'CSRF_NONCE': self.headers['CSRF_NONCE'], # required!
'Content-Type': f"multipart/form-data; boundary={boundary}" # boundary
}

# fields must be filled in this order precisely !!

# 1. Master_URL
fields = {
"Master_URL": master_url
}

# 2. CacheDescriptor_array
cacheDescriptor_array = []
for i in range(len(file_basenames)):
cacheDescriptor_array.append(f"{stream_ids[i]}:{filename_ids[i]}:{stream_ids[i]}:{file_sizes[i]}") # file_sizes is optional
fields["CacheDescriptor_array"] = ";".join(cacheDescriptor_array)

# 3. Files
for i in range(len(file_basenames)):
fields[filename_ids[i]] = (f"{file_basenames[i]}", file_contents[i], file_mimetypes[i])

body, header = encode_multipart_formdata(fields, boundary) # header not used
stage2_resp = self.callWindchillPOSTraw(stage2_url, body, stage2_headers)

if stage2_resp == None:
print(f"Stage 2 upload failed.")
self.documentUndoCheckOut(documentCheckOut['ID'])
return None

print("Stage 2 succeded.")

# ------------------------------------------------------------------
# STAGE 3: Create Document with Primary Content and attachments
# ------------------------------------------------------------------
print("Stage 3: Creating Document with contents...")
create_doc_url = self.url + "/servlet/odata/DocMgmt/Documents"

contentInfos = stage2_resp["contentInfos"]

data3 = {
"ContentInfo": [
# {
# "StreamId" : stream_id,
# "EncodedInfo" : encodedInfo,
# "FileName" : basefilename,
# "PrimaryContent" : True,
# "MimeType" : "application/pdf",
# "FileSize" : fileSize
# }
]
}

primaryContent = True # first one is primary
for i in range(len(contentInfos)):
data3["ContentInfo"].append({
"StreamId" : contentInfos[i]["streamId"],
"EncodedInfo" : contentInfos[i]["encodedInfo"],
"FileName" : file_basenames[i],
"PrimaryContent" : primaryContent,
"MimeType" : file_mimetypes[i],
"FileSize" : contentInfos[i]["fileSize"]
})
primaryContent = False # the others are attachemnts

# Now modify the document doc_id (not the checked out one)!!!!
stage3_url = self.url + f"/servlet/odata/v7/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UploadStage3Action"
response3 = self.callWindchillPOST(stage3_url, data3)

# undo the checkout to remove the working copy
full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents('{id}')/PTC.DocMgmt.UndoCheckOut"
responseUndoCheckout = self.callWindchillPATCH(full_url, data)

if response3 == None:
print(f"Stage 3 upload failed.")
return None

print(f"Stage 3 upload succeded.")