How to Generate Canva Designs with a REST API
Create Canva designs with REST APIs using direct design creation or asynchronous Autofill jobs, with OAuth, polling, validation, exports, and troubleshooting.
Canva provides two REST API paths for generating designs:
- Create a new design with
POST /rest/v1/designswhen you need a blank, preset, custom-size, copied, or template-based canvas. - Generate a personalized design with
POST /rest/v1/autofillswhen you already have a brand template or tagged design fields and want to populate them from structured data.
Autofill is asynchronous. Discover the current dataset, submit a job, persist its ID, poll until success or failed, then send the user to the returned Canva design URL or continue with your export workflow. Canva’s API acts on behalf of a Canva user, so your application needs OAuth tokens and the scopes required by each endpoint.
Choose the right Canva API path
| Use case | Endpoint | Request style | Best fit |
|---|---|---|---|
| New blank or preset canvas | POST /rest/v1/designs |
Synchronous creation response | Your application supplies content separately |
| Custom dimensions | POST /rest/v1/designs |
Synchronous creation response | Print, ad, presentation, or other non-preset sizes |
| Personalized template | POST /rest/v1/autofills |
Asynchronous job | Structured data must populate reusable fields |
| Update an existing design | POST /rest/v1/autofills |
Asynchronous job | Refresh fields in an existing design |
Direct design creation places a supplied asset as one flat image. If you need separate editable layers, use a Canva workflow that imports or creates those layers rather than treating a single uploaded image as editable content.
Prerequisites: OAuth, scopes, and plan access
- Create a Canva developer integration and configure its OAuth redirect URI.
- Ask the user to authorize the scopes your workflow needs. Create-design requests require the scopes documented for design creation. Autofill submission requires
design:content:write; reading an Autofill job requiresdesign:meta:read. - Store access and refresh tokens securely. Refresh an expired access token before retrying a request.
- For Autofill, use an account and Canva plan eligible for the feature, such as Canva Pro, Canva Education, Canva for Nonprofits, Canva Teams, or Canva Enterprise, subject to Canva’s current eligibility rules.
Never put a client secret or long-lived token in browser JavaScript. Keep OAuth exchange, token refresh, and API calls that require confidential credentials on your server.
Path A: create a new design
The Create design endpoint accepts a preset design type, custom dimensions, a copied design, and currently preview support for creation from a brand template. A custom design dimension must be between 40 and 8000 pixels, and the total area cannot exceed 25,000,000 square pixels. The endpoint is limited to 20 requests per minute per user.
Minimal cURL request
curl -X POST https://api.canva.com/rest/v1/designs \
-H "Authorization: Bearer $CANVA_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "type_and_asset",
"design_type": {"type": "preset", "name": "doc"},
"title": "My design"
}'
Custom-size design
curl -X POST https://api.canva.com/rest/v1/designs \
-H "Authorization: Bearer $CANVA_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "type_and_asset",
"design_type": {
"type": "custom",
"width": 1200,
"height": 630
},
"title": "Campaign social card"
}'
Validate dimensions before sending the request. For example, 8000 × 4000 is valid by dimension but exceeds the 25,000,000-pixel area limit, so your application should reject it locally.
Path B: generate a design with Autofill
Autofill uses an existing brand template or design containing autofillable fields. Supported values include text, image or video media, charts, and sheets. The field schema belongs to the current dataset, so query it immediately before creating a job.
Step 1: discover the dataset
Use the dataset endpoint for the brand template or design you intend to fill, such as GET /brand-templates/{TEMPLATE-ID}/dataset or the corresponding design dataset endpoint. Save the returned field names and types. Do not hard-code a field forever: Canva warns that fields can be renamed or removed.
curl https://api.canva.com/rest/v1/brand-templates/TEMPLATE-ID/dataset \
-H "Authorization: Bearer $CANVA_ACCESS_TOKEN"
Step 2: submit an Autofill job
Set type to the operation you need: create_from_brand_template, create_from_design, or update_design. The exact data keys must match the dataset response.
curl -X POST https://api.canva.com/rest/v1/autofills \
-H "Authorization: Bearer $CANVA_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "create_from_brand_template",
"brand_template_id": "TEMPLATE-ID",
"data": {
"headline": {"type": "text", "text": "Spring launch"},
"hero_image": {"type": "image", "asset_id": "ASSET-ID"}
}
}'
Persist the returned job ID before doing anything else. A worker or request handler can then resume polling after a restart.
Step 3: poll for completion
curl https://api.canva.com/rest/v1/autofills/AUTOFILL-JOB-ID \
-H "Authorization: Bearer $CANVA_ACCESS_TOKEN"
Stop polling when the status is success or failed. On success, use the returned Canva design URL and thumbnail. On failure, record the error details and show an actionable message to the user.
Python: submit and poll
import os
import time
import requests
BASE = "https://api.canva.com/rest/v1"
TOKEN = os.environ["CANVA_ACCESS_TOKEN"]
HEADERS = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
payload = {
"type": "create_from_brand_template",
"brand_template_id": "TEMPLATE-ID",
"data": {
"headline": {"type": "text", "text": "Spring launch"},
"hero_image": {"type": "image", "asset_id": "ASSET-ID"},
},
}
job = requests.post(f"{BASE}/autofills", headers=HEADERS, json=payload, timeout=30)
job.raise_for_status()
job_id = job.json()["job_id"]
for attempt in range(10):
status_response = requests.get(
f"{BASE}/autofills/{job_id}", headers=HEADERS, timeout=30
)
status_response.raise_for_status()
result = status_response.json()
status = result.get("status")
if status == "success":
print(result["design_url"])
break
if status == "failed":
raise RuntimeError(result.get("error", "Autofill failed"))
time.sleep(min(2 ** attempt, 30))
else:
raise TimeoutError("Autofill did not finish within the polling window")
Node.js: submit and poll
const token = process.env.CANVA_ACCESS_TOKEN;
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
};
const create = await fetch("https://api.canva.com/rest/v1/autofills", {
method: "POST",
headers,
body: JSON.stringify({
type: "create_from_brand_template",
brand_template_id: "TEMPLATE-ID",
data: {
headline: { type: "text", text: "Spring launch" },
hero_image: { type: "image", asset_id: "ASSET-ID" }
}
})
});
if (!create.ok) throw new Error(`Submit failed: ${create.status}`);
const { job_id } = await create.json();
for (let attempt = 0; attempt < 10; attempt++) {
const response = await fetch(
`https://api.canva.com/rest/v1/autofills/${job_id}`,
{ headers }
);
if (!response.ok) throw new Error(`Poll failed: ${response.status}`);
const result = await response.json();
if (result.status === "success") {
console.log(result.design_url);
break;
}
if (result.status === "failed") throw new Error(result.error || "Autofill failed");
await new Promise(resolve => setTimeout(resolve, Math.min(2 ** attempt * 1000, 30000)));
}
OAuth request design
Use the authorization-code flow for server-side applications. Generate and validate a state value, use PKCE where supported, verify the callback matches the expected redirect URI, and associate the resulting token with the correct Canva user in your database. Request only the scopes needed for the selected workflow. If a user revokes access, discard the stored token and restart authorization.
Validation and schema drift
- Fetch the dataset immediately before submission.
- Check that every required application field maps to a current Canva field.
- Validate value types, media identifiers, and required dimensions locally.
- Remember that a field name that no longer exists may be silently skipped. Treat skipped fields as a validation problem and alert the user rather than presenting an apparently complete design.
- Version your own mapping between business fields and Canva field names so a template edit can be rolled out safely.
Polling, retries, and rate limits
Design creation is limited to 20 requests per minute per user. Autofill submission is limited to 60 requests per minute per user, and Autofill job retrieval to 120 requests per minute per user. Use a queue for bulk work, bounded exponential backoff for transient failures, and jitter so many workers do not poll simultaneously.
- Persist the job ID and an idempotency key in your database.
- Poll quickly once, then increase the interval, capping it at a reasonable maximum.
- Stop after a deadline and mark the job for later reconciliation.
- Retry only network failures, timeouts, and documented transient server responses. Do not blindly retry invalid scopes, malformed fields, or ineligible plans.
Export and downstream processing
A successful Autofill result includes a Canva design URL and thumbnail. Direct the user to the URL so they can review, adjust, or export the design in Canva. If your product needs an automated file, add the appropriate export workflow after the design is created and confirm the export endpoint, permissions, and format requirements in Canva’s current documentation.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized |
Expired, revoked, or malformed token | Refresh the token or run OAuth again; verify the Bearer header. |
403 Forbidden |
Missing scope or plan does not include Autofill | Request the documented scope and confirm account eligibility. |
| Field has no effect | Field was renamed or removed | Query the dataset again and update your mapping. |
| Autofill remains pending | Normal asynchronous processing or overloaded polling | Use bounded backoff, a deadline, and a reconciliation worker. |
429 Too Many Requests |
Per-user rate limit exceeded | Queue work, slow polling, honor retry guidance, and add jitter. |
| Custom design rejected | Dimension below 40, above 8000, or area above 25,000,000 pixels | Validate width, height, and width × height before submission. |
| Image is not editable in layers | Asset was supplied as one flat image | Use a layer-aware Canva import or build the design from editable elements. |
Performance, reliability, and cost considerations
- Throughput: Separate submission workers from polling workers and enforce per-user rate limits centrally.
- Reliability: Store OAuth tokens encrypted, persist every job ID, and reconcile jobs after worker restarts.
- Latency: Autofill is asynchronous, so design your UI around a pending state rather than holding an HTTP request open.
- Cost: Canva plan eligibility affects whether Autofill is available. Your own costs usually come from API infrastructure, storage, queues, and any export or asset pipeline you add.
- Observability: Log request type, Canva user identifier, template or design ID, job ID, status transitions, retry count, and sanitized error details. Never log access tokens or private design data.
Or skip the browser setup
If your next step is showing the generated Canva design as an image or PDF, ScreenshotNeo can capture the published page with one request. It removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports the result in X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API docs for all options. Example:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://www.canva.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://www.canva.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://www.canva.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
1,000 screenshots per month are free with no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Can I create an editable Canva design from JSON alone?
JSON supplies values to an existing Autofill dataset. It does not automatically define a complete editable layout. Build or tag the template first.
Is Autofill synchronous?
No. Submit the job, save its ID, and poll until it succeeds or fails.
What happens when a field is missing?
Canva may silently skip a submitted field that no longer exists, so compare your payload with a freshly queried dataset and report skipped mappings.
Can I use a custom canvas larger than 8000 pixels?
No. Each dimension must be 40–8000 pixels and the total area must not exceed 25,000,000 pixels squared.
Should polling happen in the user’s web request?
Usually no. A background worker gives you bounded retries, restart recovery, and better control of per-user retrieval limits.


