Full developer documentation for Edit Square Docs. Individual pages are also available at .md for targeted access.
---
# Introduction
> Render motion graphics programmatically - send data to your template and get a finished video back.
Turn your data into finished video. Design a project once in the editor, expose the parts that change as template fields, then hand it to the API: send the values, get an MP4 back. One request from your app, CRM or workflow tool makes one video - a thousand requests make a thousand, each personalised, localised or generated on the fly from data you already have.
The Edit Square API is organised around [REST](https://en.wikipedia.org/wiki/REST). It has predictable, resource-oriented URLs, accepts [JSON](https://www.json.org/)-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP verbs, response codes and authentication. If you can make an HTTP request, you can render.
Renders are the part you drive. Projects, templates, teams and users are read-only: they are authored in the editor, so the fields you fill are always the ones your designers exposed. Ready to get started? [Create an API key](/api/authentication/).
## Base URL
The base URL for all requests to the Edit Square API is:
```http
https://api.editsquare.com
```
Every path is relative to it (e.g. `POST /v1/renders`) and requests must be authenticated - see the [Authentication](/api/authentication/) section for more details.
The full API surface is described by an [OpenAPI 3.1 document](https://api.editsquare.com/openapi.json) - import it into Postman, Bruno or Insomnia, or feed it to a client generator.
## Endpoints
- [Renders](/api/reference/operations/tags/renders/) - Create a render, track it, and collect the finished video.
- [Projects](/api/reference/operations/tags/projects/) - Browse the projects you can render from.
- [Templates](/api/reference/operations/tags/templates/) - Check which fields a project exposes for a render to set.
- [Teams](/api/reference/operations/tags/teams/) - See which teams your key can reach and bill renders to.
- [Users](/api/reference/operations/tags/users/) - Look up the people on a team, and the user your key acts as.
- [OpenAPI spec](https://api.editsquare.com/openapi.json) - The full API as an OpenAPI 3.1 document, for clients and codegen.
---
# Authentication
> How Edit Square API keys work, where to get one, and what they can reach.
All Edit Square endpoints are authenticated using API keys as a bearer token:
`GET /v1/renders`
**Shell (cURL)**
```sh
curl --request GET \
--url 'https://api.editsquare.com/v1/renders?project_id=proj_j123456789' \
--header 'Authorization: Bearer sk_your_key_here'
```
**JavaScript (Fetch)**
```js
const url = 'https://api.editsquare.com/v1/renders?project_id=proj_j123456789';
const options = {
method: 'GET',
headers: { Authorization: 'Bearer sk_your_key_here' },
};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
```
Full request and response documentation can be found in the [renders api reference](/api/reference/operations/tags/renders/) section.
Any request with no `Authorization` header will be rejected with `401`.
## Getting a key
Keys are created in the dashboard:
1. Open the dashboard and click your avatar in the bottom-left.
2. Choose **API Keys**.
3. **Create** a key and give it a name you will recognise later.
:::tip{icon="information"}
The key is only ever shown **once** at creation, make sure you take a copy of it. If you have lost you key it must be replaced and the previous key revoked.
:::
Keys look like `sk_` followed by 32 characters. The dashboard shows a
masked form (`sk_1a2b3…cdef`), which is enough to tell two keys
apart and not enough to use one.
## What a key can reach
A key belongs to the person who created it and carries that person's access: it
reaches the projects their teams can reach, and nothing else. It is not scoped
to a project, and it is not shared across a team - two people on one team hold
two different keys, and revoking one leaves the other working.
[`GET /v1/me`](/api/reference/operations/getme/) answers which user a key acts
as, which is the quickest way to tell two keys apart when a request is not
returning what you expected.
That makes revocation the tool for everything: someone leaves, a key leaks, a
script is retired - revoke that key and create a new one.
## Keeping keys safe
- Treat a key like a password: server-side only, out of git, out of the
browser. Anyone holding it can render against your team's projects and spend
the team's credits.
- Give each integration its own named key, so one can be revoked without
stopping the others.
- Revoking is immediate and permanent - the next request with that key fails.
---
# Errors
> The status codes the API returns, the shape of an error body, and what to do with each.
Edit Square uses conventional HTTP response codes to indicate whether an API
request succeeded or failed. In general:
- Codes in the `2xx` range indicate success.
- Codes in the `4xx` range indicate a problem with the request itself - a
missing parameter, an invalid API key, an ID that doesn't exist.
- Codes in the `5xx` range indicate an error on our side. These are rare, and
they are the only ones worth retrying unchanged.
## HTTP status codes
| Code | | Meaning |
| --- | --- | --- |
| `200` | OK | Everything worked as expected. |
| `201` | Created | The entity was created. |
| `400` | Bad Request | The request was understood but could not be carried out. |
| `401` | Unauthorized | No API key was provided, or the key is not valid. |
| `402` | Payment Required | The team has run out of credits. |
| `403` | Forbidden | The key is valid but does not have access to this resource. |
| `404` | Not Found | The resource doesn't exist, or your key can't access it. The API deliberately doesn't distinguish between the two. |
| `422` | Unprocessable Content | The request failed validation. The response lists the fields that failed. |
| `500` | Server Error | Something went wrong on our end. |
## The error object
Every error response is a JSON object containing an `error` field, whose
value is a human-readable sentence describing what went wrong. It is written
for a developer reading a log, not for display to an end user. For everything
except validation failures, `error` is the only field in the object:
```json
{ "error": "Render not found" }
```
### Validation errors
A `422` extends the object with two extra fields: a `message` string that
summarises the failure on a single line (the problem itself when there is only
one, a count when there are several), and a `details` array with one entry for
each field that failed:
```json
{
"error": "Validation failed",
"details": [
{
"field": "project_id",
"message": "project_id: Project ID is required",
"code": "too_small"
}
],
"message": "project_id: Project ID is required"
}
```
Each entry in `details` has:
| Attribute | | Description |
| --- | --- | --- |
| `field` | string | The name of the parameter that failed, using dot notation for nested parameters. Set to `unknown` when the failure can't be attributed to a single field. |
| `message` | string | A description of what is wrong with the value, prefixed with the field name. |
| `code` | string | The name of the validation rule that failed - `too_small`, `invalid_type`, and so on. |
`field` is what makes a `422` worth surfacing to users: it identifies the
parameter that needs fixing, so a form can highlight the offending input
instead of showing the raw response.
## Retrying
`500` responses and network failures are worth retrying with backoff. `401`,
`403`, `404` and `422` are not - they describe a problem with the request, and
the same request will fail the same way. A `402` means the team needs more
credits before anything else will work.
---
# Pagination
> How list endpoints page, what the envelope carries, and how to walk every page.
List endpoints that can grow without bound, such as
[projects](/api/reference/operations/listprojects/) and
[renders](/api/reference/operations/listrenders/), use cursor-based pagination through the `cursor`
parameter. Each page of results includes a `next_cursor` value. Pass it as
`cursor` on your next request to fetch the following page.
Endpoints whose results stay small (a project's templates, a team's members,
the teams a key can access) return everything in a single response and carry
only `data`.
## The list response
Every list endpoint returns an object, never a bare array:
```json
{
"data": [ /* … */ ],
"has_more": true,
"next_cursor": "kBIAAAAAeyJ2IjoxLCJjIjoxNzQ4ODc1OTg2fQ"
}
```
| Field | Description |
| --- | --- |
| `data` | An array containing the elements of the current page. |
| `has_more` | Whether more elements are available after this set. If `false`, this set comprises the end of the list. |
| `next_cursor` | A cursor for fetching the next page. `null` on the last page. |
## Parameters
| Parameter | Description |
| --- | --- |
| `limit` | Optional, default is 20. The number of objects to return, ranging between 1 and 60. Values above 60 are clamped to 60, not rejected. |
| `cursor` | Optional. A cursor for use in pagination. Pass the `next_cursor` value from the previous response to fetch the next page. Omit it to fetch the first page. |
Cursors are opaque: they encode a position in the result set, not an object
ID. Don't construct or parse them, and don't store them for long.
A cursor is only valid for the endpoint and filters that produced it. If you
change `search`, `status` or `order`, start again from the first page.
## Fetching every page
`GET /v1/renders`
**Shell (cURL)**
```sh
url='https://api.editsquare.com/v1/renders?project_id=proj_j123456789&limit=60'
while :; do
page=$(curl --silent --request GET \
--url "$url" \
--header 'Authorization: Bearer sk_your_key_here')
echo "$page" | jq '.data[]'
cursor=$(echo "$page" | jq --raw-output '.next_cursor // empty')
[ -z "$cursor" ] && break
url='https://api.editsquare.com/v1/renders?project_id=proj_j123456789&limit=60&cursor='$cursor
done
```
**JavaScript (Fetch)**
```js
const renders = [];
let cursor;
do {
const url = new URL('https://api.editsquare.com/v1/renders');
url.searchParams.set('project_id', 'proj_j123456789');
url.searchParams.set('limit', '60');
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, {
headers: { Authorization: 'Bearer sk_your_key_here' },
});
if (!response.ok) throw new Error(response.status + ' ' + (await response.text()));
const page = await response.json();
renders.push(...page.data);
cursor = page.next_cursor;
} while (cursor);
console.log(renders);
```
Stop when `next_cursor` is `null`, not when a page comes back short. A page
can contain fewer than `limit` results and still be followed by more.
## Ordering
Objects are returned in reverse chronological order by creation time, newest
first. Pass `order=asc` to return oldest first.
`search` orders results by relevance and ignores `order`.
---
# Projects API reference
> A project is what someone builds in the editor and what renders are made from. Projects are read-only through the API.
A project is what someone builds in the editor and what renders are made from. Projects are read-only through the API.
Base URL: `https://api.editsquare.com`. Every endpoint requires an `Authorization: Bearer ` header.
## List projects
`GET /v1/projects`
Lists a team's projects, newest first.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | How many to return. 1–60, 20 by default. Example: `20` |
| `cursor` | query | string | no | The `next_cursor` of the previous page. Omit for the first page. |
| `team_id` | query | string | yes | The team whose projects to list. Example: `team_jn7fw41m8n5jtht7hxsy69tzg97gvef8` |
| `search` | query | string | no | Filter by name. Matches are returned by relevance rather than by date. Example: `campaign` |
| `order` | query | `asc` \| `desc` | no | Oldest or newest first. Newest by default, and ignored when `search` is set. Example: `desc` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | A page of projects | ProjectList |
| `404` | Team not found | ErrorResponse |
## Get a project
`GET /v1/projects/{id}`
Retrieves a single project.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Project ID Example: `proj_jd71dcg41pjy7rax1v2h4pvcmd7gtar9` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | The project | Project |
| `404` | Project not found | ErrorResponse |
## Schemas
### ProjectList
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of Project | yes | |
| `has_more` | boolean | yes | Whether another page follows. Example: `false` |
| `next_cursor` | string \| null | yes | Pass as `cursor` to read the next page. Null on the last page. |
### Project
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Example: `"proj_jd71dcg41pjy7rax1v2h4pvcmd7gtar9"` |
| `name` | string | yes | Example: `"Summer campaign"` |
| `team` | object | yes | |
| `created_by` | string \| null | yes | Example: `"user_k970b359khfwwny0q8fjah1hc97gtcwm"` |
| `thumbnail_url` | string \| null | yes | A signed, time-limited still of the project. Null until one has been rendered. |
| `created_at` | number | yes | Example: `1748875980` |
| `updated_at` | number \| null | yes | Example: `1748875986` |
### ErrorResponse
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | yes | Example: `"Render not found"` |
---
# Renders API reference
> A render turns a project into a video file. Create one, then poll it or wait for the webhook.
A render turns a project into a video file. Create one, then poll it or wait for the webhook.
Base URL: `https://api.editsquare.com`. Every endpoint requires an `Authorization: Bearer ` header.
## List renders
`GET /v1/renders`
Lists the renders of one project, newest first.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | How many to return. 1–60, 20 by default. Example: `20` |
| `cursor` | query | string | no | The `next_cursor` of the previous page. Omit for the first page. |
| `project_id` | query | string | yes | Project ID Example: `proj_j123456789` |
| `search` | query | string | no | Filter by name. Matches are returned by relevance rather than by date. Example: `welcome` |
| `status` | query | `initializing` \| `ready` \| `queued` \| `processing` \| `finalizing` \| `complete` \| `failed` | no | Return only renders with this status. Example: `complete` |
| `order` | query | `asc` \| `desc` | no | Oldest or newest first. Newest by default, and ignored when `search` is set. Example: `desc` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | A page of renders | RenderList |
## Create a new render
`POST /v1/renders`
Queues a render of a project. Pass `template_id` and `values` to render the project with a form submission applied; omit both to render it as it stands.
**Request body** (`application/json`)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | |
| `project_id` | string | yes | |
| `template_id` | string | no | |
| `values` | object | no | |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `201` | Render created successfully | Render |
| `400` | Invalid request data | ErrorResponse |
## Get a render
`GET /v1/renders/{id}`
Retrieves a single render, including its status and - once complete - the signed URLs of its output.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Render ID Example: `rend_j123456789` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | Retrieve the render | Render |
| `404` | Render not found | ErrorResponse |
## Schemas
### RenderList
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of Render | yes | |
| `has_more` | boolean | yes | Whether another page follows. Example: `false` |
| `next_cursor` | string \| null | yes | Pass as `cursor` to read the next page. Null on the last page. |
### Render
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Example: `"rend_jh716ctpaqbye9aw64tgkqwtks7gvx7g"` |
| `name` | string | yes | Example: `"My New Shiny Render"` |
| `status` | `initializing` \| `ready` \| `queued` \| `processing` \| `finalizing` \| `complete` \| `failed` | yes | |
| `type` | `local` \| `cloud` | yes | |
| `project` | object | yes | |
| `output` | object of string | no | Signed URLs for the finished video, keyed by variant (the master plus any smaller encodes). Present once the render is complete. The URLs are time-limited. Example: `{"master":"https://files.editsquare.com/renders/rend_jh716ctpaqbye9aw64tgkqwtks7gvx7g/master.mp4?signature=..."}` |
| `created_by` | string \| null | yes | Example: `"user_k970b359khfwwny0q8fjah1hc97gtcwm"` |
| `created_at` | number | yes | Example: `1748875980` |
| `updated_at` | number \| null | yes | Example: `1748875986` |
### ErrorResponse
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | yes | Example: `"Render not found"` |
---
# Teams API reference
> A team owns projects and is what renders are billed to. An API key reaches the teams its owner belongs to, plus every team under an account they administer.
A team owns projects and is what renders are billed to. An API key reaches the teams its owner belongs to, plus every team under an account they administer.
Base URL: `https://api.editsquare.com`. Every endpoint requires an `Authorization: Bearer ` header.
## List teams
`GET /v1/teams`
Lists every team this API key can reach.
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | The teams the key can reach | TeamList |
## Get a team
`GET /v1/teams/{id}`
Retrieves a single team.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Team ID Example: `team_jn7fw41m8n5jtht7hxsy69tzg97gvef8` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | The team | Team |
| `404` | Team not found | ErrorResponse |
## Schemas
### TeamList
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of Team | yes | |
### Team
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Example: `"team_jn7fw41m8n5jtht7hxsy69tzg97gvef8"` |
| `name` | string | yes | Example: `"Brand team"` |
| `account_id` | string | yes | Example: `"acc_jh716ctpaqbye9aw64tgkqwtks7gvx7g"` |
| `role` | `admin` \| `member` \| `null` | yes | The caller's role on this team. Example: `"admin"` |
| `created_at` | number | yes | Example: `1748875980` |
### ErrorResponse
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | yes | Example: `"Render not found"` |
---
# Templates API reference
> A template is the set of fields a project exposes for filling in when creating a render.
A template is the set of fields a project exposes for filling in when creating a render.
Base URL: `https://api.editsquare.com`. Every endpoint requires an `Authorization: Bearer ` header.
## List templates
`GET /v1/templates`
Lists the templates a project exposes, in the order the project defines them.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `project_id` | query | string | yes | Project ID Example: `proj_jd71dcg41pjy7rax1v2h4pvcmd7gtar9` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | The project's templates | TemplateList |
| `404` | Project not found | ErrorResponse |
## Get a template
`GET /v1/templates/{template_id}`
Retrieves one template, including every field it exposes - the keys a render's `values` object can set.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `template_id` | path | string | yes | Template ID Example: `tpl_8f2c1d4e6a7b9c0d1e2f3a4b5c6d7e8f` |
| `project_id` | query | string | yes | Project ID Example: `proj_jd71dcg41pjy7rax1v2h4pvcmd7gtar9` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | The template | Template |
| `404` | Template not found | ErrorResponse |
## Schemas
### TemplateList
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of TemplateSummary | yes | |
### TemplateSummary
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Example: `"tpl_8f2c1d4e6a7b9c0d1e2f3a4b5c6d7e8f"` |
| `name` | string | yes | Example: `"Welcome video"` |
| `description` | string \| null | yes | Example: `"One clip per new customer."` |
| `project_id` | string | yes | Example: `"proj_jd71dcg41pjy7rax1v2h4pvcmd7gtar9"` |
| `target_composition_id` | string | yes | The composition this template renders. Example: `"comp_4f1f0f7c"` |
| `field_count` | number | yes | Example: `3` |
### ErrorResponse
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | yes | Example: `"Render not found"` |
### TemplateField
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes | The key to use in a render's `values` object. Example: `"customer_name"` |
| `label` | string \| null | yes | Example: `"Customer name"` |
| `description` | string \| null | yes | Example: `"Shown on the opening card."` |
| `kind` | string | yes | What the field drives. Example: `"control"` |
| `widget` | string \| null | yes | How the editor renders the input. Example: `"text"` |
| `default_value` | string \| number \| boolean \| null | yes | Used when a render omits this key. Example: `"Acme"` |
---
# Users API reference
> The people on a team. `GET /v1/me` resolves the user an API key acts as - the quickest way to see what a key can reach.
The people on a team. `GET /v1/me` resolves the user an API key acts as - the quickest way to see what a key can reach.
Base URL: `https://api.editsquare.com`. Every endpoint requires an `Authorization: Bearer ` header.
## Get the current user
`GET /v1/me`
Resolves the user this API key belongs to.
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | The user the key acts as | User |
## List team members
`GET /v1/users`
Lists the members of a team, with each member's role.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `team_id` | query | string | yes | The team whose members to list. Example: `team_jn7fw41m8n5jtht7hxsy69tzg97gvef8` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | The team's members | TeamMemberList |
| `404` | Team not found | ErrorResponse |
## Get a user
`GET /v1/users/{id}`
Retrieves a user the caller shares a team with.
**Parameters**
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | User ID Example: `user_k970b359khfwwny0q8fjah1hc97gtcwm` |
**Responses**
| Status | Description | Schema |
| --- | --- | --- |
| `200` | The user | User |
| `404` | User not found | ErrorResponse |
## Schemas
### User
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Example: `"user_k970b359khfwwny0q8fjah1hc97gtcwm"` |
| `email` | string \| null | yes | Example: `"ada@example.com"` |
| `created_at` | number | yes | Example: `1748875980` |
### TeamMemberList
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of TeamMember | yes | |
### ErrorResponse
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | yes | Example: `"Render not found"` |
---
# Renders
> The render lifecycle - statuses, values, outputs, and how to track one.
A **render** is a video file produced from a project. When you create a
render, Edit Square takes the project as it exists at that moment, applies the
values you supply, and renders the result to a video file.
## Lifecycle
A render moves through the following statuses:
| Status | Meaning |
| --- | --- |
| `initializing` | The render was accepted and its configuration is being prepared. |
| `ready` | The configuration is prepared and the render is waiting for a worker. |
| `queued` | The render has been handed to a worker. |
| `processing` | The render is in progress. The first progress report moves it here. |
| `finalizing` | Rendering finished. The file is being stored and the credits charged. |
| `complete` | The render is finished. `output` carries the file URLs. |
| `failed` | The render stopped with an error. There is no output to collect. |
`complete` and `failed` are final; the other five are steps along the way.
## Applying values
`template_id` names a template defined in the project, and `values` is an
object keyed by that template's field keys.
For example, this template from
[Get a template](/api/reference/operations/gettemplate/) exposes a single
field, `customer_name`:
```json
{
"id": "tpl_8f2c1d4e6a7b9c0d1e2f3a4b5c6d7e8f",
"name": "Welcome video",
"fields": [
{
"key": "customer_name",
"label": "Customer name",
"default_value": "Acme"
}
]
}
```
Each entry in `values` sets the field with the matching `key`. To render this
template for a customer named John Smith, the following would be used:
```json
{
"name": "Welcome - John Smith",
"project_id": "proj_jd71dcg41pjy7rax1v2h4pvcmd7gtar9",
"template_id": "tpl_8f2c1d4e6a7b9c0d1e2f3a4b5c6d7e8f",
"values": { "customer_name": "John Smith" }
}
```
A field you leave out falls back to its `default_value` (`"Acme"` above), and
keys the template does not define are ignored. A `template_id` the project
does not define is an error, not a silent fall-through to the plain project.
The values are applied to a copy of the project configuration for this render
only. The project itself is untouched, so two renders from the same project
with different values never affect each other.
The full request body is documented in
[Create a render](/api/reference/operations/createrender/).
## Outputs
A `complete` render carries an `output` object of signed URLs, keyed by
variant: the master file, plus any smaller encodes the project produces.
The URLs are signed and time-limited. Download the file, or re-request the
render to get fresh URLs; they are not stable links to hand to a browser
weeks later.
The full response is documented in
[Get a render](/api/reference/operations/getrender/).
## Tracking a render
There are two ways to follow a render to completion:
- **Polling**: Request [`GET /v1/renders/{id}`](/api/reference/operations/getrender/) until the status is either
`complete` or `failed`. This is simple and works well at small volume. A typical render takes a
couple of minutes, so poll on that scale.
- **Webhooks**: Edit Square calls your endpoint when the render changes state.
We recommend webhooks for anything unattended or at volume. See
[Webhooks](/api/webhooks/).
## Listing renders
[`GET /v1/renders`](/api/reference/operations/listrenders/) lists a project's
renders, newest first. `project_id` is required: renders are listed per
project, not per team.
---
# Webhooks
> Receive render events instead of polling - payloads, signature verification and retries.
Listen for events on your webhook endpoint so your integration can react as
renders progress, without polling.
After you register a webhook endpoint, Edit Square `POST`s a JSON payload to
it when a render changes state. Receiving webhook events is the right way to
respond to asynchronous work such as a render completing or failing,
especially at volume.
## Set up your endpoint
Webhook endpoints are configured per team in the dashboard, under
**Webhooks**:
1. Register your endpoint's publicly accessible HTTPS URL.
2. Select the events you want delivered to it.
3. Copy the signing secret generated for the endpoint. You will use it to
verify deliveries.
## Events
| Event | Sent when |
| --- | --- |
| `render.status_changed` | A render's status changes, including both `complete` and `failed`. |
| `render.complete` | A render finished successfully. |
| `render.failed` | A render stopped with an error. |
Only subscribe to the events your integration requires. An endpoint
subscribed to both `render.status_changed` and `render.complete` receives
**two** deliveries when a render completes, one for each event.
## The event payload
```json
{
"id": "hook_jn8x…",
"event": "render.complete",
"data": {
"id": "rend_jh716ctpaqbye9aw64tgkqwtks7gvx7g",
"name": "Example render name",
"status": "complete",
"project": { "id": "proj_jd71dcg…", "team": "team_jn7fw41…" },
"output": { "master": "https://…" }
},
"timestamp": 1748875986000
}
```
`data` is the render, in the same shape
[`GET /v1/renders/{id}`](/api/reference/operations/getrender/) returns.
Every delivery also carries these headers:
| Header | Value |
| --- | --- |
| `X-Webhook-Signature` | `sha256=`, an HMAC-SHA256 of the raw body, keyed with the endpoint's secret. |
| `X-Webhook-ID` | The delivery ID. The same across retries of one delivery. |
| `X-Webhook-Timestamp` | Milliseconds since the epoch, matching `timestamp` in the body. |
| `User-Agent` | `EditSquare-Webhooks/1.0` |
## Create a handler
Set up an HTTPS endpoint function that:
- Handles `POST` requests with a JSON payload.
- Verifies the request was sent by Edit Square, using the
`X-Webhook-Signature` header and your endpoint's signing secret.
- Quickly returns a `2xx` status code before any long-running logic.
For example, respond first and then download the render's output, rather
than holding the connection open while you download it.
## Verify deliveries
Without verification, anyone who discovers your URL can `POST` fake events to
it. Always verify that a delivery came from Edit Square before acting on it.
Compute an HMAC-SHA256 over the **raw request body**, the exact bytes before
any JSON parsing, keyed with your endpoint's signing secret, and compare it to
the `X-Webhook-Signature` header in constant time:
```js
import { createHmac, timingSafeEqual } from 'node:crypto';
function isFromEditSquare(rawBody, header, secret) {
const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
const a = Buffer.from(header ?? '');
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
```
Reject any request that does not match.
If you are using a web framework, make sure it does not parse or reformat the
request body before you read it. The signature is computed over the raw bytes,
and any change to them causes verification to fail.
## Delivery behaviour
### Retries
A delivery is attempted up to **five times**: the first attempt, then four
retries with exponential backoff starting at one second. Each attempt times
out after 15 seconds, which is why your handler must respond quickly.
### Duplicate deliveries
Because retries exist, your endpoint can receive the same event more than
once. `X-Webhook-ID` is the same across retries of one delivery, so log the
IDs you have processed and skip any you have already seen.