Skip to Content

Projects API

This chapter explains how to manage projects using the Plainly API. A project represents an uploaded After Effects package and within each project you can define one or more templates that expose layers as parameters. These templates serve as blueprints for rendering video variations.

Project lifecycle

After being created, a project can move through several stages due to the project analysis which is needed to determine its validity, structure and contents. The diagram below illustrates the state transitions, while the typical flow like following:

  • After you successfully upload a project, it’s moved to analyzing state.
  • If the analysis succeeds, the project becomes render ready, this means you can create templates based on it and render them.
  • If the analysis fails, the project enters analysis failure state. You can inspect the error details to fix the issues and re-upload the project.
  • In cases where projects are edited, they will be re-analyzed to ensure compatibility with the rendering pipeline.

Uploading an After Effects project

Read packaging guide for instructions on how to prepare an After Effects project files before uploading.

Upload your .zip package containing the project files and any assets via POST /api/v2/projects. The request must be multipart/form-data with a single file field named file and optional project name and description fields.

create-project.sh
curl -u "$PLAINLY_API_KEY:" \ -F file=@my_project.zip \ -F name="My Project" \ -F description="This is my project description" \ https://api.plainlyvideos.com/api/v2/projects

Large uploads may take several seconds. The returned JSON response will contain the ID of the new project and all the other properties of the Project object. You can always use this ID to execute a GET request to /api/v2/project/{projectId} to obtain information about the project, including its current analysis state.

Create project example JSON response

{ "name": "My Project", "id": "00000000-acf3-4d3e-bb48-07036d023220", "size": 32235740, "attributes": { "tags": [] }, "lastModified": "2025-07-30T11:07:02.309Z", "description": "This is my project description", "sharingLinks": null, "createdBy": null, "createdDate": "2025-07-30T11:07:02.309Z", "revisionHistory": null, "sharing": null, "aeVersion": "AE2025", "defaultTemplateId": null, "templates": [], "uploaded": true, "analyzed": false, "strictFontChecks": null, "analysis": { "pending": false, "done": false, "failed": false, "error": null, "upgradeError": null } }

Check the Projects API reference  for a complete list of parameters and options you can use when creating a project.

Checking analysis status

To check the status of your project’s analysis, periodically poll the GET /api/v2/projects/{projectId} endpoint. In the returned Project object, the analyzed field offers a quick way to determine if the analysis is complete and successful. If analyzed is false, the project is either still being processed or has encountered an error.

The analysis object provides additional details, any error details in case the analysis failed.

NameTypeDefault
pendingboolean

Indicates whether the project analysis is still pending or in-progress.

doneboolean

Indicates whether the project analysis has completed successfully.

failedboolean

Indicates whether the project analysis has failed.

error{ code: string; message: string; params?: object; } | null

If the analysis failed, this field contains the error message.

upgradeError{ code: string; message: string; params?: object; } | null

If the upgrade failed, this field contains the error message.

Editing a project

If you need to update an existing project, you can re-upload the ZIP file using the POST /api/v2/projects/{projectId} endpoint. The endpoint accepts the same parameters as the initial upload, allowing you to replace the existing project files with new content. As for the creation, the project will be analyzed after the upload completes.

In the Project object, the revisionHistory field contains the array of all revisions made to a single project.

Currently it is impossible to restore a project to a previous revision.

Retrieving project metadata

If a project is successfully analyzed, you can retrieve its metadata using GET /api/v2/projects/{projectId}/meta. The metadata contains a complete tree of all layers, compositions and assets used in the project. This information is essential for creating templates that expose specific layers as parameters. In addition, it allows you to present to end-users content of a project, including media assets previews, texts and more.

get-project-metadata.sh
curl -u "$PLAINLY_API_KEY:" \ "https://api.plainlyvideos.com/api/v2/projects/$PROJECT_ID/meta"

The meta JSON response includes an array containing every root composition contained in the After Effects project. The composition object contains all the properties of the composition, as well as a complete tree structure of it’s layers:

NameTypeDefault
type"COMPOSITION" | "MEDIA" | "TEXT"

Indicates a type of the item.

idnumber

The unique identifier of the item as reported by After Effects.

internalIdstring

The Plainly internal ID of the item, used for referencing in templates.

namestring

The name of the item as it appears in After Effects.

startTimenumber

The start time of the item in seconds.

inPointnumber

