Text Layer Operations Migration
This guide helps you migrate text layer operations from V1's /documentCreate and /documentOperations endpoints to V2's unified /create-composite endpoint.
Overview
V1 Endpoints:
/pie/psdService/documentCreate- Create documents with text layers/pie/psdService/documentOperations- Add/edit text layers
V2 Endpoint: /v2/create-composite with edits.layers containing text layer operations
Creating text layers
V1 approach: documentCreate
curl -X POST \
https://image.adobe.io/pie/psdService/documentCreate \
-H "Authorization: Bearer $token" \
-H "x-api-key: $apiKey" \
-H "Content-Type: application/json" \
-d '{
"options": {
"document": {
"width": 1000,
"height": 1000,
"resolution": 72,
"fill": "white",
"mode": "rgb"
},
"layers": [
{
"type": "textLayer",
"name": "My Title",
"text": {
"content": "Hello World",
"characterStyles": [
{
"fontSize": 48,
"fontName": "Arial-BoldMT",
"fontColor": {
"rgb": {
"red": 255,
"green": 0,
"blue": 0
}
}
}
],
"paragraphStyles": [
{
"alignment": "center"
}
]
}
}
]
},
"outputs": [{
"href": "<SIGNED_POST_URL>",
"storage": "external",
"type": "image/vnd.adobe.photoshop"
}]
}'
V2 approach
curl -X POST \
https://photoshop-api.adobe.io/v2/create-composite \
-H "Authorization: Bearer $token" \
-H "x-api-key: $apiKey" \
-H "Content-Type: application/json" \
-d '{
"document": {
"width": 1000,
"height": 1000,
"resolution": {"unit": "density_unit", "value": 72},
"fill": {
"solidColor": {
"red": 255,
"green": 255,
"blue": 255
}
},
"mode": "rgb"
},
"edits": {
"layers": [
{
"type": "text_layer",
"name": "My Title",
"text": {
"content": "Hello World",
"antiAliasing": "smooth",
"textOrientation": "horizontal",
"characterStyles": [
{
"characterStyle": {
"fontSize": 48,
"font": {
"postScriptName": "ArialMT"
},
"fontColor": {
"rgb": {
"red": 255,
"green": 0,
"blue": 0
}
}
}
}
],
"paragraphStyles": [
{
"paragraphStyle": {
"alignment": "center"
}
}
]
},
"operation": {
"type": "add",
"placement": {
"type": "top"
}
}
}
]
},
"outputs": [
{
"destination": {
"url": "<SIGNED_POST_URL>"
},
"mediaType": "image/vnd.adobe.photoshop"
}
]
}'
Key migration changes
1. Layer type name
V1: "type": "textLayer"
V2: "type": "text_layer"
typeis required on every layer entry inedits.layers[]. V1 allowed omittingtypefor simple visibility or property-only edits on existing layers; V2 always requires it. Error when omitted:Missing required field 'type' for edit operation at path 'edits.layers[N]'.
2. Character styles structure
data-variant=warning
data-slots=text
to field on a character or paragraph style range entry was interpreted as a length — so from: 0, to: 5 meant "5 characters starting at index 0" (indices 0-4). In V2, apply.to is an inclusive end index (0-based) — so apply: {from: 0, to: 4} means "characters at indices 0, 1, 2, 3, 4" (the first 5 characters). When migrating V1 ranges to V2, set apply.to to the last character index you want to style, not a length.Off-by-one example — styling the word "Hello" in "Hello World":
// V1: to=5 means length 5 → characters 0,1,2,3,4
{"from": 0, "to": 5, "fontName": "Arial-BoldMT"}
// V2: to=4 is the inclusive end index → characters 0,1,2,3,4 (same result)
{"apply": {"from": 0, "to": 4}, "characterStyle": {"font": {"postScriptName": "Arial-BoldMT"}}}
characterStyles with no range (implicit full-string in V1): If a V1 characterStyle entry has neither from nor to (applies to the entire content implicitly), V2 requires an explicit apply block. Set apply.from = 0 and apply.to = len(text.content) - 1. Omitting apply entirely causes the style to not apply, resulting in default font rendering and significant pixel differences.
V1: Direct properties in characterStyles array. The range is given with from and to on each item (no apply wrapper):
{
"characterStyles": [
{
"fontName": "Arial-BoldMT",
"fontSize": 60,
"from": 0,
"to": 3,
"fontColor": {
"rgb": {
"red": 255,
"green": 0,
"blue": 0
}
}
}
]
}
V2:
- Put style properties inside a
characterStyleobject. - Use
apply.fromandapply.tofor the character range (0-based, inclusive). - Use
font.postScriptNameinstead offontName.
{
"characterStyles": [
{
"apply": {
"from": 0,
"to": 11
},
"characterStyle": {
"font": {
"postScriptName": "Arial-BoldMT"
},
"fontSize": 48,
"syntheticBold": false,
"syntheticItalic": false,
"underline": false,
"capsOption": "all_caps",
"fontColor": {
"rgb": {
"red": 255,
"green": 0,
"blue": 0
}
}
}
}
]
}
data-variant=info
data-slots=text
apply.from and apply.to are inclusive: the style applies to characters at indices from through to (0-based). For example, "from": 0, "to": 4 applies to the first five characters (indices 0, 1, 2, 3, 4).3. Paragraph styles structure
V1: Range-based: each array item has from, to, and direct properties (e.g. alignment).
{
"paragraphStyles": [
{
"from": 0,
"to": 20,
"alignment": "center"
}
]
}
data-variant=info
data-slots=text
to was incorrectly interpreted as length. In V2, apply.from and apply.to are both inclusive 0-based indices. When migrating, use the last character index for apply.to, not a length.V2: Each item is a paragraph style range with optional apply.from/apply.to (0-based inclusive character indices) and a paragraphStyle object.
{
"paragraphStyles": [
{
"paragraphStyle": {
"alignment": "center"
}
}
]
}
With a range (V2 only): use apply.from and apply.to to apply the paragraph style to a specific character range (same inclusive 0-based indexing as character styles).
{
"paragraphStyles": [
{
"apply": { "from": 0, "to": 20 },
"paragraphStyle": {
"alignment": "left",
"writingDirection": "left_to_right",
"firstLineIndent": 0,
"startIndent": 0,
"endIndent": 0,
"spaceBefore": 0,
"spaceAfter": 6
}
}
]
}
4. Font management and font options
Font management covers how you supply custom fonts (e.g. .ttf, .otf), set a default or fallback font when a referenced font is missing, and choose whether to fail the job or substitute the default.
V1 font options
options.fontshref (URL) and storage (e.g. "external").options.globalFontoptions.manageMissingFonts"useDefault" or "fail". When "useDefault", missing fonts are replaced by globalFont if set, otherwise ArialMT.Example (V1):
{
"options": {
"fonts": [
{ "href": "<SIGNED_GET_URL>", "storage": "external" }
],
"globalFont": "ArialMT",
"manageMissingFonts": "useDefault"
}
}
V2 font options
fontOptions.additionalFontssource.url. Downloaded before the document is opened.fontOptions.defaultFontPostScriptNamefontOptions.missingFontStrategy"use_default" or "fail". Default is "use_default".Example (V2):
{
"fontOptions": {
"additionalFonts": [
{ "source": { "url": "<SIGNED_GET_URL>" } }
],
"defaultFontPostScriptName": "Arial_BoldMT",
"missingFontStrategy": "use_default"
}
}
Property mapping (V1 to V2)
options.fontsfontOptions.additionalFonts (use source.url instead of href + storage)options.globalFontfontOptions.defaultFontPostScriptNameoptions.manageMissingFontsfontOptions.missingFontStrategy"useDefault""use_default""fail""fail"Font types
Both V1 and V2 support three font sources:
.ttf, .otf) provided via fontOptions.additionalFonts.Default font value
- V1: If
globalFontis not set andmanageMissingFontsis"useDefault", the worker uses ArialMT as the fallback. - V2: If
defaultFontPostScriptNameis not set and the strategy is use default, the worker uses ArialMT as the fallback.
In both V1 and V2, the default fallback font is ArialMT when no global/default is specified.
Missing-font strategy
manageMissingFonts)missingFontStrategy)"useDefault""use_default" (default)"fail""fail"Adding text to existing document
V1 approach: documentOperations
curl -X POST \
https://image.adobe.io/pie/psdService/documentOperations \
-H "Authorization: Bearer $token" \
-H "x-api-key: $apiKey" \
-H "Content-Type: application/json" \
-d '{
"inputs": [{
"href": "<SIGNED_GET_URL>",
"storage": "external"
}],
"options": {
"fonts": [
{
"href": "<FONT_FILE_URL>",
"storage": "external"
}
],
"layers": [
{
"add": { "insertTop": true },
"type": "textLayer",
"bounds": {
"left": 25,
"top": 42,
"width": 977,
"height": 267
},
"text": {
"content": "NEW TEXT LAYER",
"characterStyles": [
{
"fontSize": 56,
"orientation": "horizontal",
"fontColor": {
"rgb": {
"red": 0,
"green": 0,
"blue": 0
}
},
"fontName": "AcerFoco-SemiboldItalic"
}
]
}
}
]
},
"outputs": [{
"href": "<SIGNED_POST_URL>",
"storage": "external",
"type": "image/vnd.adobe.photoshop"
}]
}'
V2 approach
curl -X POST \
https://photoshop-api.adobe.io/v2/create-composite \
-H "Authorization: Bearer $token" \
-H "x-api-key: $apiKey" \
-H "Content-Type: application/json" \
-d '{
"image": {
"source": {
"url": "<SIGNED_GET_URL>"
}
},
"fontOptions": {
"additionalFonts": [
{ "source": { "url": "<FONT_FILE_URL>" } }
],
"defaultFontPostScriptName": "Arial_BoldMT",
"missingFontStrategy": "use_default"
},
"edits": {
"layers": [
{
"type": "text_layer",
"name": "New Text Layer",
"operation": {
"type": "add",
"placement": {
"type": "top"
}
},
"text": {
"content": "NEW TEXT LAYER",
"antiAliasing": "smooth",
"textOrientation": "horizontal",
"frame": {
"type": "area",
"bounds": {
"top": 42,
"left": 25,
"right": 1002,
"bottom": 309
}
},
"characterStyles": [
{
"apply": { "from": 0, "to": 13 },
"characterStyle": {
"font": { "postScriptName": "AcerFoco-SemiboldItalic" },
"fontSize": 56,
"fontColor": {
"rgb": {
"red": 0,
"green": 0,
"blue": 0
}
}
}
}
]
}
}
]
},
"outputs": [
{
"destination": { "url": "<SIGNED_POST_URL>" },
"mediaType": "image/vnd.adobe.photoshop"
}
]
}'
Editing existing text layers
V1 approach
{
"options": {
"layers": [
{
"name": "Existing Text Layer",
"text": {
"content": "Updated Text"
},
"edit": {}
}
]
}
}
V2 approach
{
"edits": {
"layers": [
{
"name": "Existing Text Layer",
"operation": {
"type": "edit"
},
"text": {
"content": "Updated Text",
"characterStyles": [
{
"characterStyle": {
"fontSize": 48
}
}
]
}
}
]
}
}
Common migration patterns
Multi-style text
V1:
{
"text": {
"content": "Bold and Italic",
"characterStyles": [
{
"from": 0,
"to": 4,
"fontSize": 48,
"fontName": "Arial-BoldMT"
},
{
"from": 9,
"to": 15,
"fontSize": 48,
"fontName": "Arial-ItalicMT"
}
]
}
}
data-variant=info
data-slots=text
to was treated as length; in V2, apply.from and apply.to are both inclusive 0-based indices. Use the last character index for apply.to, not a length. See Character styles structure.V2: Use apply.from/apply.to (inclusive) and characterStyle with font.postScriptName
{
"text": {
"content": "Bold and Italic",
"characterStyles": [
{
"apply": { "from": 0, "to": 4 },
"characterStyle": {
"fontSize": 48,
"font": { "postScriptName": "Arial-BoldMT" }
}
},
{
"apply": { "from": 9, "to": 15 },
"characterStyle": {
"fontSize": 48,
"font": { "postScriptName": "Arial-ItalicMT" }
}
}
]
}
}
Multi-line text
Both V1 and V2 use \r for line breaks:
{
"text": {
"content": "Line 1\rLine 2\rLine 3"
}
}
Custom fonts
V1:
{
"options": {
"fonts": [
{
"href": "<FONT_FILE_URL>",
"storage": "external"
}
]
}
}
V2:
{
"fontOptions": {
"additionalFonts": [
{
"source": {
"url": "<FONT_FILE_URL>"
}
}
]
}
}
Character style properties
V2 has expanded character style options beyond V1:
fontSizefontNamefont.postScriptName in V2font.postScriptNamecharacterStyle.fontfontColorfontColor.rgb, fontColor.cmyk)orientationtextOrientation on textsyntheticBoldsyntheticItalicletterSpacinglineHeightfontAlphacapsOptionall_caps)data-variant=info
data-slots=text
Paragraph style properties
In V2, all paragraph style properties below are optional; defaults are applied when omitted.
alignmentleft (default), center, right, justify, justify_left, justify_right, justify_center.writingDirectionleft_to_right (default), right_to_left.firstLineIndentstartIndentendIndentspaceBeforespaceAfterText positioning
Frame types: point vs area
text.frame with type: "area" and bounds: { top, left, right, bottom }.text.frame with type: "point" and origin: { x, y }. Single-point text, no wrapping.V1 only supported area frames. V2 supports both point and area frames.
Default bounds when frame is omitted
left: 0, top: 0, width: 4, height: 4 (pixels).text.frame is omitted, the default is a point frame at the center of the canvas.In V1 the default was a small area at the top-left; in V2 the default is a point at the middle of the canvas. To get predictable placement in V2, always set text.frame (either type: "area" with bounds or type: "point" with origin).
Using bounds
V1: Layer-level bounds with left, top, width, height. Only area frame; no point frame support.
{
"type": "textLayer",
"name": "Positioned Text",
"bounds": {
"left": 100,
"top": 50,
"width": 800,
"height": 200
},
"text": {"content": "..."}
}
V2: Position and size the text using text.frame with type: "area" and bounds as top, left, right, bottom (no width/height). Convert V1 bounds: right = left + width, bottom = top + height.
{
"type": "text_layer",
"name": "Positioned Text",
"operation": {
"type": "add",
"placement": {
"type": "top"
}
},
"text": {
"content": "...",
"frame": {
"type": "area",
"bounds": {
"top": 50,
"left": 100,
"right": 900,
"bottom": 250
}
}
}
}
Font size units
Both V1 and V2 use pixels for fontSize. In V2 it goes inside characterStyle.fontSize.
Font color structure
Supported in both V1 and V2: rgb, cmyk, lab, and gray are all supported in both versions; all components use 16-bit integer values. Use the same nested shape in V2 as in V1.
Value ranges by color model:
rgbred, green, bluecmykcyan, magenta, yellow, blacklabllaba, bgraygrayAll components are required and default to 0 when omitted.
V1 and V2 (same structure): rgb, cmyk, lab, gray:
{
"fontColor": {
"rgb": {
"red": 32768,
"green": 0,
"blue": 0
}
}
}
{
"fontColor": {
"cmyk": {
"cyan": 32768,
"magenta": 0,
"yellow": 0,
"black": 0
}
}
}
{
"fontColor": {
"lab": {
"l": 5000,
"a": 10000,
"b": -5000
}
}
}
{
"fontColor": {
"gray": {
"gray": 16384
}
}
}
Migration checklist
When migrating text layer operations from V1 to V2:
- Change
type: "textLayer"totype: "text_layer" - Use
apply.from/apply.tofor character style ranges (V2); both indices are inclusive (0-based); wrap style properties incharacterStyle - Use
font.postScriptName(insidecharacterStyle.font) in V2 instead of top-levelfontName - Wrap paragraph style properties in
paragraphStyleobject - Font color: V1 and V2 both support rgb, cmyk, lab, and gray; use the same nested shape (
fontColor.rgb,fontColor.cmyk,fontColor.lab,fontColor.gray) - Update font management properties (
manageMissingFonts→missingFontStrategy,globalFont→defaultFontPostScriptName) - Update custom fonts:
options.fonts(href, storage) →fontOptions.additionalFonts(source.url) - Update inputs/outputs: V1
inputs[].href→ V2image.source.url; V1outputs[].href→ V2outputs[].destination.url, and usemediaTypeinstead oftype - Map layer placement: V1
add.insertTop(or insertBottom, etc.) → V2operation: { type: "add", placement: { type: "top" } }(or bottom, above, below, into) - Convert bounds: V1 layer
bounds(left, top, width, height) → V2text.framewithtype: "area"andbounds: { top, left, right, bottom }whereright = left + width,bottom = top + height
Common migration issues
Differences between V1 and V2 that often cause issues when migrating:
- Character and paragraph style range (
from/to): In V1,towas incorrectly taken as a length. In V2,apply.fromandapply.toare both inclusive 0-based character indices. When migrating, setapply.toto the last character index you want to style, not a length. Same applies to paragraph style ranges. - Layer type: V1 uses
"textLayer"; V2 uses"text_layer". - Font name: V1 uses top-level
fontName. V2 usescharacterStyle.font.postScriptName. - Bounds: V1 uses layer-level
boundswithleft,top,width,height. V2 usestext.framewithtype: "area"andbounds: { top, left, right, bottom }whereright = left + width,bottom = top + height. - Default when no frame/bounds given: V1 default was an area frame (0, 0, 4, 4). V2 default is a point frame at the center of the canvas. Set
text.frameexplicitly for predictable placement. - Font options: V1 uses
options.fonts(href, storage),options.globalFont,options.manageMissingFonts. V2 usesfontOptions.additionalFonts(source.url),fontOptions.defaultFontPostScriptName,fontOptions.missingFontStrategy(e.g."use_default").
Feature availability
Currently available in V2
- ✅ Add text layers
- ✅ Edit text content
- ✅ Character styles (font, size, color: rgb, cmyk, lab, gray)
- ✅ Paragraph styles (alignment)
- ✅ Multi-style text (from/to ranges)
- ✅ Custom fonts
- ✅ Text positioning (bounds)
- ✅ Expanded character properties (tracking, leading, etc.)
V1 features not yet in V2
- ⏳
fill_to_canvasproperty - ⏳ Some advanced text effects
Complete migration example
V1 documentOperations:
{
"inputs": [
{
"href": "<SIGNED_GET_URL>",
"storage": "external"
}
],
"options": {
"layers": [
{
"type": "textLayer",
"name": "Headline",
"text": {
"content": "Welcome",
"characterStyles": [
{
"fontSize": 72,
"fontName": "Arial-BoldMT",
"fontColor": {
"rgb": {
"red": 0,
"green": 0,
"blue": 0
}
}
}
],
"paragraphStyles": [
{
"alignment": "center"
}
]
},
"bounds": {
"left": 0,
"top": 100,
"width": 1920,
"height": 150
}
}
],
"manageMissingFonts": "useDefault"
},
"outputs": [
{
"href": "<SIGNED_POST_URL>",
"storage": "external",
"type": "image/vnd.adobe.photoshop"
}
]
}
V2 create-composite:
{
"image": {
"source": {
"url": "<SIGNED_GET_URL>"
}
},
"edits": {
"layers": [
{
"type": "text_layer",
"name": "Headline",
"text": {
"content": "Welcome",
"frame": {
"type": "area",
"bounds": {
"top": 100,
"left": 0,
"right": 1920,
"bottom": 250
}
},
"characterStyles": [
{
"characterStyle": {
"fontSize": 72,
"font": {
"postScriptName": "Arial-BoldMT"
},
"fontColor": {
"rgb": {
"red": 0,
"green": 0,
"blue": 0
}
}
}
}
],
"paragraphStyles": [
{
"paragraphStyle": {
"alignment": "center"
}
}
]
},
"operation": {
"type": "add"
}
}
]
},
"fontOptions": {
"missingFontStrategy": "use_default"
},
"outputs": [
{
"destination": {
"url": "<SIGNED_POST_URL>"
},
"mediaType": "image/vnd.adobe.photoshop"
}
]
}
Next steps
- Review Layer Operations Overview for general concepts
- Check Document Creation for creating new documents with text
- See Advanced Operations for text layer transforms and effects