curl --request POST \
--url https://api.print.app/v2/option-sets \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"config": {},
"productIds": [
"<string>"
],
"title": "<string>",
"dryRun": true
}
'import requests
url = "https://api.print.app/v2/option-sets"
payload = {
"config": {},
"productIds": ["<string>"],
"title": "<string>",
"dryRun": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({config: {}, productIds: ['<string>'], title: '<string>', dryRun: true})
};
fetch('https://api.print.app/v2/option-sets', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.print.app/v2/option-sets",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'config' => [
],
'productIds' => [
'<string>'
],
'title' => '<string>',
'dryRun' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.print.app/v2/option-sets"
payload := strings.NewReader("{\n \"config\": {},\n \"productIds\": [\n \"<string>\"\n ],\n \"title\": \"<string>\",\n \"dryRun\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.print.app/v2/option-sets")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"config\": {},\n \"productIds\": [\n \"<string>\"\n ],\n \"title\": \"<string>\",\n \"dryRun\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.print.app/v2/option-sets")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"config\": {},\n \"productIds\": [\n \"<string>\"\n ],\n \"title\": \"<string>\",\n \"dryRun\": true\n}"
response = http.request(request)
puts response.read_body{
"object": "option_set_change",
"dryRun": false,
"optionSetId": "2f7c9e1a-4b8d-4e2f-9a61-0c3d5b7e8f10",
"title": "Flyers",
"currency": "USD",
"created": true,
"deleted": false,
"changes": [
{
"productId": "8123456789",
"title": "Flyers",
"change": "added",
"fromOptionSetId": null,
"ok": true,
"error": null
}
],
"failed": 0,
"fields": [
{
"id": "size",
"label": "Size",
"type": "select-one",
"role": "size",
"source": {
"role": "size"
},
"reference": "po:role/size",
"sends": "choice id as value, label as text",
"choices": [
{
"value": "a5",
"label": "A5",
"selected": true
},
{
"value": "a4",
"label": "A4",
"selected": false
}
]
}
],
"warnings": []
}{
"error": true,
"code": "invalid_request",
"message": "width is required.",
"param": "width",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "unauthorized",
"message": "Missing or invalid secret key. Send it as \"Authorization: Bearer sk_…\".",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "account_suspended",
"message": "This account is suspended. Settle the outstanding invoice in the Print.App dashboard.",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "folder_not_empty",
"message": "The folder still holds designs. Remove them from it (or delete them) first.",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "payload_too_large",
"message": "The file at url is larger than 50 MB.",
"param": "url",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "unsupported_media_type",
"message": "Send the request body as JSON, with \"Content-Type: application/json\".",
"requestId": "FZn-uhymjoEEJow="
}{
"message": "Too Many Requests"
}{
"error": true,
"code": "internal_error",
"message": "Something went wrong on our side. Retry the request; if it keeps failing, contact support with the requestId.",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "service_unavailable",
"message": "Searching the library is unavailable right now. Try again in a minute.",
"requestId": "FZn-uhymjoEEJow="
}Create an option set
Checks an option set and publishes it to Shopify products: their pages show it (once the store’s theme has the Print Configurator block), and its prices apply at add-to-cart. It must be priced in the store’s currency (409 currency_mismatch). A product already on another option set moves to this one. With dryRun, nothing is written and the answer (200) says what would change. The merchant can open and edit it in Print Options, under Products.
curl --request POST \
--url https://api.print.app/v2/option-sets \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"config": {},
"productIds": [
"<string>"
],
"title": "<string>",
"dryRun": true
}
'import requests
url = "https://api.print.app/v2/option-sets"
payload = {
"config": {},
"productIds": ["<string>"],
"title": "<string>",
"dryRun": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({config: {}, productIds: ['<string>'], title: '<string>', dryRun: true})
};
fetch('https://api.print.app/v2/option-sets', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.print.app/v2/option-sets",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'config' => [
],
'productIds' => [
'<string>'
],
'title' => '<string>',
'dryRun' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.print.app/v2/option-sets"
payload := strings.NewReader("{\n \"config\": {},\n \"productIds\": [\n \"<string>\"\n ],\n \"title\": \"<string>\",\n \"dryRun\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.print.app/v2/option-sets")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"config\": {},\n \"productIds\": [\n \"<string>\"\n ],\n \"title\": \"<string>\",\n \"dryRun\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.print.app/v2/option-sets")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"config\": {},\n \"productIds\": [\n \"<string>\"\n ],\n \"title\": \"<string>\",\n \"dryRun\": true\n}"
response = http.request(request)
puts response.read_body{
"object": "option_set_change",
"dryRun": false,
"optionSetId": "2f7c9e1a-4b8d-4e2f-9a61-0c3d5b7e8f10",
"title": "Flyers",
"currency": "USD",
"created": true,
"deleted": false,
"changes": [
{
"productId": "8123456789",
"title": "Flyers",
"change": "added",
"fromOptionSetId": null,
"ok": true,
"error": null
}
],
"failed": 0,
"fields": [
{
"id": "size",
"label": "Size",
"type": "select-one",
"role": "size",
"source": {
"role": "size"
},
"reference": "po:role/size",
"sends": "choice id as value, label as text",
"choices": [
{
"value": "a5",
"label": "A5",
"selected": true
},
{
"value": "a4",
"label": "A4",
"selected": false
}
]
}
],
"warnings": []
}{
"error": true,
"code": "invalid_request",
"message": "width is required.",
"param": "width",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "unauthorized",
"message": "Missing or invalid secret key. Send it as \"Authorization: Bearer sk_…\".",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "account_suspended",
"message": "This account is suspended. Settle the outstanding invoice in the Print.App dashboard.",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "folder_not_empty",
"message": "The folder still holds designs. Remove them from it (or delete them) first.",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "payload_too_large",
"message": "The file at url is larger than 50 MB.",
"param": "url",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "unsupported_media_type",
"message": "Send the request body as JSON, with \"Content-Type: application/json\".",
"requestId": "FZn-uhymjoEEJow="
}{
"message": "Too Many Requests"
}{
"error": true,
"code": "internal_error",
"message": "Something went wrong on our side. Retry the request; if it keeps failing, contact support with the requestId.",
"requestId": "FZn-uhymjoEEJow="
}{
"error": true,
"code": "service_unavailable",
"message": "Searching the library is unavailable right now. Try again in a minute.",
"requestId": "FZn-uhymjoEEJow="
}Authorizations
A secret key from Print.App admin → Domains → Secret keys, sent as Authorization: Bearer sk_…. Server-side only.
Headers
Makes a retry safe: a second request with the same key and the same body gets the first response back (for 24 hours) instead of creating the resource again. Use a new key, such as a UUID, for every new request.
1 - 255Body
An option set and the Shopify products to publish it to.
The option set itself: Print Options' JSON (version "2.0"), as in the option set JSON reference.
Shopify product ids (from GET /v2/store-products), up to 50.
1 - 50 elements^(gid://shopify/Product/)?[0-9]{1,20}$Its name in Print Options. Default: the config's title.
1 - 200Check everything and say what would change, writing nothing.
Response
Created.
What a create, update or delete did to each product, or would do (dryRun).
"option_set_change"true when nothing was written.
The option set. null for a dry-run create.
Show child attributes
Show child attributes
Products that did not change.
x >= 0Its fields as connectors address them: give a field's source as a connector's source.
Show child attributes
Show child attributes
Things that will not stop it working but may not be meant.
Show child attributes
Show child attributes