The in-point of the item in seconds.

outPointnumber

The out-point of the item in seconds.

durationnumber

The duration of the item in seconds. Available only for compositions.

workAreaStartnumber

The work area start time of the item in seconds. Available only for compositions.

widthnumber

The width of the item in pixels. Not provided for text layers.

heightnumber

The height of the item in pixels. Not provided for text layers.

guideLayerboolean

Indicates whether the item is a guide layer.

adjustmentLayerboolean

Indicates whether the item is an adjustment layer.

enabledboolean

Indicates whether the item is enabled.

shyboolean

Indicates whether the item is shy.

children(MetaItemProps & { effects?: object[]; mediaType?: "IMAGE" | "VIDEO" | "AUDIO" | "SOLID"; value?: string; font?: string; fontLocation?: string; fontStyle?: string; fontFamily?: string; fontSize?: number; isParagraph?: boolean; preview?: PreviewProps; })[]

The children of the item, which can be compositions or media.

Media layers usually contain a preview object with a link to the media file, which can be used to display a thumbnail or preview image in your application.

NameTypeDefault
expiresAtstring

The expiration time of the preview link in ISO 8601 format.

internalboolean

Indicates whether the preview is internal or public.

false

Get project metadata example JSON response

[ { "type": "COMPOSITION", "internalId": "16215", "name": "Single Product Promo", "id": 16215, "duration": 6.03333333333333, "workAreaStart": 0, "width": 720, "height": 720, "guideLayer": false, "adjustmentLayer": false, "enabled": true, "shy": false, "children": [ { "type": "COMPOSITION", "internalId": "25984", "name": "discount comp", "startTime": 1.36, "inPoint": 1.36, "outPoint": 6.04, "layerName": "discount comp", "id": 25984, "duration": 4.68, "workAreaStart": 0, "width": 1080, "height": 194, "guideLayer": false, "adjustmentLayer": false, "enabled": true, "shy": false, "children": [ { "type": "MEDIA", "internalId": "26045", "name": "boundingBox", "startTime": 0, "inPoint": 0, "outPoint": 60, "value": "#848484", "width": 250, "height": 500, "mediaType": "SOLID", "guideLayer": false, "adjustmentLayer": false, "enabled": false, "shy": true, "effects": [] }, { "type": "TEXT", "internalId": "26001", "name": "EDIT-discount", "startTime": -0.72, "inPoint": 0, "outPoint": 4.68, "value": "30% OFF", "font": "Poppins-Black", "fontLocation": "C:\\Program Files\\Common Files\\Adobe\\Fonts\\Poppins-Black.ttf", "fontStyle": "Black", "fontFamily": "Poppins", "fontSize": 266, "guideLayer": false, "adjustmentLayer": false, "enabled": true, "shy": false, "isParagraph": false, "allCaps": true, "leading": 25.0997009277344, "tracking": 0 } ] }, { "type": "COMPOSITION", "internalId": "26013", "name": "price comp", "startTime": 0.72, "inPoint": 0.72, "outPoint": 6.04, "layerName": "price comp", "id": 26013, "duration": 5.32, "workAreaStart": 0, "width": 1080, "height": 228, "guideLayer": false, "adjustmentLayer": false, "enabled": true, "shy": false, "children": [ { "type": "MEDIA", "internalId": "26043", "name": "boundingBox", "startTime": 0, "inPoint": 0, "outPoint": 60, "value": "#848484", "width": 250, "height": 500, "mediaType": "SOLID", "guideLayer": false, "adjustmentLayer": false, "enabled": false, "shy": true, "effects": [] }, { "type": "TEXT", "internalId": "26028", "name": "EDIT-price", "startTime": -0.72, "inPoint": 0, "outPoint": 5.32, "value": "$199,99", "font": "Poppins-Black", "fontLocation": "C:\\Program Files\\Common Files\\Adobe\\Fonts\\Poppins-Black.ttf", "fontStyle": "Black", "fontFamily": "Poppins", "fontSize": 266, "guideLayer": false, "adjustmentLayer": false, "enabled": true, "shy": false, "isParagraph": false, "allCaps": false, "leading": 31.9737205505371, "tracking": 0 } ] }, { "type": "COMPOSITION", "internalId": "25942", "name": "product pic comp", "startTime": 0, "inPoint": 0, "outPoint": 6.04, "layerName": "product pic comp", "id": 25942, "duration": 6.04, "workAreaStart": 0, "width": 500, "height": 500, "guideLayer": false, "adjustmentLayer": false, "enabled": true, "shy": false, "children": [ { "type": "MEDIA", "internalId": "25954", "name": "EDIT-example 2 product pic", "startTime": 0, "inPoint": 0, "outPoint": 6.04, "value": "(Footage)/Assets/generic%20keyboard.jpg", "width": 500, "height": 500, "mediaType": "IMAGE", "guideLayer": false, "adjustmentLayer": false, "enabled": true, "shy": false, "effects": [ { "type": "EFFECT_CHECKBOX_CONTROL", "name": "STC - Scale to Fit (Maintain Ratio)", "propertyName": "Effects.STC - Scale to Fit (Maintain Ratio).Checkbox", "value": 1, "keyframeValues": [] }, { "type": "EFFECT_CHECKBOX_CONTROL", "name": "STC - Scale to Fill", "propertyName": "Effects.STC - Scale to Fill.Checkbox", "value": 1, "keyframeValues": [] }, { "type": "EFFECT_CHECKBOX_CONTROL", "name": "STC - Restore Original Size", "propertyName": "Effects.STC - Restore Original Size.Checkbox", "value": 0, "keyframeValues": [] }, { "type": "EFFECT_CHECKBOX_CONTROL", "name": "STC - Bypass", "propertyName": "Effects.STC - Bypass.Checkbox", "value": 0, "keyframeValues": [] } ], "preview": { "link": "https://example.com/preview/25954", "expiresAt": "2025-08-12T14:07:39.718489058Z", "internal": false } } ] }, { "type": "MEDIA", "internalId": "25910", "name": "bg", "startTime": 0, "inPoint": 0, "outPoint": 6.04, "value": "#00274a", "width": 800, "height": 800, "mediaType": "SOLID", "guideLayer": false, "adjustmentLayer": false, "enabled": true, "shy": false, "effects": [] } ] } ]

