How to update a page with Python using REST API?
Asked by Nick Hilton:

I would like to write content to an existing page using the REST API, here's my current script:

```
#!/usr/bin/python## Reference: http://isbullsh.it/2012/06/Rest-api-in-python/#import sysimport getpassimport jsonimport requestsBASE_URL = "http://confluence.XXXXXX.com/rest/api/content"PAGEID = 9470040def main():    username = raw_input('login: ')    passwd = getpass.getpass()    url = "{base}/{pageid}".format(        base = BASE_URL, pageid = PAGEID)    data = json.dumps(    {        'id' : '%d' % PAGEID,        'type' : 'page',        'title' : 'Sandbox',        'space' : {'key' : 'HSWc'},        'body' :        {            'storage' :            {                'representation' : 'storage',                'value' : """&lt;table&gt;    &lt;tbody&gt;        &lt;tr&gt;            &lt;th&gt;Hello&lt;/th&gt;            &lt;th&gt;World&lt;/th&gt;        &lt;/tr&gt;        &lt;tr&gt;            &lt;td&gt;Nick&lt;/td&gt;            &lt;td&gt;Rocks!&lt;/td&gt;        &lt;/tr&gt;    &lt;/tbody&gt;&lt;/table&gt;"""            }        }    })    r = requests.put(        url,        data = data,        auth = (username, passwd),        headers = {            'Content-Type' : 'application/json',            'Accept' : 'application/json'            }    )    r.raise_for_status()if __name__ == "__main__" : main()
```

But I'm getting "requests.exceptions.HTTPError: 400 Client Error: Bad Request"

There currently isn't any working documentation for using the REST API. Please help!


Answers:
author: Nick Hilton
body: I've now got a working example. The documentation does not specify all the required fields to make this work, but it does state that the next version number must be specified.

So root cause to my failure was not explicitly specifying the next version number.

