Project Workflow Integration
The Project Workflow Integration API provides an asynchronous flow that uploads a file from an external system, automatically runs the workflow you configured on the project, lets you poll for the processing status, and returns the result as a ZIP.
When a file is uploaded, it is processed automatically while the project's active workflow is running. The caller does not need to send a separate "start execution" request.
All API requests require API key authentication.
Prerequisites
- An API key must be issued. See the Authentication page.
- Identify the project ID that will receive the file. Use the
publicIdvalue returned byGET /projects/summarybelow. - The project's active workflow must be running. If it is stopped, the upload will succeed but analysis will not start, so start it from the project's Workflows menu in advance.
- The API key user must hold administrator permission on that project. Without it, the upload returns
403.
Authentication
All API requests must include an API key in the HTTP header. The API key follows Bearer token authentication.
| Header | Value |
|---|---|
Authorization |
Bearer {API_KEY} |
Replace {API_KEY} with your actual issued API key in the request.
End-to-end Flow
This API works as a three-step asynchronous flow.
- Upload the file — Call
POST /projects/{projectId}/documents/uploadand receive adocumentUuid. - Poll the status — Call
GET /external/documents/{uuid}/statusperiodically to check progress. Stop polling once it reaches a terminal status (succeededorfailed). - Download the result — Once status is
succeeded, callGET /external/documents/{uuid}/exportto download the result ZIP.
Endpoints
`GET /projects/summary`
Returns the external ID (publicId) and name (name) of the projects owned by the authenticated API key client.
Use the publicId value from this response as the projectId path parameter of the file upload request. Add includeFolders=true to also receive each project's folder list, which you can use when specifying folderId on upload.
This API returns only the projects you own. An administrator who is not the owner will not see the project in this list even though they can upload to it — ask the project owner for the ID in that case.
Request Examples
cURL
# Projects only
curl -X GET 'https://origin.synapfoundry.ai/api/v1/projects/summary' \
-H 'Authorization: Bearer {API_KEY}'
# Including folders
curl -X GET 'https://origin.synapfoundry.ai/api/v1/projects/summary?includeFolders=true' \
-H 'Authorization: Bearer {API_KEY}'Python
import requests
api_key = "{API_KEY}"
url = "https://origin.synapfoundry.ai/api/v1/projects/summary"
headers = { "Authorization": f"Bearer {api_key}" }
response = requests.get(url, headers=headers)
projects = response.json()Node.js
const axios = require('axios');
const apiKey = "{API_KEY}";
const url = 'https://origin.synapfoundry.ai/api/v1/projects/summary';
axios.get(url, {
headers: { 'Authorization': `Bearer ${apiKey}` }
}).then(response => {
console.log(response.data);
}).catch(error => {
console.error('Error:', error.response?.data || error.message);
});Headers
| Parameter | Type | Description |
|---|---|---|
| Authorization | string |
Bearer token for API request authentication. Must be provided in the format Bearer {API_KEY}. |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| includeFolders | boolean |
No | If true, each project includes its folder list (folders). Defaults to false. |
Success Responses
| Status Code | Media Type | Description |
|---|---|---|
| 200 OK | application/json |
Returns the list of owned projects. |
Response Example (includeFolders=true)
[
{
"publicId": "Ab1Cd2Ef3G",
"name": "My Project",
"folders": [
{ "id": 3, "name": "Contracts", "parentFolderId": null },
{ "id": 4, "name": "2026", "parentFolderId": 3 }
]
},
{
"publicId": "Xy9Zw8Vt7U",
"name": "Analysis Project",
"folders": []
}
]Response Fields
| Field | Type | Description |
|---|---|---|
| publicId | string |
The external identifier of the project (10-character alphanumeric string). Use this value as the projectId path parameter of the file upload request. |
| name | string |
The name of the project. |
| folders | array |
Included only when includeFolders=true. The list of folders in the project. |
| folders[].id | number |
The folder ID. Use this value as the folderId query parameter on upload. |
| folders[].name | string |
The folder name. |
| folders[].parentFolderId | number | null |
The parent folder's ID, or null for top-level folders. |
Error Responses
| Status Code | Message | Description |
|---|---|---|
401 Unauthorized |
- | Returned when the request is made with an invalid or expired API key. |
500 Internal Server Error |
Internal server error |
Returned when request processing fails due to an unexpected internal server issue. |
`POST /projects/{projectId}/documents/upload`
Uploads a file to the project. If the project's active workflow is running, the backend automatically triggers that workflow. Use the documentUuid returned in the response for the subsequent status polling and export steps.
Request Examples
cURL
curl -X POST 'https://origin.synapfoundry.ai/api/v1/projects/{PROJECT_ID}/documents/upload' \
-H 'Authorization: Bearer {API_KEY}' \
-F 'file=@{FILE_PATH}'Python
import requests
api_key = "{API_KEY}"
file_path = "/path/to/your/file.ext"
url = "https://origin.synapfoundry.ai/api/v1/projects/{PROJECT_ID}/documents/upload"
headers = { "Authorization": f"Bearer {api_key}" }
with open(file_path, "rb") as file:
files = { "file": file }
response = requests.post(url, headers=headers, files=files)
document_uuid = response.json()["documentUuid"]Node.js
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');
const apiKey = "{API_KEY}";
const projectId = "{PROJECT_ID}";
const filePath = "/path/to/your/file.ext";
const url = `https://origin.synapfoundry.ai/api/v1/projects/${projectId}/documents/upload`;
const form = new FormData();
form.append('file', fs.createReadStream(filePath));
const headers = {
...form.getHeaders(),
'Authorization': `Bearer ${apiKey}`,
};
axios.post(url, form, { headers }).then(response => {
const documentUuid = response.data.documentUuid;
console.log(documentUuid);
}).catch(error => {
console.error('Error:', error.response?.data || error.message);
});Headers
| Parameter | Type | Description |
|---|---|---|
| Authorization | string |
Bearer token for API request authentication. Must be provided in the format Bearer {API_KEY}. |
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| projectId | string |
Yes | The external ID of the project to upload to (10-character alphanumeric string). Use the publicId value returned by GET /projects/summary. |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| folderId | number |
No | The ID (positive integer) of the folder to place the file in. Use a folders[].id value returned by GET /projects/summary?includeFolders=true. If omitted, the file is uploaded to the project root. Only folders belonging to that project can be specified. |
Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| file | file |
Yes | The single file to upload. Sent via the file field of multipart/form-data. |
Success Responses
| Status Code | Media Type | Description |
|---|---|---|
| 201 Created | application/json |
Returns the identifiers of the created document on successful upload. |
Response Example
{
"documentId": 1289,
"documentUuid": "550e8400-e29b-41d4-a716-446655440000",
"projectPublicId": "Ab1Cd2Ef3G"
}Response Fields
| Field | Type | Description |
|---|---|---|
| documentId | number |
Internal identifier of the document. Not used in later steps. |
| documentUuid | string |
The document UUID. Used for status polling and export. |
| projectPublicId | string |
The external ID of the project the document was uploaded to. Same value as the projectId in the request path. |
Error Responses
| Status Code | Message | Description |
|---|---|---|
400 Bad Request |
업로드할 파일이 필요합니다. (An upload file is required.) |
Returned when the file field cannot be found in the multipart/form-data request. |
400 Bad Request |
folderId 는 양의 정수여야 합니다. (folderId must be a positive integer.) |
Returned when folderId is not an integer or is zero or negative. |
401 Unauthorized |
"API Key authentication failed. Please use a valid API Key for your request." |
Returned when the API key is missing or invalid. |
403 Forbidden |
Project context could not be resolved |
Returned when projectId is not a 10-character alphanumeric string, or no project matches it. The two cases are not distinguished. |
403 Forbidden |
Permission denied: data.access.all |
Returned when the project exists but the API key user has no data access permission on it. |
403 Forbidden |
Admin permission required for this project |
Returned when the user has data access but is not an administrator of the project. |
404 Not Found |
폴더를 찾을 수 없습니다. (Folder not found.) |
Returned when no folder matches folderId or it does not belong to this project. The two cases are not distinguished. |
413 Payload Too Large |
File size exceeds 100MB. |
Returned when the size of a single uploaded file exceeds the maximum size (100MB) allowed by the server. |
415 Unsupported Media Type |
Unsupported file format. |
Returned when the format of the uploaded file is not supported by the server. Check Supported Formats. |
`GET /external/documents/{uuid}/status`
Returns the workflow processing status of the uploaded document as a simple flag. Stop polling immediately once the status reaches a terminal value.
Status Values
| Value | Meaning | terminal? |
|---|---|---|
pending |
Waiting to be processed | no |
processing |
In progress | no |
review_pending |
Awaiting human review (HITL) | no |
succeeded |
Completed — result is ready to download | yes |
failed |
Failed / aborted / out of credits | yes |
Clients should treat any unknown value as terminal and stop polling. The backend safely normalizes unknown internal states to
failed, but defensive handling is recommended.
Recommended Polling Policy
- Interval: Start at 5 seconds with exponential backoff (e.g., 5s → 10s → 20s → up to 60s).
- Maximum duration: Varies by document size and workflow, but typically within 30 minutes. Contact operations if it exceeds.
- Retries: Retry
5xxresponses with backoff. Do not retry4xxresponses.
Request Examples
cURL
curl -X GET 'https://origin.synapfoundry.ai/api/v1/external/documents/{DOC_UUID}/status' \
-H 'Authorization: Bearer {API_KEY}'Python
import requests
import time
api_key = "{API_KEY}"
url = "https://origin.synapfoundry.ai/api/v1/external/documents/{DOC_UUID}/status"
headers = { "Authorization": f"Bearer {api_key}" }
interval = 5
max_seconds = 1800 # 30-minute safety cap
elapsed = 0
while elapsed < max_seconds:
response = requests.get(url, headers=headers)
if response.status_code >= 500:
# 5xx: retry after backoff
time.sleep(interval)
elapsed += interval
interval = min(interval * 2, 60)
continue
response.raise_for_status() # 4xx: raise immediately and stop polling
status = response.json()["status"]
if status in ("succeeded", "failed"):
break
time.sleep(interval)
elapsed += interval
interval = min(interval * 2, 60)
else:
raise TimeoutError("Polling exceeded 30 minutes.")Node.js
const axios = require('axios');
const apiKey = "{API_KEY}";
const docUuid = "{DOC_UUID}";
const url = `https://origin.synapfoundry.ai/api/v1/external/documents/${docUuid}/status`;
async function pollStatus() {
let interval = 5000;
const maxMs = 30 * 60 * 1000; // 30-minute safety cap
let elapsed = 0;
while (elapsed < maxMs) {
try {
const { data } = await axios.get(url, {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
if (data.status === 'succeeded' || data.status === 'failed') {
return data.status;
}
} catch (err) {
const code = err.response?.status;
if (code && code >= 400 && code < 500) {
// 4xx: do not retry — stop polling
throw err;
}
// 5xx / network errors: retry after backoff
}
await new Promise(r => setTimeout(r, interval));
elapsed += interval;
interval = Math.min(interval * 2, 60000);
}
throw new Error('Polling exceeded 30 minutes.');
}Headers
| Parameter | Type | Description |
|---|---|---|
| Authorization | string |
Bearer token for API request authentication. Must be provided in the format Bearer {API_KEY}. |
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| uuid | string |
Yes | The documentUuid returned from the upload response. |
Success Responses
| Status Code | Media Type | Description |
|---|---|---|
| 200 OK | application/json |
Returns the current processing status. |
Response Example
{ "status": "processing" }Error Responses
| Status Code | Message | Description |
|---|---|---|
401 Unauthorized |
"API Key authentication failed. Please use a valid API Key for your request." |
Returned when the API key is missing or invalid. |
404 Not Found |
{ "statusCode": 404, "message": "Not Found" } |
Returned identically when the UUID does not exist, does not belong to the caller, or is malformed (to prevent information disclosure). Stop polling. |
`GET /external/documents/{uuid}/export`
Downloads the result of the workflow's final component as a ZIP for a document that completed successfully. Call this right after the polling API returns succeeded.
ZIP Contents by Final Component
The contents of the ZIP differ depending on the workflow's final output component.
| Final Component | ZIP Contents |
|---|---|
| Analyze | The cached analysis result ZIP as-is |
| Review | The review-completed result ZIP |
| Transform Analysis Result | Conversion result files bundled into a single ZIP |
| LLM processing | A ZIP containing the LLM response text as a single .md or .txt entry |
Because the internal layout differs per component, integration clients are recommended to either know the final output component type in advance or process the ZIP entries generically (by iterating entries).
Request Examples
cURL
curl -L 'https://origin.synapfoundry.ai/api/v1/external/documents/{DOC_UUID}/export' \
-H 'Authorization: Bearer {API_KEY}' \
-o result.zipPython
import requests
api_key = "{API_KEY}"
url = "https://origin.synapfoundry.ai/api/v1/external/documents/{DOC_UUID}/export"
response = requests.get(url, headers={ "Authorization": f"Bearer {api_key}" })
with open("result.zip", "wb") as f:
f.write(response.content)Node.js
const axios = require('axios');
const fs = require('fs');
const apiKey = "{API_KEY}";
const docUuid = "{DOC_UUID}";
const url = `https://origin.synapfoundry.ai/api/v1/external/documents/${docUuid}/export`;
axios.get(url, {
headers: { 'Authorization': `Bearer ${apiKey}` },
responseType: 'stream'
}).then(response => {
response.data.pipe(fs.createWriteStream('result.zip'));
}).catch(error => {
console.error('Error:', error.response?.data || error.message);
});Headers
| Parameter | Type | Description |
|---|---|---|
| Authorization | string |
Bearer token for API request authentication. Must be provided in the format Bearer {API_KEY}. |
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| uuid | string |
Yes | The documentUuid returned from the upload response. |
Success Responses
| Status Code | Media Type | Description |
|---|---|---|
| 200 OK | application/zip |
Returns the workflow result ZIP. The response also includes Content-Disposition: attachment; filename="<uuid>.zip". |
Error Responses
| Status Code | Message | Description |
|---|---|---|
401 Unauthorized |
"API Key authentication failed. Please use a valid API Key for your request." |
Returned when the API key is missing or invalid. |
404 Not Found |
{ "statusCode": 404, "message": "Not Found" } |
Returned identically when the UUID does not exist, does not belong to the caller, or is malformed. |
409 Conflict |
{ "code": "NOT_EXPORTABLE", "status": "<current status>" } |
Document is not yet exportable. The object on the left is the entire response body (no statusCode or message wrapper). Go back to the polling API, wait for succeeded, and retry. |
500 Internal Server Error |
Internal server error |
A transient internal error. Retry with exponential backoff. |