Migrate from v2 to v3
Move previous-generation requests to v3 without duplicating writes or losing data.
The documented v2 and unversioned public operations are deprecated. They still authenticate, validate, and return the same bodies and status codes. This change adds deprecation signals and a supported path to v3. It does not remove an operation or change its business behaviour.
The generated schedule below records the fixed deprecation instant and scheduled Sunset. The six-month migration window starts at that deprecation instant. Actual route removal still depends on observed usage, affected-consumer communication, and separate approval. Do not treat the scheduled date as permission to leave a migration until the final day.
Read the response signals
Every affected response carries the same three headers, including authentication, validation, and not-found responses:
Deprecation: @<unix-seconds>Sunset: <http-date>Link: <migration-guide-url>; rel="deprecation"; type="text/html"Deprecation is an RFC 9745 structured-field date expressed as Unix seconds. Sunset is an RFC 8594 HTTP date. Link points to this guide and may appear alongside other Link values. These headers describe lifecycle state. They do not alter the response body.
The operation replacement table below is generated from the checked migration map. It covers all 13 deprecated request operations and names the Store-key scope required by each v3 replacement.
Replace wildcard keys
Legacy wildcard tokens continue to work during migration, including on v3, but do not carry a useful least-privilege boundary. Create a scoped key for the exact Store and operations you are moving. Start with GET /api/v3/stores/me and confirm data.id, data.platform, and data.scopes before reading or writing anything.
The API and documentation now say Store where older payloads or code may say Team. This is a terminology change at the public boundary. Do not rewrite an integer identifier merely because its label changed.
Keep the old credential until the migrated integration has passed read comparisons and a controlled write cutover. Revoke it once no previous-generation caller remains.
Update response handling
V3 success responses use an envelope:
{ "data": { "object": "customiser", "id": 42 }, "links": { "self": "https://web.signcustomiser.com/api/v3/customisers/42", "documentation": "https://www.signcustomiser.com/help/api/" }, "meta": { "api_version": "v3", "request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t" }}Collections add pagination and links.next. Follow links.next unchanged while pagination.has_more is true. Cursors are opaque and tied to the filter set that produced them.
V3 errors use RFC 9457 application/problem+json, not the previous generation’s message plus field-keyed errors shape. Branch on the HTTP status and stable code. Keep request_id in logs and support reports. Validation problems put field details in errors[] with JSON Pointers and may include allowed_values.
{ "type": "https://www.signcustomiser.com/help/api/problems/validation-failed", "title": "The request failed validation", "status": 422, "code": "validation_failed", "detail": "Correct the fields listed in errors.", "errors": [ { "pointer": "/pricing_model", "code": "unsupported_value", "allowed_values": ["simple_letter", "advanced_letter", "frame_fit"] } ], "request_id": "req_01jz9x2k7c8f3m5n6p7q8r9s0t"}Migrate pricing documents
Read the current v3 document before changing it. GET /api/v3/customisers/{customiser_id}/pricing returns the document under data. Send the document itself, without the data, links, or meta envelope, to a write operation.
Choose the write that matches your intent:
PATCHapplies an RFC 7396 JSON Merge Patch. Omitted properties stay unchanged,nullremoves a property, and arrays are replaced as a whole.PUTis a full replacement. Omitted sizes, price lists, and rows are deleted. Keep every record that must survive.POST /api/v3/customisers/{customiser_id}/pricing/validateruns the same validation without persisting, changing caches, or consuming an idempotency key. To dry-run a patch, apply it locally to the fetched document and validate the result.
Both persistent writes require pricing:write and an Idempotency-Key. Reusing the same key with the same method, path, and body replays the original result. Reusing it with a different body returns 409 idempotency_key_conflict.
Translate the document shape
Most structural fields remain snake_case. Apply these field changes before validation:
| Previous-generation field | v3 field or behaviour |
|---|---|
request wrapper pricing | send the pricing document as the whole request body |
response wrapper pricing | read the document from data |
customiser_id | unchanged; optional read-only guard on writes |
pricing_model | unchanged field name; translate its value with the generated glossary and treat it as a read-only guard |
value_units | removed; read currency and the field descriptions instead |
sizes | unchanged collection name |
sizes[].height_strategy | sizes[].sizing_strategy; translate every value with the generated glossary |
sizes[].height_measurement_mode | unchanged field name; translate LINE_HEIGHT and LETTER_HEIGHT to their lowercase values |
base_pricing and its snake_case fields | unchanged |
price_lists and its snake_case fields | unchanged, except each pricing_id becomes price_list_id |
sheet_pricing.pricings | sheet_pricing.rows |
cmLimitWidth | width_limit_cm |
cmLimitHeight | height_limit_cm |
basePrice | base_price |
pricePerSqCm | price_per_sq_cm |
| no equivalent | object, pricing_model_display_name, and currency are new read-only fields; omit them or echo the fetched values on writes |
| implied currency and units | integer amount fields use minor units; per-centimetre rates use decimal major units |
The generated glossary below owns every pricing and sizing enum translation. The important distinctions are:
FIXED_WIDTHbecomessimple_letter.MATERIAL_LENGTHbecomesadvanced_letter.FRAME_FITbecomesframe_fit.- The retired
FIXED_HEIGHTpricing model becomes read-onlylegacy_fixed_heightmigration metadata. V3 rejects it on writes. - The supported
fixed_heightsizing strategy is different. Its older spellings areHEIGHTandFIXED. - The supported
fixed_widthsizing strategy replacesWIDTHandDYNAMIC. LINE_HEIGHTbecomesline_height;LETTER_HEIGHTbecomesletter_height.
Pricing request diff
Previous-generation PUT wraps the document and mixes old enum values with camelCase sheet bands:
{ "pricing": { "pricing_model": "FIXED_WIDTH", "sizes": [ { "size_id": 7, "width_cm": 50, "height_strategy": "WIDTH", "height_measurement_mode": "LINE_HEIGHT" } ], "sheet_pricing": { "pricings": [ { "cmLimitWidth": 100, "cmLimitHeight": 60, "basePrice": 2000, "pricePerSqCm": 1.5 } ] } }}V3 PUT takes the canonical document directly:
{ "pricing_model": "simple_letter", "sizes": [ { "size_id": 7, "width_cm": 50, "sizing_strategy": "fixed_width", "height_measurement_mode": "line_height" } ], "sheet_pricing": { "rows": [ { "width_limit_cm": 100, "height_limit_cm": 60, "base_price": 2000, "price_per_sq_cm": 1.5 } ] }}The v3 response puts the saved document in data, then adds links and meta. Do not send those envelope fields back on a write.
Move webhook management
Webhook CRUD moves to /api/v3/webhook-subscriptions. The old PUT update becomes PATCH and accepts a merge patch for topic and url. Create, update, and delete require webhooks:write plus an Idempotency-Key. Reads require webhooks:read.
Replace GET /api/v2/webhook-examples/{topic} with GET /api/v3/webhook-event-types. The v3 catalogue returns every available topic and a static example in one response. Clients must tolerate new topic values. Subscription creation returns its signing secret once; later reads do not expose it.
The delivery payload and signature-verification contract are separate from subscription CRUD. Keep verifying the raw request body before decoding JSON.
Move integration writes
Both legacy order-create routes move to POST /api/v3/orders. The unversioned product route moves to POST /api/v3/customisers/{customiser_id}/products. These operations require enabled custom integrations and Store-scoped keys.
Translate the v2 custom-integration order
| v2 order field | v3 order field or behaviour |
|---|---|
integration_id | unchanged |
external_id | store_order_id |
external_order_number | store_order_number |
| no equivalent | total_amount is now required in integer minor currency units |
currency | unchanged; send an uppercase ISO 4217 code |
shipping_line | shipping_method |
customer.* | unchanged field names |
billing_address.*, shipping_address.* | unchanged snake_case names; v3 also accepts nullable company, email, and phone fields |
products[].external_id | line_items[].product_id; use the integer data.id returned by the v3 product write |
products[].quantity | line_items[].quantity |
products[].price | removed from the order request; v3 snapshots the price from the referenced product |
The v2 response uses an order wrapper and repeats the submitted external product fields. V3 returns 201, puts the canonical order under data, projects stored products under data.line_items, and adds links and meta.request_id.
Translate the unversioned order
| Unversioned field | v3 order field or behaviour |
|---|---|
order_id | store_order_id |
order_number | store_order_number |
order_total | total_amount; convert decimal major units to integer minor units before sending |
order_currency | currency |
email | customer.email |
shipping_line | shipping_method |
shipping_address.name | split into shipping_address.first_name and shipping_address.last_name |
shipping_address.address1, address2, zip | shipping_address.address_1, address_2, postcode |
| other shipping address fields | unchanged names |
products[].id | create each product through v3 first, then use its integer data.id as line_items[].product_id |
| legacy product artwork fields | move them to the v3 product write, not the order request |
The unversioned response is only { "ok": true }. V3 returns the stored order resource, line-item projection, self link, and request ID.
Translate the unversioned product
| Unversioned field | v3 product field or behaviour |
|---|---|
path {customiser} | path {customiser_id} |
| no equivalent | integration_id, title, price_amount, and currency are now required |
product_id | store_product_id |
cart | transform the selections you need into customisations[] records with stable snake_case key, label, and value fields |
price_breakdown | no direct field; calculate price_amount in minor units and keep any integration-specific breakdown outside the API resource |
custom_background_path | artwork.custom_background_url |
custom_background_original_path | artwork.custom_background_original_url |
logo_original_path | artwork.source_file_url |
product_type_paths | no generic map; send supported URLs through the matching explicit artwork field, such as preview_image_url, outline_image_url, svg_url, eps_url, illustrator_pdf_url, or dxf_url |
The unversioned response is only { "ok": true }. The v3 201 response returns the product under data, including its integer ID for later order line items, plus links and meta.request_id.
The product write now names the integration and external record explicitly. Money uses integer minor units, dimensions use _cm, and artwork fields sit under artwork:
{ "integration_id": 17, "store_product_id": "ext-product-1001", "title": "Custom neon sign", "price_amount": 12900, "currency": "AUD", "width_cm": 80.5, "height_cm": 31.5, "artwork": { "preview_image_url": "https://cdn.example.com/sign.png", "svg_url": "https://cdn.example.com/sign.svg" }}A successful response returns the modern product ID under data.id. Use that ID as line_items[].product_id when creating the v3 order:
{ "integration_id": 17, "store_order_id": "ext-order-1001", "store_order_number": "#1001", "total_amount": 25800, "currency": "AUD", "line_items": [{ "product_id": 731, "quantity": 2 }]}The order response returns data.object: "order", data.status: "received", the stored line-item projection, a links.self URL, and meta.request_id. received means Sign Customiser accepted the order. It does not claim payment, manufacture, fulfilment, shipping, refund, or cancellation.
Both integration writes require a unique Idempotency-Key for each logical record. A replay with the same key and body returns the original 201 and Idempotency-Replay: true. Never send one logical product or order to the previous-generation and v3 write routes in parallel.
Cut over without duplicate writes
- Inventory every previous-generation method and path in your integration. Match the result against the generated table below.
- Create a Store-scoped key with the listed read and write scopes. Confirm the Store with
/api/v3/stores/me. - Move reads first. Compare old and v3 data in a non-production environment while teaching the client about envelopes, pagination, problems, and canonical values.
- Transform a fetched pricing document, then use the validate endpoint. Test
PATCHorPUTagainst controlled data only after validation passes. - Add durable idempotency keys to every v3 write. Store each key with the logical product, order, webhook change, or pricing change.
- Stop the old writer before enabling its v3 replacement. Do not dual-write. If the cutover result is uncertain, retry the v3 request with the same key and byte-equivalent body.
- Monitor v3 responses and
request_idvalues. Keep the old path disabled rather than using it as an automatic write fallback. - Remove old reads, revoke the wildcard key, and confirm no previous-generation request remains before the scheduled Sunset.
The tables below are generated from the checked operation map and canonical glossary. Use them as the source for route, scope, pricing, and sizing translations.
Deprecation schedule
- Deprecation took effect
- 22 July 2026
- Sunset
- 22 January 2027
Operation replacements
| Previous operation | v3 replacement | Required scope |
|---|---|---|
POST /api/v2/orders | POST /api/v3/orders | orders:write |
GET /api/v2/customisers | GET /api/v3/customisers | customisers:read |
GET /api/v2/customisers/{customiser} | GET /api/v3/customisers/{customiser_id} | customisers:read |
POST /api/orders | POST /api/v3/orders | orders:write |
POST /api/customisers/{customiser}/products | POST /api/v3/customisers/{customiser_id}/products | products:write |
GET /api/v2/customisers/{customiser}/pricing | GET /api/v3/customisers/{customiser_id}/pricing | pricing:read |
PUT /api/v2/customisers/{customiser}/pricing | PUT /api/v3/customisers/{customiser_id}/pricing | pricing:write |
GET /api/v2/webhooks | GET /api/v3/webhook-subscriptions | webhooks:read |
POST /api/v2/webhooks | POST /api/v3/webhook-subscriptions | webhooks:write |
GET /api/v2/webhooks/{webhook_id} | GET /api/v3/webhook-subscriptions/{webhook_id} | webhooks:read |
PUT /api/v2/webhooks/{webhook_id} | PATCH /api/v3/webhook-subscriptions/{webhook_id} | webhooks:write |
DELETE /api/v2/webhooks/{webhook_id} | DELETE /api/v3/webhook-subscriptions/{webhook_id} | webhooks:write |
GET /api/v2/webhook-examples/{topic} | GET /api/v3/webhook-event-types | webhooks:read |
Finish kind
Field: finish_kind
| Canonical API value | Legacy spelling | Current UI label | Notes |
|---|---|---|---|
colour | COLOUR | Colour | |
texture | TEXTURE | Texture |
Height measurement mode
Field: height_measurement_mode
| Canonical API value | Legacy spelling | Current UI label | Notes |
|---|---|---|---|
line_height | LINE_HEIGHT | Line height | |
letter_height | LETTER_HEIGHT | Letter height |
Pricing model
Field: pricing_model
| Canonical API value | Legacy spelling | Current UI label | Notes |
|---|---|---|---|
simple_letter | FIXED_WIDTH | Simple Letter | |
advanced_letter | MATERIAL_LENGTH | Advanced Letter | |
frame_fit | FRAME_FIT | Frame Fit | |
legacy_fixed_height | FIXED_HEIGHT | Fixed Height (legacy) | Read-only legacy value. Read-only migration metadata. It is not accepted on writes. |
Product family
Field: product_family
| Canonical API value | Legacy spelling | Current UI label | Notes |
|---|---|---|---|
neon | NEON | Neon | |
channel_letter | CHANNEL | Channel Letter | |
metal_cut_out | CUTOUT_METAL | Metal Cut-Out |
Selection mode
Field: selection_mode
| Canonical API value | Legacy spelling | Current UI label | Notes |
|---|---|---|---|
single_style | SINGLE | Single | |
per_word | MULTI | Multiple |
Sign category
Field: sign_category
| Canonical API value | Legacy spelling | Current UI label | Notes |
|---|---|---|---|
neon | NEON | Neon | |
acrylic_channel_letter | ACRYLIC | Channel Letter | |
metal_channel_letter | METAL | Metal Channel Letter | |
metal_cut_out | CUTOUT_METAL | Metal Cut-Out | |
stencil | STENCIL | Stencil | |
lightbox | LIGHTBOX | Lightbox | |
wood_letter | WOOD | Wood Letter | |
aluminium_plate | ALUMINIUM | Aluminium Plate | |
braille_sign | BRAILLE | Braille Sign | |
wood_frame | WOOD_FRAME | Wood Frame |
Sizing strategy
Field: sizing_strategy
| Canonical API value | Legacy spelling | Current UI label | Notes |
|---|---|---|---|
fixed_width | WIDTH, DYNAMIC | Fixed Width - Height scales dynamically | |
fixed_height | HEIGHT, FIXED | Fixed Height - Width scales dynamically | Supported sizing strategy; this is not the retired FIXED_HEIGHT pricing model. |