After I corrected that, the newly modified page was orphaned (<https://jira.atlassian.com/browse/CRA-487>). But as [sash](https://answers.atlassian.com/questions/users?username=sash) pointed out, I found the same workaround. Below is my complete working example:

```
$ python write_page.py -husage: write_page.py [-h] [-u USER] [-t TITLE] [-f FILE] pageid [html]positional arguments:  pageid                Specify the Conflunce page id to overwrite  html                  Write the immediate html string to confluence pageoptional arguments:  -h, --help            show this help message and exit  -u USER, --user USER  Specify the username to log into Confluence  -t TITLE, --title TITLE                        Specify a new title  -f FILE, --file FILE  Write the contents of FILE to the confluence page
```

```
import argparseimport getpassimport sysimport jsonimport keyringimport requests#-----------------------------------------------------------------------------# GlobalsBASE_URL = "http://confluence.XXXXX.com/rest/api/content"VIEW_URL = "http://confluence.XXXXX.com/pages/viewpage.action?pageId="def pprint(data):    '''    Pretty prints json data.    '''    print json.dumps(        data,        sort_keys = True,        indent = 4,        separators = (', ', ' : '))def get_page_ancestors(auth, pageid):    # Get basic page information plus the ancestors property    url = '{base}/{pageid}?expand=ancestors'.format(        base = BASE_URL,        pageid = pageid)    r = requests.get(url, auth = auth)    r.raise_for_status()    return r.json()['ancestors']def get_page_info(auth, pageid):    url = '{base}/{pageid}'.format(        base = BASE_URL,        pageid = pageid)    r = requests.get(url, auth = auth)    r.raise_for_status()    return r.json()def write_data(auth, html, pageid, title = None):    info = get_page_info(auth, pageid)    ver = int(info['version']['number']) + 1    ancestors = get_page_ancestors(auth, pageid)    anc = ancestors[-1]    del anc['_links']    del anc['_expandable']    del anc['extensions']    if title is not None:        info['title'] = title    data = {        'id' : str(pageid),        'type' : 'page',        'title' : info['title'],        'version' : {'number' : ver},        'ancestors' : [anc],        'body'  : {            'storage' :            {                'representation' : 'storage',                'value' : str(html),            }        }    }    data = json.dumps(data)    url = '{base}/{pageid}'.format(base = BASE_URL, pageid = pageid)    r = requests.put(        url,        data = data,        auth = auth,        headers = { 'Content-Type' : 'application/json' }    )    r.raise_for_status()    print "Wrote '%s' version %d" % (info['title'], ver)    print "URL: %s%d" % (VIEW_URL, pageid)def get_login(username = None):    '''    Get the password for username out of the keyring.    '''    if username is None:        username = getpass.getuser()    passwd = keyring.get_password('confluence_script', username)    if passwd is None:        passwd = getpass.getpass()        keyring.set_password('confluence_script', username, passwd)    return (username, passwd)def main():    parser = argparse.ArgumentParser()    parser.add_argument(        "-u",        "--user",        default = getpass.getuser(),        help = "Specify the username to log into Confluence")    parser.add_argument(        "-t",        "--title",        default = None,        type = str,        help = "Specify a new title")    parser.add_argument(        "-f",        "--file",        default = None,        type = str,        help = "Write the contents of FILE to the confluence page")    parser.add_argument(        "pageid",        type = int,        help = "Specify the Conflunce page id to overwrite")    parser.add_argument(        "html",        type = str,        default = None,        nargs = '?',        help = "Write the immediate html string to confluence page")    options = parser.parse_args()    auth = get_login(options.user)    if options.html is not None and options.file is not None:        raise RuntimeError(            "Can't specify both a file and immediate html to write to page!")    if options.html:        html = options.html    else:        with open(options.file, 'r') as fd:            html = fd.read()    write_data(auth, html, options.pageid, options.title)if __name__ == "__main__" : main()
```

comments: author: absciexbuild
body: Thanks for posting this, which saved me a great deal of time. One thing to be careful of is if you are renaming the page by giving it a new title, then that title cannot be the same as the any of the titles in the space. REST API will return a 400 error if there's a name clash.


author: Dirce Richards
body: Hi Nick,

I am using your example. My code writes to the Confluence page only as text, I can't get Confluence to display it as a table. For example, I can write this:

\<table\>\<tbody\>\<tr\>\<th\>a\</th\>\<th\>b\</th\>\<th\>c\</th\>\<th\>d\</th\>\<th\>\<p\>e\</p\>\</th\>\</tr\>\<tr\>\<td colspan="1"\>1\</td\>\<td colspan="1"\>2\</td\>\<td colspan="1"\>3\</td\>\<td colspan="1"\>4\</td\>\<td colspan="1"\>5\</td\>\</tr\>\</tbody\>\</table\>

or I tried adding html elements to it, and it writes this:

\<!DOCTYPE html\> \<html\> \<head\> \<title\>Page Title\</title\> \</head\> \<body\> \<h1\>My First Heading\</h1\> \<p\>My first paragraph.\</p\> \</body\> \</html\>

If you are able to view this as a table, can you please tell me, what did you supply in the 'html' argument in your last example?

Thanks,

Dirce


author: Dan McMahill
body: Nick,

Thanks so much for posting this. It is very helpful. One thing I have found is that one html file may upload with no issues while a different html file throws a

400 Client Error: Bad Request for url: ....

I think it may have to do with any errors in the html, but I'm not sure about that. Have you run into this and do you have any solutions?

Thanks

-Dan


author: Alex Medved _ConfiForms_
body: [@Dan McMahill](/t5/user/viewprofilepage/user-id/1194072)

You could not post \*any\* HTML, it expects a [storage format](https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html) afaik


author: Dan McMahill
body: I should have said storage format, not html. But the problem is the same. If the storage format file has any bug at all, it won't upload. Maybe there is a way to validate the file locally?


author: Alex Medved _ConfiForms_
body: May be you can try to convert it: <https://developer.atlassian.com/cloud/confluence/rest/#api-contentbody-convert-to-post>


author: Keyan Kazemian
body: [@Dan McMahill](/t5/user/viewprofilepage/user-id/1194072) I'm running into the same problem. Did you ever resolve this issue?


author: Stan Ry
body: Following code worked for me.

I slightly modified the original code by getting rid of \`auth\` and changing it to session \`s\` variable. For some reason, I had to replace invocations o resquests module instances to HTTP session instances so that requests.get() and requests.post() becomes s.get() and s.post(). Otherwise I had been facing with SSL issues trying to run the code. My SSL connection is verified at proxy level, so SSL connection does become invalid. To ignore that I had to use:

```
requests.packages.urllib3.disable_warnings()
```

I am running it in Jupyter using a 64 bit Anaconda 3.

To run the code you have to:

1. Download Anaconda 3. Any will do, but 64-bit version is probably your best friend.

2. When installed, open Anaconda Navigator. It may take a whhhhhhiiiile before the Navigator starts especially if you are behind some proxy (it seems to fetch some external data which may be restricted on the proxy, like in my case).

3. From the Navigator window open Jupyter. It will start in your default browser.

4. In the started Jypyter tab (default address is http://localhost:8888/lab) create a new py file, rename it by right-clicking in the left navigation panel to whatever you like. Something like post_page.py will do.

5. Paste the code line by line into the notebook file.

6. Any Shift+Enter for the entered line executes the block of code. Any plain Enter just adds the edits to the specific code block.

You can execute the code line by line. I stripped off the _main_() call because I don't know how to call the main module from the notebook.

It seems wise to run this as architected by the author, but I failed to modify the code so that it runs without SSL issues from a PY file opened in JetBrains PyCharm.

I appreciate if someone fixes that the proper way.

Hope this little guide will help other rookies like me to get started with automated posting to Confluence.

comments: author: Stan Ry
body: ```
import requests
import json
import getpass

requests.packages.urllib3.disable_warnings() #ignoring HTTPS errors

auth = ('StanRy', getpass.getpass()) #put here your Confluence login instead of StanRy

s = requests.Session() #creating a single HTTP session
s.auth = auth
s.verify = False
s.headers = {"Content-Type": "application/json"}

BASE_URL = 'https://confluence.example.net/confluence_instance/rest/api/content'

def pprint(data):
    '''
    Pretty prints json data.
    '''
    print (json.dumps(
           data,
           sort_keys = True,
           indent = 4,
           separators = (', ', ' : '))
           )

def get_page_ancestors(pageid):
# Get basic page information plus the ancestors property

    url = '{base}/{pageid}?expand=ancestors'.format(
        base = BASE_URL,
        pageid = pageid)

    r = s.get(url, auth = auth) #changed requests to 's' -- HTTP session object

    r.raise_for_status()

    return r.json()['ancestors']


def get_page_info(pageid):

    url = '{base}/{pageid}'.format(
        base = BASE_URL,
        pageid = pageid)

    r = s.get(url, auth = auth)

    r.raise_for_status()

    return r.json()


def write_data(html, pageid, title = None):

    info = get_page_info(pageid)

    ver = int(info['version']['number']) + 1

    ancestors = get_page_ancestors(pageid)

    anc = ancestors[-1]
    del anc['_links']
    del anc['_expandable']
    del anc['extensions']

    if title is not None:
       info['title'] = title

    data = {
        'id' : str(pageid),
        'type' : 'page',
        'title' : info['title'],
        'version' : {'number' : ver},
        'ancestors' : [anc],
        'body' : {
            'storage' :
            {
                'representation' : 'storage',
                'value' : str(html),
            }
        }
     }

data = json.dumps(data)

url = '{base}/{pageid}'.format(base = BASE_URL, pageid = pageid)

r = s.put(
    url,
    data = data,
    headers = { 'Content-Type' : 'application/json' }
)

r.raise_for_status()

print ("Wrote '%s' version %d" % (info['title'], ver))
print ("URL: %s%s" % (VIEW_URL, pageid)) #changed %d to % since I submit pageId as a string 

#reading file with HTML data
with open(r'C:\\Users\\StanRy\\AppData\\Local\\Temp\\Test.txt') as htmlf:
htmld = htmlf.read()
print(htmld)

write_data(htmld, '1234567890') #pageId as a string
```


author: Stan Ry
body: Since this is a Python script, please check that all indentations are set correctly. For some reason, pasting Python code into the forum's form breaks all tabulations which are vital for Python.


author: Stan Ry
body: Hi, thanks for the script. Any clues on how to avoid SSL errors when running the script?

I am running this on a test server with non-trusted certificates, and want to get rid of SSL notifications like:

```
requests.exceptions.SSLError: HTTPSConnectionPool(host='confluence.example.net', port=443): Max retries exceeded with url: /confred/rest/api/content/1234567 (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate
verify failed: unable to get local issuer certificate (_ssl.c:1051)')))
```

I have added

```
requests.packages.urllib3.disable_warnings()
```

everywhere before calls of requests, but that didn't help.

Thank you.


author: Alex Medved _ConfiForms_
body: Take a look at the discussions here <https://answers.atlassian.com/questions/312039>

It looks like it the issue is related to missing ancestors in your json

```
"ancestors":[        {           "id":"ID_OF_THE_PARENT_PAGE",         "type":"page",         "title":"TITLE OF THE PARENT PAGE"      }],
```

Java example for quick ref <https://bitbucket.org/jaysee00/confluence-rest-api-example/src/master/src/main/java/com/atlassian/api/examples/Main.java>


author: Philipp Sendek
body: Hey Nick,  

did you happen to come across [this documentation](https://developer.atlassian.com/confdev/confluence-rest-api/confluence-rest-api-examples#ConfluenceRESTAPIExamples-Createanewpage) already?  
Following the example curl-request to create a new page, you also have an example on how to update a page.

I hope this helps!  

Regards,  
Philipp  

<br />

comments: author: Nick Hilton
body: That's the only example I could find on the planet. When I tried it, I got a strange error complaining about the header. I'm not at work today so I can't paste in the error message. But that's the example I'm trying to imitate with the script. It doesn't work.


author: Nick Hilton
body: Also, those example are using strange expressions: \&#39; which I think just means char ' So I think bash doesn't like seeing \&#39;, I've tried just using ' instead but no joy.


author: Philipp Sendek
body: I see. Unfortunately I haven't messed with the API in a while and even then it was only JIRA and read-requests.Since the header is presented, it's strange that this would result in an error considering it matches the type of data provided.Yes, the \&#39; are ' and are probably used for compatibility in case one decides to use single-quotes instead of quotes.


Tags: confluence-development