Flattened project metadata

In some cases the tree structure of the project metadata is not perfect when you need to present the available layers in a user interface. In this case you can use the GET /api/v2/projects/{projectId}/meta?responseType=flatten to retrieve a flattened version of the metadata. The response will contain an array of items, each representing a layer or composition, with all the properties of the original metadata.

The only difference is that the children property is not present, and each item contains a compositions property that holds an array of all the compositions that contain that layer or composition. The compositions array contains items with the properties needed to identify a composition, thus allowing you to easily make a certain layer a dynamic parameter in a template.

In addition, each composition descriptor item contains the inCompositionProps object that provides a set of properties related to a layer behavior in a given composition, such as startTime, inPoint, outPoint, and others.

NameTypeDefault
type"COMPOSITION" | "MEDIA" | "TEXT"

Indicates a type of the item.

compositions{ internalId: string; name: string; path: string; inCompositionProps: InCompositionProps; }[]
idnumber

The unique identifier of the item as reported by After Effects.

internalIdstring

The Plainly internal ID of the item, used for referencing in templates.

namestring

The name of the item as it appears in After Effects.

durationnumber

The duration of the item in seconds. Available only for compositions.

workAreaStartnumber

The work area start time of the item in seconds. Available only for compositions.

widthnumber

The width of the item in pixels. Not provided for text layers.

heightnumber

The height of the item in pixels. Not provided for text layers.

effectsobject[]

The collected layer effects.

mediaType"IMAGE" | "VIDEO" | "AUDIO" | "SOLID"

The media type of the item, if applicable.

valuestring

The value of the item, if applicable (e.g., for solids or text).

fontstring

The font used in the text layer, if applicable.

fontLocationstring

The font location on the system, if applicable.

fontStylestring

The font style used in the text layer, if applicable.

fontFamilystring

The font family used in the text layer, if applicable.

fontSizenumber

The font size used in the text layer, if applicable.

isParagraphboolean

Indicates whether the text layer is a paragraph.

previewPreviewProps

The preview link for the media item, if applicable.

Get flattened project metadata example JSON response

