Renders API
This section covers everything you need to know about generating videos via the Plainly API. A render represents a single output created from a project template. Renders are processed asynchronously, so your application must adapt to that and process the final video when it is ready.
Render lifecycle
Behind the scenes a render moves through several phases. The diagram below illustrates the state transitions, while the typical flow like following:
- After you submit the request a render object is created and it enters the pending state while Plainly validates provided parameters.
- If validation fails due to missing parameters or invalid media assets, the state transitions to invalid. Since the validation is synchronous, you’ll get this state immediately in the create render response.
- If the render exceeds your organization’s concurrent rendering limits, it enters the throttled state until resources become available. This state transition is also synchronous.
- If validation passes and resources are available, the render is sent to the rendering queue and has queued state while it waits for processing.
- Once rendering starts, the state changes to in-progress.
- Finally, the render completes with either a done state if successful or an error state if something went wrong.
Understanding this lifecycle helps you design responsive user interfaces and decide when to fetch the output file. Webhooks fire only after the render reaches the final state, ensuring you do not attempt to download a file that is still being written.
Creating a render
To trigger a render send a POST
request to /api/v2/renders
with the project and template identifiers. The request body also includes a parameters
object containing values for each template layer you wish to customize. Optionally you can supply a webhook
configuration so Plainly can notify you when the render finishes.
curl -X POST \
-H "Content-Type: application/json" \
-u "$PLAINLY_API_KEY:" \
-d '{
"projectId": "media-abstract@v1",
"templateId": "square",
"parameters": {
"image": "https://picsum.photos/1920/1920",
"newsCta": "Book a demo",
"newsHeading": "Plainly Videos",
"newsSubheading": "Create personalized videos on autopilot",
"newsLogo": "https://storage.googleapis.com/plainly-static-data/plainly-logo-black.png"
},
"webhook": {
"url": "https://example.com/render-callback",
"passthrough": "custom123"
}
}' \
https://api.plainlyvideos.com/api/v2/renders
The request above uses an existing design as the project ID. Once you upload your After Effects project to Plainly, it becomes a project that you can reference by its ID.
The returned JSON response will contain the ID of the new render and all the other properties of the Render
object. You can always use this ID to execute a GET
request to /api/v2/renders/{renderId}
to obtain information about the render, including its current state and output URL once available.
Create render example JSON response
{
"id": "00000000-c3fe-46bf-8dc6-bfd29797c206",
"state": "PENDING",
"parameters": {
"colorPrimary": "242423",
"image": "https://picsum.photos/1920/1920",
"newsCta": "Book a demo",
"newsMusic": "",
"newsHeading": "Plainly Videos",
"newsSubheading": "Create personalized videos on autopilot",
"colorSecondary": "F5CB5C",
"newsLogo": "https://storage.googleapis.com/plainly-static-data/plainly-logo-black.png"
},
"attributes": null,
"lastModified": "2025-07-29T13:23:16.611Z",
"options": null,
"projectId": "media-abstract@v1",
"expired": null,
"templateId": "square",
"integrationDelivery": null,
"webhookDelivery": {
"status": "PENDING",
"deliveries": []
},
"expirationDate": null,
"convertedAssets": null,
"output": null,
"projectName": "Media / Abstract",
"templateName": "Square",
"outputWatermark": null,
"projectZipUrl": null,
"thumbnailUris": null,
"submittedDate": null,
"createdBy": null,
"createdDate": "2025-07-29T13:23:16.611Z",
"error": null,
"compositionName": "render_square",
"parametrizationResults": [
{
"parametrization": {
"value": "#colorPrimary",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"scripting": null,
"assets": [
{
"type": "data",
"composition": "control_comp",
"layerName": "colorPrimary",
"property": "Source Text",
"value": "242423"
}
],
"mandatoryNotResolved": false,
"fatalError": false,
"errorMessage": null
},
{
"parametrization": {
"value": "#colorSecondary",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"scripting": null,
"assets": [
{
"type": "data",
"composition": "control_comp",
"layerName": "colorSecondary",
"property": "Source Text",
"value": "F5CB5C"
}
],
"mandatoryNotResolved": false,
"fatalError": false,
"errorMessage": null
},
{
"parametrization": {
"value": "#newsCta",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"scripting": null,
"assets": [
{
"type": "data",
"composition": "ending_square",
"layerName": "newsCta",
"property": "Source Text",
"value": "Book a demo"
}
],
"mandatoryNotResolved": false,
"fatalError": false,
"errorMessage": null
},
{
"parametrization": {
"value": "#newsHeading",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"scripting": null,
"assets": [
{
"type": "data",
"composition": "source_square",
"layerName": "newsHeading",
"property": "Source Text",
"value": "Plainly Videos"
}
],
"mandatoryNotResolved": false,
"fatalError": false,
"errorMessage": null
},
{
"parametrization": {
"value": "#newsSubheading",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"scripting": null,
"assets": [
{
"type": "data",
"composition": "source_square",
"layerName": "newsSubheading",
"property": "Source Text",
"value": "Create personalized videos on autopilot"
}
],
"mandatoryNotResolved": false,
"fatalError": false,
"errorMessage": null
},
{
"parametrization": {
"value": "#image",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"scripting": null,
"assets": [
{
"type": "image",
"composition": "compImage_square",
"layerName": "image",
"src": "https://picsum.photos/1920/1920",
"name": "QCGlMbjHVHZ2oBCOWzHR",
"contentType": "image/jpeg",
"skipAssetCheck": false,
"asSequence": false
}
],
"mandatoryNotResolved": false,
"fatalError": false,
"errorMessage": null
},
{
"parametrization": {
"value": "#newsLogo",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"scripting": null,
"assets": [
{
"type": "image",
"composition": "logoComp",
"layerName": "newsLogo",
"src": "https://storage.googleapis.com/plainly-static-data/plainly-logo-black.png",
"name": "RQsN2Y4Rkd1FmgXFYbvF",
"contentType": "image/png",
"skipAssetCheck": false,
"asSequence": false
}
],
"mandatoryNotResolved": false,
"fatalError": false,
"errorMessage": null
},
{
"parametrization": {
"value": "#newsMusic",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"scripting": null,
"assets": [],
"mandatoryNotResolved": false,
"fatalError": false,
"errorMessage": null
}
],
"outputFormat": {
"afterEffectsVersion": "AE2023_AMD64",
"outputModule": null,
"settingsTemplate": null,
"attachment": false,
"attachmentFileName": null,
"postEncoding": null,
"ext": "mp4",
"encodingNeeded": true,
"contentType": "video/mp4"
},
"webhook": {
"url": "https://example.com/render-callback",
"passthrough": "custom123",
"onFailure": false,
"onInvalid": null
},
"retried": false,
"publicDesign": true
}
Check the Renders API reference for a complete list of parameters and options you can use when creating a render.
Retrieving render parameters
Before triggering a render you often need to know which parameters are available. Use GET /api/v2/projects/{projectId}/templates/{templateId}
to fetch the template details which contains the parameter definitions.
curl -u "$PLAINLY_API_KEY:" \
https://api.plainlyvideos.com/api/v2/projects/$PLAINLY_PROJECT_ID/templates/$PLAINLY_TEMPLATE_ID
The response includes a layers
array that lists all the dynamic elements in the template. Each layer has a parametrization
object that defines how it can be customized during rendering:
In addition to the parametrization
object, each layer defines a human-readable label
which can be used in user interfaces to help users understand what each parameter does.
Get template example JSON response
{
"name": "Single product",
"id": "00000000-8da7-4358-9384-833f2b7d3ab8",
"duration": 6.03333333333333,
"lastModified": "2025-02-28T10:29:17.033Z",
"renderingComposition": "Single Product Promo",
"defaultRenderOptions": {
"options": null,
"outputFormat": {
"settingsTemplate": null,
"attachmentFileName": null,
"attachment": false,
"outputModule": null,
"postEncoding": null
},
"webhook": null
},
"layers": [
{
"internalId": "26028",
"layerName": "EDIT-price",
"scripting": null,
"compositions": [
{
"name": "Single Product Promo->price comp",
"id": 26013
}
],
"label": "Price",
"parametrization": {
"value": "#price",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"propertyName": "Source Text",
"layerType": "DATA"
},
{
"internalId": "25954",
"layerName": "EDIT-example 2 product pic",
"scripting": null,
"compositions": [
{
"name": "Single Product Promo->product pic comp",
"id": 25942
}
],
"label": "Image",
"parametrization": {
"value": "#image",
"defaultValue": null,
"expression": true,
"mandatory": false
},
"mediaType": "image",
"layerType": "MEDIA"
},
{
"internalId": "25984",
"layerName": "discount comp",
"scripting": {
"scripts": [
{
"scriptType": "LAYER_MANAGEMENT",
"parameterName": "test"
}
]
},
"compositions": [
{
"name": "Single Product Promo",
"id": 16215
}
],
"layerType": "COMPOSITION",
"parametrization": null
}
],
"renderingCompositionId": 16215,
"resolution": {
"width": 720.0,
"height": 720.0
},
"createdBy": "00000000-4efe-4e4b-b809-a097882e4d3d",
"createdDate": "2025-02-25T14:09:34.559Z"
}
Check the Templates API reference for a complete operation reference.
Checking render status
While you can poll /api/v2/renders/{renderId}
periodically to check progress, we strongly recommend configuring webhooks for a more efficient workflow. The polling approach is useful for simple scripts or during development, but it can generate unnecessary traffic at scale. When using webhooks you only need to process the final output once Plainly notifies you that rendering is complete.
When specifying the webhook, you can control if webhook should be called also when render is invalid or fails. This allows you to handle render failures gracefully in your application.
The passthrough
field simply echoes back whatever value you supplied in the render request. Use it to correlate webhook notifications with data in your own system, such as a user ID or an invoice reference.
Read the webhook notifications guide for detailed information.
Advanced webhook security
If you need extra security for your webhook notifications, there are few options to consider.
If you are an enterprise user, you can request a static IP that will be used to execute requests against your HTTP endpoint
Or alternatively, you can can deliver additional context by including query parameters in the callback URL. Simply compute the HMAC signature of the passthrough using a secret and append it to the webhook URL as query param. Then, in your webhook handler, validate that the signature matches the expected value.
Storing and serving results
Once the render is complete, Plainly provides a publicly-reachable URL in the output
field of the Render
object and webhook notification payload. This URL allows you to download the final video file. In addition, the expirationDate
field indicates when will the URL expire.
It is advise to download the video file as part of your integration and move it to long-term storage or a delivery network. This way you are ensuring that the file is available for your users even after the URL expires. Plainly does not store the rendered video files indefinitely, so it is important to handle this step in your application.
Expiration date is reflected to all downloadable files, including thumbnails, video with watermark, etc.
Example code snippets (Node.js)
These snippets illustrate the basic pattern of sending JSON and handling the result. In production you would add error checking, retries and perhaps an additional layer of security as described earlier.
Triggering a render
import axios from "axios";
async function triggerRender(
projectId: string,
templateId: string,
parameters: Record<string, unknown>,
additionalConfig?: object,
) {
const response = await axios.post(
"https://api.plainlyvideos.com/api/v2/renders",
{
projectId,
templateId,
parameters,
...(additionalConfig || {}),
},
{
auth: {
username: process.env.PLAINLY_API_KEY!,
password: "",
},
headers: {
"Content-Type": "application/json",
},
},
);
return response.data;
}
Webhook handler
import express from "express";
const app = express();
app.use(express.json());
app.post("/render-callback", (req, res) => {
const { renderId, success, passthrough, output, error } = req.body;
if (success) {
console.log(`Render ${renderId} finished with video file downloadable at ${output}. Context: ${passthrough}`);
} else {
console.error(`Render ${renderId} failed`, error);
}
res.status(200).end();
});
app.listen(8080);
Additional operations
You can also perform the following operations related to renders using the API:
- List all renders - Get a paginated list of all renders in your organization.
- Search renders - Full-text search for renders based on various criteria.
- Cancel render - Stop a render that is not yet in-progress.
- Cancel all - Cancel all renders for a specific project and template.
- Promote render - Promote a render to a publicly shared link, if enabled by the project.
- Resubmit render - Retry a render that has failed, by creating a new copy of the render with the same parameters.
- Delete render - Remove a render from the system, optionally deleting its output files as well.
- Re-trigger webhook - Re-sends webhook notification for an existing and finished render.
Best practices
- Validate parameters before sending requests to reduce the chance of errors. Check that all required layers are supplied.
- Use passthrough to store context so your webhook handler can link the render back to a user action or database record.
- Use
attributes
field to store your own custom metadata about the render. - Clean up old renders if you do not need to store them indefinitely. The API provides endpoints to delete renders.
- Secure webhook endpoints by using HTTPS and verifying payloads. Consider using HMAC signatures.
- Troubleshooting by checking the
error
field in the render object. It contains detailed information about what went wrong. See troubleshooting guide for more details.