IT teams and integrators
ConnectGH DMS API Integration Guide
1. About This Guide
This guide is for the IT team or supplier connecting another system to ConnectGH DMS: an ERP that files invoices, a core banking system that stores loan files, an HR system, or a scanning station. It explains how to get an API key, how to call the API, and every call a key can make.
1.1 How it works
- The organisation's ConnectGH DMS administrator creates an API key for your system. ConnectGH must first switch on the API Access module for the organisation.
- The key acts as its own integration account, for example "ERP sync (integration)". It reaches only the folders and actions the administrator chose, from the addresses they allowed, up to a set number of calls a minute, until the key expires.
- Everything your system does is recorded in the organisation's audit log under that account's name.
- A key can never reach people's accounts, settings, users, sharing or backups, or another organisation.
1.2 Conventions
- Base address. Hosted service:
https://dms.connectgh.org/api. An organisation's own server (on-premises):https://<their system address>/api, for examplehttps://dms.customer.com/api. - Requests and responses are JSON unless stated (file uploads and downloads aren't).
- Times are in UTC, in ISO 8601 format, for example
2026-10-08T09:30:00. - Examples use
curl, with the key in the variable$CGH_API_KEY.
2. Getting a Key
Ask the organisation's administrator for a key, telling them:
- what your system will do: Read only (list, search and download), Upload only (add documents; it can't read what's there) or Read & write (also edit details, move documents and add new versions);
- whether it also needs to create subfolders, start workflows on documents or read form submissions;
- which folders it needs; it reaches everything inside them and nothing else;
- your server's public IP address or range, so the key works only from there;
- how many calls a minute it makes at most (120 unless you ask for more, up to 600).
The administrator sees the full key once and passes it to you. It looks like cgh_k7f3a2b9_ followed by 40 more characters. The first part (cgh_k7f3a2b9) identifies the key on their API Keys page.
Note Store the key like a password: in your system's secret store or a password manager, never in source code, a web page, a phone app, email or chat. Use one key per system, so each can be limited and revoked on its own.
Keys expire after 90 days, 6 months or 1 year, as the administrator chose. They are emailed 14 days before; they then rotate the key (a new secret, and the old one stops working at once) or extend it, and give you the new key.
3. Making Calls
Send the key in the Authorization header of every call:
curl -H "Authorization: Bearer $CGH_API_KEY" https://dms.connectgh.org/api/folders
- From your server only. Calls that carry a browser
Originheader are refused, so a key can't be used from web-page code. - Header only. A key in a cookie or in the address is not accepted.
- From allowed addresses. If the key lists addresses, calls from anywhere else are refused.
- At any time of day. An organisation's sign-in hours apply to people, not to keys, so overnight jobs keep running.
3.1 Responses and errors
Successful calls answer 200 OK, or 201 Created when something was made. Errors answer with a status code and a JSON body with a plain-language message:
{"error": "This API key can't be used from this address."}
| Status | Meaning | What to do |
|---|---|---|
| 400 | Something in the request is missing or invalid | Read the message and fix the request |
| 401 | The key is wrong, revoked or expired | Ask the administrator for a new key |
| 403 | Not allowed: API Access is off, a call from an address not on the key's list, a route keys can't use, an action the key wasn't given, or a folder it can't reach | Read the message; ask the administrator to change the key if needed |
| 404 | Not found, or not visible to this key | Check the id; the key may not reach that folder |
| 409 | A conflict, for example an upload that hasn't finished | Read the message |
| 413 | A file is too large, or the organisation's storage is full | Upload in parts (5.5), or ask the administrator |
| 422 | A file was refused by the content check or virus scan | Don't retry the same file |
| 429 | Too many calls this minute | Wait for the Retry-After seconds, then continue |
| 5xx | A problem on the server | Retry later, waiting longer each time |
3.2 Rate limit
Every response carries X-RateLimit-Limit (calls allowed a minute) and X-RateLimit-Remaining (calls left this minute). Over the limit, calls answer 429 with a Retry-After header giving the seconds to wait.
3.3 Unchanged answers
Answers to GET calls carry an ETag header. Send it back in If-None-Match the next time you ask for the same thing: if nothing has changed, the answer is 304 Not Modified with no body, so a system that checks a folder regularly doesn't download the same list again. The call still counts toward the rate limit.
4. What a Key Can Call
A key may call only the routes below, and only those its access opens. Everything else answers 403 with "This route isn't available to API keys."
| Route | Read only | Upload only | Read & write |
|---|---|---|---|
GET /folders, GET /folders/{id} |
Yes | Yes | Yes |
GET /documents, GET /documents/{id}, download a version, GET /tags |
Yes | No | Yes |
Upload: GET /uploads/config, /uploads calls, POST /documents |
No | Yes | Yes |
PATCH /documents/{id}, POST /documents/{id}/move, POST /documents/{id}/versions |
No | No | Yes |
POST /folders |
No | With "Create subfolders" | With "Create subfolders" |
GET /workflow-templates, GET and POST /documents/{id}/workflow-runs |
No | No | With "Start workflows" |
GET /intake-forms, GET /intake-forms/{id}/submissions |
With "Read form submissions" | No | With "Read form submissions" |
Within those routes the key reaches only its folders and what's inside them. The organisation's own modules must be on, too: workflows need Document Workflows, forms need Intake Forms.
5. Folders and Documents
5.1 Finding your folders
curl -H "Authorization: Bearer $CGH_API_KEY" "https://dms.connectgh.org/api/folders"
Returns {"folders": [...]}: the top-level folders the key can reach, A to Z. Each folder has id, name, parentId, path (for example "Finance / Invoices"), documentCount and subfolderCount. Add ?parentId=12 to list the folders inside folder 12.
GET /folders/{id} returns {"folder", "breadcrumb", "subfolders", "documents"} for one folder: its details, its path, its subfolders, and every document directly in it. Add ?documents=0 to leave the documents out when you only need the folder's details and subfolders; for a large folder, list its documents a page at a time instead (5.2).
Note If the key was given a folder that sits inside another folder, it won't appear in the top-level list. Ask the administrator for the folder ids: their API Keys page shows them with each key, for example "Invoices (id 12)", and the example given with a new key opens its first folder. Then call
GET /folders/12.
5.2 Listing and searching documents
curl -H "Authorization: Bearer $CGH_API_KEY" "https://dms.connectgh.org/api/documents?folderId=12&page=1"
Returns {"documents": [...], "page", "pages", "total"}, newest changes first, 20 a page.
| Parameter | Meaning |
|---|---|
folderId |
Documents directly in this folder (not its subfolders) |
page |
Page number, from 1 |
q |
Search words, matched against the title, tags, description and the text inside the files (including scanned pages). Returns up to 50 best matches on one page, across every folder the key reaches; folderId is ignored |
Each document has id, title, description, folderId, folderPath, tags, ownerUsername, createdAt, updatedAt, and its latest file: latestVersionId, latestVersionNumber, latestOriginalFilename, latestFileSize (bytes) and latestMimeType.
GET /documents/{id} returns {"document": {...}} with the same fields plus versions (the latest version: id, versionNumber, originalFilename, fileSize, mimeType, uploadedBy, uploadedAt, changeNote) and attachments.
5.3 Downloading a file
Use the document's latestVersionId:
curl -H "Authorization: Bearer $CGH_API_KEY" -OJ \
"https://dms.connectgh.org/api/documents/345/versions/678/download"
The answer is the file itself, with its original name in Content-Disposition. Keys download the latest version of each document; earlier versions are for the organisation's administrators. If the organisation watermarks downloads, PDFs and images arrive watermarked.
5.4 Uploading a document (up to 100 MB)
Send the file as a multipart form, with the folder to put it in:
curl -H "Authorization: Bearer $CGH_API_KEY" \
-F "folderId=12" -F "title=Invoice 2026-0412" -F "files=@invoice-0412.pdf" \
https://dms.connectgh.org/api/documents
| Field | Meaning |
|---|---|
folderId |
Required: one of the key's folders, or a folder inside them |
files |
The file. Repeat the field for several files; each becomes its own document unless mode=combined, where the first is the document and the rest its attachments |
title |
Optional, when uploading one file; otherwise each document is named after its file |
description |
Optional |
Answers 201 with {"id", "ids", "document", "documents"}: the new document's id (and all ids, for several files). Each file is checked against its type and virus-scanned before it is stored; a refused file answers 422. Allowed types: pdf, docx, doc, dotx, xlsx, xls, csv, pptx, ppt, txt, md, png, jpg, jpeg, tiff, bmp, gif, dxf, dwg, dwt, stl, obj, gltf, glb, ply and fbx.
Note Don't send
tags- keys can't create or apply tags, and the upload is refused. There's no duplicate check: if an upload call times out, list the folder before trying again, so the document isn't stored twice.
5.5 Uploading large files in parts
For files over 100 MB, or on unreliable links, upload in parts and resume after a break. The limits are in GET /uploads/config: chunkBytes (the part size to use, 4 MB), maxUploadBytes (largest file, 2 GB unless the organisation's server is set otherwise) and allowedExtensions.
- Start:
POST /uploadswith{"filename": "statement.pdf", "size": 52428800}. The answer has the upload'sidandchunkBytes. - Send each part in order:
PUT /uploads/{id}with the raw bytes as the body and the headersUpload-Offset: <bytes sent so far>andContent-Type: application/octet-stream. Each answer hasoffset(bytes received) andcomplete. - If an answer is
409"Offset mismatch", it includes theoffsetthe server has; continue from there. After a dropped connection,GET /uploads/{id}gives theoffsetto resume from. - When an answer has
"complete": true, the file has been checked. Ifblockedisn't empty, the file was refused. - File it:
POST /documentswith{"folderId": 12, "uploadIds": ["<id>"], "title": "Statement"}. The answer is as in 5.4.
Unfinished uploads expire after 24 hours. DELETE /uploads/{id} cancels one.
5.6 Adding a new version (Read & write)
POST /documents/{id}/versions with either a multipart file field, or JSON {"uploadId": "<id>"} after uploading in parts, and an optional changeNote. Answers 201 with the updated document; latestVersionNumber is the new version.
5.7 Changing details and moving (Read & write)
PATCH /documents/{id}with{"description": "..."}changes the description. A newtitleis sent to the administrators as a rename request; the title changes once one of them approves it.POST /documents/{id}/movewith{"folderId": 14}moves a document to another of the key's folders.
5.8 Creating a subfolder ("Create subfolders")
POST /folders with {"name": "2026", "parentId": 12}. parentId is required: keys create folders inside their own folders. Answers 201 with {"folder": {...}}. A folder with the same name in the same place answers 400.
6. Workflows and Forms
6.1 Starting a workflow ("Start workflows")
GET /workflow-templateslists the organisation's active workflows:{"templates": [{"id", "name", "description", "stages": [...]}]}.POST /documents/{id}/workflow-runswith{"templateId": 3, "note": "From the ERP"}starts one on a document. Answers201with{"run": {...}}.GET /documents/{id}/workflow-runsreturns{"runs": [...]}, newest first. A run'sstatusisin_progress,completed,rejectedorcancelled; itsstepsshow each step'sname,status,actedByandactedAt.
A document can have one workflow in progress at a time. Check the run's status every few minutes rather than every second.
6.2 Reading form submissions ("Read form submissions")
GET /intake-formslists the organisation's forms:id,name,isActive,submissionCount,newCount, andfields(each withlabel,fieldTypeandkey).GET /intake-forms/{id}/submissions?status=newreturns{"submissions": [...]}, newest first, up to 1,000. Each hasid,reference,status,submittedAt,submitterName,submitterEmail,documentId(the filed PDF) andanswers(the answers, by field key).statuscan benew,in_review,in_progress,approved,declinedorclosed. A signature question's answer has atype:drawn,typed, orthumbprint(withthumbandsource:camera,inkedorreader); the thumbprint image itself isn't in the answers - it's on the filed PDF.
7. Worked Example: Filing Invoices from an ERP
A Python script, using the requests library, that uploads every PDF in a folder into ConnectGH DMS folder 12 in parts, waits when the rate limit is reached, and remembers what it has sent:
import json, os, pathlib, time
import requests
BASE = "https://dms.connectgh.org/api"
HEAD = {"Authorization": f"Bearer {os.environ['CGH_API_KEY']}"}
FOLDER_ID = 12
SENT = pathlib.Path("sent.json")
def call(method, path, **kw):
while True:
r = requests.request(method, BASE + path, headers={**HEAD, **kw.pop("headers", {})},
timeout=300, **kw)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
return r
def upload(path, title):
size = path.stat().st_size
up = call("POST", "/uploads", json={"filename": path.name, "size": size})
up.raise_for_status()
up = up.json()
offset = 0
with path.open("rb") as f:
while offset < size:
f.seek(offset)
r = call("PUT", f"/uploads/{up['id']}", data=f.read(up["chunkBytes"]),
headers={"Upload-Offset": str(offset),
"Content-Type": "application/octet-stream"})
if r.status_code == 409:
offset = r.json()["offset"]
continue
r.raise_for_status()
state = r.json()
offset = state["offset"]
if state.get("blocked"):
raise RuntimeError(f"{path.name} refused: {state['blocked']}")
r = call("POST", "/documents",
json={"folderId": FOLDER_ID, "uploadIds": [up["id"]], "title": title})
r.raise_for_status()
return r.json()["id"]
sent = json.loads(SENT.read_text()) if SENT.exists() else {}
for pdf in sorted(pathlib.Path("outbox").glob("*.pdf")):
if pdf.name not in sent:
sent[pdf.name] = upload(pdf, pdf.stem)
SENT.write_text(json.dumps(sent, indent=1))
print("filed", pdf.name, "as document", sent[pdf.name])
8. Good Practice
- One key per system, with only the folders and access it needs, and its server's address on the key.
- Keep the key secret. If it may have leaked, ask the administrator to rotate or revoke it at once; every call made with it is in the key's Activity.
- Handle 429 and 5xx by waiting and retrying, longer each time. Don't retry 400, 403 or 422 unchanged.
- Remember what you've sent, as in the example, so a retry never files a document twice.
- Plan for rotation. Read the key from configuration, so a new key can be put in without changing code.
- Use search sparingly. Listing a folder page by page is lighter than repeated searches.
- Don't download what hasn't changed. Send back the
ETagyou were given (3.3). - Test against one folder first, then widen the key once the connection works.
9. Getting Help
The organisation's administrator can see each key's status and its last 200 calls (with the result of each) under Admin tools → API Keys → Activity; start there when something fails.
For help with the API, contact ConnectGH support: info@connectgh.org · Tel +233 (0) 35 229 3915 · Mobile +233 (0) 244 086 889 · WhatsApp +233 (0) 209 857 782. Give the key's first part (for example cgh_k7f3a2b9), the time of the call and the error message, never the whole key.