[ { "compositions": [], "type": "COMPOSITION", "internalId": "16215", "name": "Single Product Promo", "value": null, "id": 16215, "duration": 6.03333333333333, "workAreaStart": 0.0, "width": 720.0, "height": 720.0, "layerName": null }, { "compositions": [ { "id": 16215, "internalId": "16215", "name": "Single Product Promo", "layerName": null, "path": "Single Product Promo", "inCompositionProps": { "startTime": 1.36, "inPoint": 1.36, "outPoint": 6.04, "guideLayer": false, "enabled": true, "adjustmentLayer": false, "shy": false } } ], "type": "COMPOSITION", "internalId": "25984", "name": "discount comp", "value": null, "id": 25984, "duration": 4.68, "workAreaStart": 0.0, "width": 1080.0, "height": 194.0, "layerName": "discount comp" }, { "compositions": [ { "id": 25984, "internalId": "25984", "name": "discount comp", "layerName": "discount comp", "path": "Single Product Promo->discount comp", "inCompositionProps": { "startTime": 0.0, "inPoint": 0.0, "outPoint": 60.0, "guideLayer": false, "enabled": false, "adjustmentLayer": false, "shy": true } } ], "type": "MEDIA", "internalId": "26045", "name": "boundingBox", "value": "#848484", "width": 250.0, "height": 500.0, "mediaType": "SOLID", "effects": [] }, { "compositions": [ { "id": 25984, "internalId": "25984", "name": "discount comp", "layerName": "discount comp", "path": "Single Product Promo->discount comp", "inCompositionProps": { "startTime": -0.72, "inPoint": 0.0, "outPoint": 4.68, "guideLayer": false, "enabled": true, "adjustmentLayer": false, "shy": false } } ], "type": "TEXT", "internalId": "26001", "name": "EDIT-discount", "value": "30% OFF", "font": "Poppins-Black", "fontLocation": "C:\\Program Files\\Common Files\\Adobe\\Fonts\\Poppins-Black.ttf", "fontStyle": "Black", "fontFamily": "Poppins", "fontSize": 266.0, "isParagraph": false, "allCaps": true, "leading": 25.0997009277344, "tracking": 0.0 }, { "compositions": [ { "id": 16215, "internalId": "16215", "name": "Single Product Promo", "layerName": null, "path": "Single Product Promo", "inCompositionProps": { "startTime": 0.72, "inPoint": 0.72, "outPoint": 6.04, "guideLayer": false, "enabled": true, "adjustmentLayer": false, "shy": false } } ], "type": "COMPOSITION", "internalId": "26013", "name": "price comp", "value": null, "id": 26013, "duration": 5.32, "workAreaStart": 0.0, "width": 1080.0, "height": 228.0, "layerName": "price comp" }, { "compositions": [ { "id": 26013, "internalId": "26013", "name": "price comp", "layerName": "price comp", "path": "Single Product Promo->price comp", "inCompositionProps": { "startTime": 0.0, "inPoint": 0.0, "outPoint": 60.0, "guideLayer": false, "enabled": false, "adjustmentLayer": false, "shy": true } } ], "type": "MEDIA", "internalId": "26043", "name": "boundingBox", "value": "#848484", "width": 250.0, "height": 500.0, "mediaType": "SOLID", "effects": [] }, { "compositions": [ { "id": 26013, "internalId": "26013", "name": "price comp", "layerName": "price comp", "path": "Single Product Promo->price comp", "inCompositionProps": { "startTime": -0.72, "inPoint": 0.0, "outPoint": 5.32, "guideLayer": false, "enabled": true, "adjustmentLayer": false, "shy": false } } ], "type": "TEXT", "internalId": "26028", "name": "EDIT-price", "value": "$199,99", "font": "Poppins-Black", "fontLocation": "C:\\Program Files\\Common Files\\Adobe\\Fonts\\Poppins-Black.ttf", "fontStyle": "Black", "fontFamily": "Poppins", "fontSize": 266.0, "isParagraph": false, "allCaps": false, "leading": 31.9737205505371, "tracking": 0.0 }, { "compositions": [ { "id": 16215, "internalId": "16215", "name": "Single Product Promo", "layerName": null, "path": "Single Product Promo", "inCompositionProps": { "startTime": 0.0, "inPoint": 0.0, "outPoint": 6.04, "guideLayer": false, "enabled": true, "adjustmentLayer": false, "shy": false } } ], "type": "COMPOSITION", "internalId": "25942", "name": "product pic comp", "value": null, "id": 25942, "duration": 6.04, "workAreaStart": 0.0, "width": 500.0, "height": 500.0, "layerName": "product pic comp" }, { "compositions": [ { "id": 25942, "internalId": "25942", "name": "product pic comp", "layerName": "product pic comp", "path": "Single Product Promo->product pic comp", "inCompositionProps": { "startTime": 0.0, "inPoint": 0.0, "outPoint": 6.04, "guideLayer": false, "enabled": true, "adjustmentLayer": false, "shy": false } } ], "type": "MEDIA", "internalId": "25954", "name": "EDIT-example 2 product pic", "value": "(Footage)/Assets/generic%20keyboard.jpg", "width": 500.0, "height": 500.0, "mediaType": "IMAGE", "effects": [ { "type": "EFFECT_CHECKBOX_CONTROL", "name": "STC - Scale to Fit (Maintain Ratio)", "propertyName": "Effects.STC - Scale to Fit (Maintain Ratio).Checkbox", "value": 1, "keyframeValues": [] }, { "type": "EFFECT_CHECKBOX_CONTROL", "name": "STC - Scale to Fill", "propertyName": "Effects.STC - Scale to Fill.Checkbox", "value": 1, "keyframeValues": [] }, { "type": "EFFECT_CHECKBOX_CONTROL", "name": "STC - Restore Original Size", "propertyName": "Effects.STC - Restore Original Size.Checkbox", "value": 0, "keyframeValues": [] }, { "type": "EFFECT_CHECKBOX_CONTROL", "name": "STC - Bypass", "propertyName": "Effects.STC - Bypass.Checkbox", "value": 0, "keyframeValues": [] } ], "preview": { "link": "https://example.com/preview/25954", "expiresAt": "2025-08-12T14:01:59.117749219Z", "internal": false } }, { "compositions": [ { "id": 16215, "internalId": "16215", "name": "Single Product Promo", "layerName": null, "path": "Single Product Promo", "inCompositionProps": { "startTime": 0.0, "inPoint": 0.0, "outPoint": 6.04, "guideLayer": false, "enabled": true, "adjustmentLayer": false, "shy": false } } ], "type": "MEDIA", "internalId": "25910", "name": "bg", "value": "#00274a", "width": 800.0, "height": 800.0, "mediaType": "SOLID", "effects": [] } ]

