Start your integration
Examples use placeholders. Replace IDs with values from your own account and use the latest revision for every edit. Response excerpts show relevant fields, not a second endpoint specification.
- Create an access token
- Create from a template or start from scratch
- Discover options and edit with REST
- Edit through Pencil AI
- Embed the live preview and renew access
- Prepare pricing and add to cart
- Create independent design variants
- Download designer and production outputs
1. Create an access token
In Pencil, open Platform Settings → API → Access tokens. Create a token with Read designs and Write designs, copy it once, and store it in your backend secret store. Use a separate test account/token for testing.
Send Authorization: Bearer <ACCESS_TOKEN>. Never put an account token in an iframe, a customer-facing bundle or a shared URL. The preview uses a separate temporary design-scoped session. The documentation is read-only; run authenticated requests from your backend or API client.
Choose the API origin for the same environment as your account. Dev uses https://dev-api.pencildesign.co/public-api/v1 and its viewer uses https://dev.pencildesign.co. This reference targets the API origin serving this page. Do not mix dev IDs, tokens or templates with another environment.
GET requests read data. For writes that require Idempotency-Key, generate and persist one unique key per intended operation. Retry an uncertain request with exactly the same key and body; do not generate another key for that retry. Use a fresh key for the next edit.
First authenticated request: your upload library
curl -X GET '<API_ORIGIN>/public-api/v1/uploads' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
{ "items": [], "nextCursor": null } — an empty account library is valid. HTTP 401 means the token is missing, expired, revoked or invalid.
2. Create from a template or start from scratch
For a finished design, obtain its DesignerTemplate ID from your account’s template configuration or ask your Pencil contact. It must be an enabled, published template accessible to that account. A Model ID and a parametric ring-template ID are different identifiers. This API does not currently list DesignerTemplates; do not guess an ID or treat a viewer URL as a template ID.
A template copy infers modelBuilder. If you also supply it, it must match or the request is rejected before creation. A blank design requires modelBuilder: RING, PENDANT, NAMEPLATE or GENERIC. Unsupported families are rejected. A blank design has no geometry until you add a shape or ask AI to build it.
landingPageSlug is optional. Supply the exact enabled page slug from your account for its pricing rules; the page is pricing context, not a restriction to its original stones. Omit it for design-only or supported unpriced Pencil carts. It is selected at creation, so choose it before editing.
Save id, revision, viewerUrl and viewerExpiresAt from creation. With a read/write token, viewerUrl opens the new design immediately. The URL contains a temporary secret; do not log it or send it to AI.
Copy a designer template
curl -X POST '<API_ORIGIN>/public-api/v1/designs' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"templateId": "<DESIGNER_TEMPLATE_ID>",
"name": "My ring",
"landingPageSlug": "<LANDING_PAGE_SLUG>"
}'
HTTP 201: { "apiVersion": "v1", "id": "<DESIGN_ID>", "revision": 1, "viewerUrl": "<VIEWER_URL>", "viewerExpiresAt": "<EXPIRATION>" }
Start a blank ring
curl -X POST '<API_ORIGIN>/public-api/v1/designs' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"modelBuilder": "RING",
"name": "New ring"
}'
HTTP 201 with a new design ID and revision. The preview is empty until geometry is added.
Discover parametric ring bands for the blank ring
curl -X GET '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/ring-options' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
Use an items[].id as RING_TEMPLATE_ID. Read its defaults, morphs and editable ranges. Follow nextCursor with ?after=<NEXT_CURSOR> when present.
Add a band using its existing defaults
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/changes' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"type": "ADD_RING_BAND",
"expectedRevision": 1,
"templateId": "<RING_TEMPLATE_ID>"
}'
HTTP 200: { "apiVersion": "v1", "id": "<DESIGN_ID>", "revision": 2 }. Read components to obtain the band componentId.
3. Discover options and edit with REST
The GET endpoints discover what the current design supports. GET components returns the existing instances and their stable componentId; component-options returns catalog choices. These IDs are not interchangeable. Follow catalog pagination and choose authorized IDs from the responses.
Preview controls without editing with POST parameter-options. For a band, put morphs at the top level beside componentId; do not put morphs inside parameters. Use exact names and ranges from the selected band’s morphOptions. Standard band dimensions belong in parameters as bandWidthMm and bandHeightMm, when offered. The response keeps saved values and revision unchanged; proposedParameters describes the proposal. Read scope is sufficient and no Idempotency-Key is needed.
POST changes applies one operation. Replace every sample expectedRevision with the latest saved revision. Read components after adding a stone to find the new componentId. To edit stone A without changing stone B, send only A’s componentId and the desired fields; compare B before and after.
Set shape, stoneTypeId or caratWeight independently, or combine them. width, length and depth are millimetres; do not combine dimensions with caratWeight in the same request. A type-only edit preserves physical dimensions, so carat may change with density. Use returned options and existing setting restrictions rather than assuming every combination is valid.
HTTP 409 REVISION_CONFLICT means another edit won. Read the latest snapshot, review the intended edit and submit a new operation with the current revision and a new key. Do not blindly replay it against another revision.
Read materials and supported changes
curl -X GET '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/parameter-options' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
Choose materialId from materials[].id. supportedCommands identifies applicable change types.
Read existing stone and band IDs
curl -X GET '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/components' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
Response contains id, revision and components. Save the componentId of each STONE and MAIN_SHAPE you want to target.
Discover controls for the selected band
curl -X GET '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/parameter-options?componentId=%3CBAND_COMPONENT_ID%3E' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
Read component.values and component.morphOptions. Offer only the morph keys and ranges returned for this band.
Preview band morphs without changing the design
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/parameter-options' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
--data '{
"componentId": "<BAND_COMPONENT_ID>",
"morphs": {
"bandWidth": 3
}
}'
For a band offering bandWidth: HTTP 200 with component.proposedParameters.morphs.bandWidth = 3. Saved component.values and revision remain unchanged. Apply the chosen edit separately through changes.
Preview standard band height without changing the design
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/parameter-options' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
--data '{
"componentId": "<BAND_COMPONENT_ID>",
"parameters": {
"bandHeightMm": 2
}
}'
Use only when bandHeightMm is offered in component.parameters. Returns proposed controls without saving or generating geometry.
Discover ready stone settings
curl -X GET '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/component-options?kind=STONE' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
Choose a ready setting from items and its supported stone type. Use the catalog config ID as STONE_CONFIG_ID.
Change material
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/changes' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"type": "SET_MATERIAL",
"expectedRevision": 2,
"materialId": "<MATERIAL_ID>"
}'
HTTP 200 with the new committed revision; the open preview updates.
Add a stone at the shared shop default position
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/changes' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"type": "ADD_STONE",
"expectedRevision": 3,
"stoneConfigId": "<STONE_CONFIG_ID>",
"stoneTypeId": "<STONE_TYPE_ID>",
"parentComponentId": "<BAND_COMPONENT_ID>",
"placement": {
"mode": "RING_DEFAULT"
}
}'
One new stone instance is added. Read components again; existing stones keep their identities.
Edit only stone A
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/changes' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"type": "SET_STONE_PARAMETERS",
"expectedRevision": 4,
"componentId": "<STONE_A_ID>",
"shape": "PEAR",
"stoneTypeId": "<RUBY_TYPE_ID>",
"caratWeight": 1
}'
HTTP 200 with a new revision. Stone A is pear ruby, approximately 1 ct; stone B is not targeted. Catalog/generator rounding can slightly change the measured carat.
Move and rotate only stone A
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/changes' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"type": "SET_RING_STONE_PLACEMENT",
"expectedRevision": 5,
"componentId": "<STONE_A_ID>",
"placement": {
"mode": "RING",
"locationDegrees": 200,
"heightMm": 0,
"sideOffsetMm": 0,
"rotationDegrees": {
"x": 0,
"y": 0,
"z": 90
}
}
}'
Only the selected stone placement changes. Use the saved placement as the starting point when changing one coordinate.
4. Edit through Pencil AI
Create a conversation for the design, then submit a turn from your backend. The run request waits for the result; configure your client timeout appropriately. Use precise component IDs for multi-stone edits. Your own AI can instead call the same REST changes endpoint.
SUCCEEDED means the turn completed. NEEDS_INPUT includes question.id, text and options: submit the answer in a new turn with answerToQuestionId and the latest revision. FAILED does not imply rollback: earlier tool changes may already be saved. Read the design and returned revision before deciding how to continue.
After a transport interruption, retry the exact request with the same idempotency key and body. If you have its run ID, GET that run to recover its state. Never automatically repeat “add a stone” with a new key: that could add another stone. QUEUED/RUNNING are non-terminal states; read the run again with backoff.
Create the conversation
curl -X POST '<API_ORIGIN>/public-api/v1/ai/conversations' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"designId": "<DESIGN_ID>"
}'
HTTP 201 with id (CONVERSATION_ID), designId, origin PUBLIC_API and createdAt.
Request a targeted edit
curl -X POST '<API_ORIGIN>/public-api/v1/ai/conversations/<CONVERSATION_ID>/runs' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"text": "Change only stone <STONE_A_ID> to a pear ruby, 1 carat. Keep the other stone and band unchanged.",
"expectedRevision": 6
}'
HTTP 200 with id (RUN_ID), status, revision, message, question and error. Example result fields: { "status": "SUCCEEDED", "revision": 7, "question": null, "error": null }.
Answer only if the turn requests clarification
curl -X POST '<API_ORIGIN>/public-api/v1/ai/conversations/<CONVERSATION_ID>/runs' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"text": "Use the first option.",
"expectedRevision": 7,
"answerToQuestionId": "<QUESTION_ID>"
}'
A new turn resolves the selected question. Use the actual question and saved revision, not the sample values.
Recover the turn state
curl -X GET '<API_ORIGIN>/public-api/v1/ai/conversations/<CONVERSATION_ID>/runs/<RUN_ID>' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
Read status and revision. A FAILED result includes error.code/message; inspect the latest saved design even if the turn failed.
5. Embed the live preview and renew access
Your backend returns viewerUrl to the customer site. Verify its origin against your independently configured Pencil app origin, then assign it as the iframe src. For example: <iframe title="Pencil design preview" src="<VIEWER_URL>" width="100%" height="600"></iframe>. Do not put the account token in the iframe.
Keep the same preview open while REST or AI edits run. It observes committed revisions automatically, typically polling every three seconds. It is read-only; the API performs the edits. Multiple quick edits may appear together as the latest state.
The creation URL is temporary and does not grant geometry preparation. Use browser-sessions to open an existing design, renew expiration, or request preparation. On renewal, recreate the iframe or fully navigate to the new URL; changing only its fragment does not reinitialize the viewer. Do not cache or log session URLs.
Renew read-only access from your backend
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/browser-sessions' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
--data '{
"mode": "VIEW",
"expiresInSeconds": 900
}'
HTTP 201 with viewerUrl, expiresAt and a design-scoped token. Give the viewerUrl to the iframe; keep your account credential on the backend.
6. Prepare pricing and add to cart
Create the design with landingPageSlug for the account’s pricing rules. Material, stone type/carat and geometry changes invalidate the previous preparation. Open a preparation-enabled viewer and wait for its current geometry before quoting or cart submission.
Browser rendering is required today: the shared viewer loads the assets, measures geometry and submits the glTF. A backend alone cannot substitute a plain VIEW token or account credential for this preparation grant. Without the viewer, weight/price may not become READY. Do not fabricate volume or submit example geometry.
A READY result includes the current revision and quoteId. Use those exact values for cart. If the shopper edits again, wait for a fresh READY result. Handle FAILED visibly; do not leave a spinner running. A missing page follows the existing unpriced Pencil cart policy (price may be null); fixed product prices still apply. External storefront handoffs require the applicable pricing/configuration.
The returned cart receipt identifies the frozen order/snapshot and downstream handoff. Follow the configured Shopify/Wix/Squarespace handoff in the response; API cart creation is not browser checkout or payment. If effectStatus is PENDING, read the receipt again; RECOVERY_REQUIRED needs investigation, not a new cart request.
Preparation uploads geometry, not a guaranteed fresh thumbnail. Do not assume merely opening the iframe or adding to cart captures a new screenshot. Production-file generation/download uses the existing Pencil order workflow; this API does not add a separate public production-export endpoint.
Open a preparation-enabled read-only viewer
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/browser-sessions' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
--data '{
"mode": "VIEW",
"expiresInSeconds": 900,
"prepareForCart": true
}'
Open the returned viewerUrl and keep it running while the geometry is prepared.
Wait for the current quote
curl -X GET '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/preparation' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
Example result fields: { "status": "READY", "revision": 7, "price": { "amount": 150, "currency": "USD" }, "quoteId": "<QUOTE_ID>" }. Amounts are illustrative; use the actual response.
Add the prepared snapshot to cart
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/cart' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"expectedRevision": 7,
"quoteId": "<QUOTE_ID>",
"customer": {
"name": "Example Shopper",
"email": "shopper@example.com",
"phoneNumber": "<SHOPPER_PHONE>",
"budget": "<SHOPPER_BUDGET>"
},
"externalSessionId": "<YOUR_SHOPPER_SESSION_ID>"
}'
HTTP 201 with cartId, orderId, snapshotModelId, status ADDED_TO_CART, price and effectStatus. Customer fields are examples; use consented shopper details or an existing leadId, never both.
Recover the same cart receipt
curl -X GET '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/cart/<CART_ID>' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
Read the immutable snapshot and current effectStatus/handoff. After a lost POST response, retry the same POST key/body instead of creating a second order.
7. Create independent design variants
Create one independent copy per variant from the same DesignerTemplate ID. Use a new idempotency key for each copy and save each returned designId/revision separately. Change each copy through REST or a separate AI conversation.
Example: create “Ruby variant” and “Sapphire variant”, then change their respective stone type and material. Each has its own preview, preparation and cart snapshot; editing one does not update the other. Copying a template copies its saved template state, not edits made to another API design.
You can also build each variant from scratch with the same sequence of band/stone changes. There is no bulk-variants endpoint and no documented copy-from-public-design-ID operation. For large batches, limit concurrency, follow pagination, handle 429 with backoff and keep durable request keys.
Create the ruby variant
curl -X POST '<API_ORIGIN>/public-api/v1/designs' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"templateId": "<DESIGNER_TEMPLATE_ID>",
"name": "Ruby variant",
"landingPageSlug": "<LANDING_PAGE_SLUG>"
}'
Save this design ID as VARIANT_A_ID and use its own revision for edits.
Create the sapphire variant with a different request key
curl -X POST '<API_ORIGIN>/public-api/v1/designs' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"templateId": "<DESIGNER_TEMPLATE_ID>",
"name": "Sapphire variant",
"landingPageSlug": "<LANDING_PAGE_SLUG>"
}'
Save this independent design ID as VARIANT_B_ID. Discover and target its own component IDs.
8. Download designer and production outputs
Use a backend credential with Read designs and Write designs and the same STL download entitlement as the editor. Browser grants cannot request or retrieve manufacturing files. Order snapshots remain read-only. No output operation creates an order or adds to a cart.
Finalize the current design through the existing editor download flow, or prepare it through the shared viewer. Scene freshness is independent of shop/cart pricing. PREPARATION_REQUIRED means the finalized scene or required native production quote is unavailable for the current geometry. Send the current expectedRevision and a new Idempotency-Key to request an output. A retry with the same key and body returns the same outputId without another job or design revision.
DESIGNER produces the existing designer STL with coordinates in meters. PRODUCTION and BAND_PRODUCTION use STL coordinates in millimeters. STL carries no unit declaration, so use the returned format and units fields when importing it. The optional production preview is GLB in meters (previewFormat and previewUnits). PRODUCTION uses the native full assembly recipe, material connections and default/scaled settings. isDefault defaults to true and is independent of shrinkagePercent (default 0). enableMultiMaterialProduction defaults to true and preserves existing different-metal separation/connection behavior; it does not introduce same-metal setting splits. BAND_PRODUCTION requires a visible band componentId and exports that band with its native attached parts.
Poll the output endpoint. PENDING means queued or running. READY includes a downloadUrl valid for expiresInSeconds (300 seconds), and previewUrl when the native recipe produces a material preview GLB. The STL and preview retain the native part behavior. Preview GLB mesh nodes carry extras.customizationId; apply the matching model.customizations[].material (or model.defaultMaterial) from a GET design snapshot whose revision matches the output, as the native viewer does. The preview does not bake glTF materials. Refresh links by reading the output again. FAILED has a safe error; use a new key to request a fresh attempt. STALE means the design or source changed: prepare the latest revision and request a new output. Polling never treats an old revision as current.
Queue records are retained for seven days. After a record expires, request a new output rather than retrying an old receipt indefinitely. Download generation uses the existing background processor and can take time.
Request a full production STL
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/outputs' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"expectedRevision": 8,
"kind": "PRODUCTION",
"isDefault": true,
"shrinkagePercent": 0,
"enableMultiMaterialProduction": true
}'
HTTP 202: { "apiVersion": "v1", "id": "<DESIGN_ID>", "revision": 8, "outputId": "<OUTPUT_ID>" }. The design revision remains 8.
Request one band with its attached parts
curl -X POST '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/outputs' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"expectedRevision": 8,
"kind": "BAND_PRODUCTION",
"componentId": "<BAND_COMPONENT_ID>"
}'
HTTP 202 with outputId. Uses native per-band export; sibling bands are excluded.
Read output status
curl -X GET '<API_ORIGIN>/public-api/v1/designs/<DESIGN_ID>/outputs/<OUTPUTID>' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'
HTTP 200: { "apiVersion": "v1", "id": "<DESIGN_ID>", "revision": 8, "outputId": "<OUTPUT_ID>", "kind": "PRODUCTION", "format": "STL", "units": "MILLIMETER", "status": "READY", "expiresInSeconds": 300, "downloadUrl": "<SIGNED_DOWNLOAD_URL>" }. Links appear only for complete current outputs.
API reference
This reference loads the deployed OpenAPI contract directly. Reading is public; protected calls require a valid token. This page is read-only and does not execute API requests.