Customiser extras
Attach an extra choice image
Takes the raster image of a staged upload and puts it on one choice of a
dropdown or image-choice extra, returning the extra with that choice's
*_url pointing at the new file. Requires customisers:write and an
Idempotency-Key.
Overview
The choice is addressed by its value, percent-encoded as a single path
segment (RFC 3986): a choice valued warm white is
…/choices/warm%20white/images/image. A value the extra does not offer
answers 404, exactly as an unknown extra id does. Two values cannot be
addressed at all and have to be renamed first: the empty string, and any
value containing /.
Create the upload first with POST/api/v3/uploads — purpose
option_image for both slots — send the bytes to the presigned target it
returns, then send that upload's id here. An upload whose purpose is not
the slot's answers 422 upload_purpose_mismatch. The upload is single
use: attaching it a second time answers 409, and each slot needs its own
upload.
Only PNG, JPEG and WebP are accepted. SVG is refused when the upload is
created, because the option-image path stores files as they arrive and
does not sanitise them.
The bytes are verified here, not at create: the object must exist, be
within the purpose's cap, start with the magic bytes of the type it was
declared as, and carry a readable image header. A file that fails leaves
the upload rejected with the failure's code, and attaching it again
replays that code — fix the file and create a new upload rather than
retrying.
Attaching to a slot that already holds an image replaces it. The previous
file is deleted once the new one is committed, so any URL published for it
stops resolving.
A picture survives a later select_options rewrite that keeps the
choice's value, because a write matches each submitted choice to the
stored one with the same value and carries the images across. A choice
given a new value starts without pictures. An attach racing such a rewrite
cannot be silently dropped: both writers compose the stored list under the
extra's row lock, so whichever commits second is composed from the other's
result.
The attach clears the customiser's cached storefront configuration and
runs the advisory language sync. It never regenerates Shopify products,
product images, or the product cache.
Authorisation
Bearer token required. Include it in theAuthorization header.
Required scope: customisers:write
Path parameters
The customiser id.
Example: 1
The extra id.
Example: 9
The choice's value, percent-encoded as one path segment. The empty string, and any value containing a forward slash, cannot be addressed.
Example: oak
Which of the choice's pictures to write (closed set): the artwork the choice is drawn as, or the example photograph shown beside it.
Example: image
Request body
Request body example
{
"upload_id": "upl_9f2c4b7ad13e6058ba41cd"
}curl https://web.signcustomiser.com/api/v3/customisers/1/extras/9/choices/oak/images/image \
--request PUT \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_API_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"upload_id": "upl_9f2c4b7ad13e6058ba41cd"
}'{
"data": {
"object": "extra",
"id": 9,
"customiser_id": 42,
"name": "Backing",
"description": null,
"input_type": "image_choice",
"true_label": null,
"false_label": null,
"default_value": null,
"char_limit": null,
"yes_colour_exclusions": null,
"no_colour_exclusions": null,
"select_options": [
{
"value": "oak",
"label": "Oak",
"hexcode": null,
"image_url": "https://cdn.example.com/files/2f1c9c0e-6a7b-4c1d-8e2f-3a4b5c6d7e8f.png",
"example_image_url": null,
"base_price": 0,
"price_multiplier": 0,
"price_multiplier_target": "base_letter_price"
}
],
"image_url": null,
"base_price": null,
"currency": "USD",
"price_multiplier": null,
"price_multiplier_target": null,
"sort_order": 1,
"created_at": "2026-08-13T00:00:00Z",
"updated_at": "2026-08-13T00:05:00Z"
},
"links": {
"self": "https://web.signcustomiser.com/api/v3/customisers/42/extras/9/choices/oak/images/image",
"documentation": "https://www.signcustomiser.com/help/api/"
},
"meta": {
"api_version": "v3",
"request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"
}
}{
"type": "https://www.signcustomiser.com/help/api/problems/missing_idempotency_key",
"title": "Missing idempotency key",
"status": 400,
"code": "missing_idempotency_key",
"detail": "Provide an Idempotency-Key header for this write.",
"request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"
}{
"type": "https://www.signcustomiser.com/help/api/problems/missing_api_key",
"title": "Missing API key",
"status": 401,
"code": "missing_api_key",
"detail": "Provide a store API key as a bearer token.",
"request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"
}{
"type": "https://www.signcustomiser.com/help/api/problems/insufficient_scope",
"title": "Insufficient scope",
"status": 403,
"code": "insufficient_scope",
"detail": "This operation requires the customisers:write scope.",
"required_scopes": [
"customisers:write"
],
"granted_scopes": [
"customisers:read"
],
"request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"
}{
"type": "https://www.signcustomiser.com/help/api/problems/resource_not_found",
"title": "Resource not found",
"status": 404,
"code": "resource_not_found",
"detail": "The requested resource does not exist or does not belong to the authenticated store.",
"request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"
}{
"type": "https://www.signcustomiser.com/help/api/problems/upload_already_attached",
"title": "Upload already attached",
"status": 409,
"code": "upload_already_attached",
"detail": "This upload has already been attached to a file slot. Each upload is single use: create a new upload for each assignment.",
"request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"
}{
"type": "https://www.signcustomiser.com/help/api/problems/upload_purpose_mismatch",
"title": "Upload purpose mismatch",
"status": 422,
"code": "upload_purpose_mismatch",
"detail": "This slot accepts uploads created with purpose option_image, and this upload was created with purpose font_ttf. Create a new upload with the slot's purpose.",
"errors": [
{
"pointer": "/upload_id",
"code": "upload_purpose_mismatch",
"detail": "The upload's purpose is font_ttf.",
"allowed_values": [
"option_image"
]
}
],
"request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"
}{
"type": "https://www.signcustomiser.com/help/api/problems/rate_limited",
"title": "Too many requests",
"status": 429,
"code": "rate_limited",
"detail": "You have exceeded the request limit for this API key.",
"retry_after": 37,
"request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"
}