Check out the Templates API for more information on how to create templates based on the project metadata.

Example code snippets (Node.js)

These snippets illustrate the basic pattern of creating a project and iterating the metadata. In production you would add error checking.

Create a project

import axios from "axios"; import FormData from "form-data"; import fs from "fs"; async function uploadProject(zipPath: string) { const form = new FormData(); form.append("file", fs.createReadStream(zipPath)); form.append("name", "My Project"); form.append("description", "This is my project description"); const response = await axios.post("https://api.plainlyvideos.com/api/v2/projects", form, { headers: { ...form.getHeaders(), }, auth: { username: process.env.PLAINLY_API_KEY, password: "", }, }); return response.data; }

Iterate project metadata

import axios from "axios"; async function getProjectMetadata(projectId: string) { // in case project is not analyzed yet, this will return a HTTP 404 error const response = await axios.get<object[]>(`https://api.plainlyvideos.com/api/v2/projects/${projectId}/meta`, { auth: { username: process.env.PLAINLY_API_KEY, password: "", }, }); return response.data; } async function iterateProjectMetadata(projectId: string) { const processItem = (item: any) => { console.log(`Item: ${item.name} (ID: ${item.internalId}, Type: ${item.type})`); if (item.children) { item.children.forEach(processItem); } if (item.preview) { console.log(` Preview link: ${item.preview.link}`); } }; const metadata = await getProjectMetadata(projectId); metadata.forEach(processItem); }

Additional operations

You can also perform the following operations related to projects using the API:

Best practices

  • Clean up old projects if you do not need to store them indefinitely. The API provides endpoints to delete projects which would reduce the used storage space.
  • Use attributes field to store your own custom metadata about the project.
  • Troubleshooting by checking the error field in the analysis object. It contains detailed information about what went wrong. See troubleshooting guide for more details.

White-label considerations

If you plan to let your own customers upload projects directly you are effectively offering Plainly as a white-labelled service. In this scenario you should build a user interface that validates the uploaded files before sending them to the API. Check file size limits, ensure the package contains the expected directory structure and provide clear feedback if validation fails. Once the project is uploaded store the returned projectId in your database along with any metadata you need to associate it with the user.