← Petit Point
API
Tokens

Drive Petit Point from your own code

The metered step - the stitch plan - is available over HTTP. A natural use is a canvas shop or a charting tool: your code charts the design, sends its palette and counts, and shows the plan beside the chart.

The split first: the page does the pixels and the counting; the model writes the judgement. Charting, the palette, dithering, lone-stitch tidying, the counts, the yarn and time estimates and the reply checks ship as a plain script, stitchkit.js (StitchKit.downsample, quantize, renderSlogan, stats, buildInput, toWire, normalize, reconcile), which you can load in node. Swatch names (W01-W48) are this app's approximations, not a manufacturer's colour numbers; yardage and hours are rough estimates.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"petit-point"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the plan is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task. There is one: plan. It returns a title; a readability verdict (reads, tweak or rework) with a summary and located issues; 0–5 palette moves (merge, swap, outline, keep); exactly one stitch per palette colour; 3–7 steps of work; finishing steps for the project; a time range; tips and warnings.

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (StitchKit.toWire does this). task, palette and facts are required.

Input fields

FieldTypeNotes
projectstringbag, ornament, pillow, belt, coaster, key fob, wall hanging, eyeglass case, other.
skillstringbeginner, intermediate or confident.
meshnumber10, 12, 13, 14 or 18 holes per inch.
source, sloganstringphoto or slogan; slogan lines joined with / .
subject, notesstringWhat the picture shows (200 characters) and anything else (800; longer text keeps its start and end around a [...] marker).
sizestringStitches and finished inches.
palettestring (JSON)Array of {code, name, hex, symbol, stitches, pct, regions, largest_region, lone_stitches, yards_est}, most stitches first.
factsstring (JSON){stitches_w, stitches_h, total_stitches, finished_in, cut_canvas_in, lone_stitches_total, small_regions, edge_color, low_contrast_pairs[], yards_total_est, hours_est, tidied, dithered, letter_stroke_stitches} from StitchKit.stats.
mapstringA coarse text picture: the block size on the first line, then one palette symbol per block, at most 32 per row.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with StitchKit.parseReply, then normalize, then check it with reconcile(plan, stats, project): it flags colours with no stitch, codes not in the chart, swaps with no valid target, a reads verdict that ignores many lone stitches, low-contrast pairs left unaddressed, a time range far from the stitch count, and half cross on a piece that gets handled. applyMoves(grid, plan.palette_moves) applies the merges and swaps to a grid.

Worked examples

Replies produced against the app's prompt (palette and lists shortened here; the full replies ship in example-result.js). The page replays each of them free from its example buttons.

task: plan - a two-colour slogan tote for a beginner (reads)

Input (decoded for reading; on the wire palette and facts are JSON strings):

{
  "task": "plan",
  "project": "bag",
  "skill": "beginner",
  "mesh": 13,
  "source": "slogan",
  "slogan": "SOUP / SEASON",
  "subject": "",
  "notes": "My first canvas - I want it on the front of a canvas tote for fall.",
  "size": "104 x 54 stitches, 8 x 4.2 in at 13 mesh",
  "palette": [
    {
      "code": "W02",
      "name": "Oat",
      "hex": "#e8dcc4",
      "symbol": "A",
      "stitches": 4688,
      "pct": 83.5,
      "regions": 6,
      "largest_region": 3608,
      "lone_stitches": 0,
      "yards_est": 32
    },
    {
      "code": "W19",
      "name": "Tomato",
      "hex": "#d93b2b",
      "symbol": "B",
      "stitches": 928,
      "pct": 16.5,
      "regions": 35,
      "largest_region": 288,
      "lone_stitches": 0,
      "yards_est": 6.5
    }
  ],
  "facts": {
    "stitches_w": 104,
    "stitches_h": 54,
    "total_stitches": 5616,
    "finished_in": "8 x 4.2",
    "cut_canvas_in": "12 x 9",
    "lone_stitches_total": 0,
    "small_regions": 0,
    "edge_color": "W02",
    "low_contrast_pairs": [],
    "yards_total_est": 38.5,
    "hours_est": {
      "low": 18,
      "high": 28.1
    },
    "tidied": 0,
    "dithered": false,
    "letter_stroke_stitches": 2
  },
  "map": "block = 4x4 stitches, symbols as in palette\nAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAABAAAAAAAAA\n..."
}

Reply:

{
  "task": "plan",
  "title": "Soup Season Tote Banner",
  "readability": {
    "verdict": "reads",
    "summary": "At 13 mesh the Tomato letters sit in a 2-stitch-wide stroke, which is clear of the 1-stitch danger zone the page warns about, and there are no low-contrast pairs, no dithering, and not a single lone stitch to muddy an edge. SOUP and SEASON each sit well clear of the canvas border inside a generous Oat field, so the two words read as a clean two-line banner rather than crowding the frame. The biggest risk is not the chart but the stitching: Tomato breaks into 35 small regions for only 928 stitches, so a dragged dark fiber under the Oat or a sloppy thread start is more likely to spoil the piece than anything in the design itself.",
    "issues": [
      {
        "issue": "The round letters (the O in SOUP, the O and S in SEASON) are built from short, stepped strokes only 2 stitches wide, so a basketweave fill can round their corners into a blockier shape than charted.",
        "where": "the curves of the O and S letters in both SOUP and SEASON",
        "fix": "Work every letter in continental stitch instead of basketweave so each stroke holds its exact shape, then fill the surrounding Oat in basketweave."
      }
    ]
  },
  "palette_moves": [
    {
      "action": "keep",
      "codes": [
        "W02"
      ],
      "to": "",
      "why": "Oat covers 83.5% of the chart with no low-contrast neighbor and already sits on every edge, so it works as charted for the background."
    },
    {
      "action": "keep",
      "codes": [
        "W19"
      ],
      "to": "",
      "why": "Tomato against Oat has strong contrast with nothing bordering it closely in color, so the lettering needs no swap or outline to stand out."
    }
  ],
  "stitches": [
    {
      "code": "W02",
      "area": "the Oat field behind and around the SOUP and SEASON lettering",
      "stitch": "basketweave",
      "why": "it is the largest area on the canvas (83.5%, 6 regions) and basketweave keeps the canvas square with the back fully covered for a tote that will be handled."
    },
    {
      "code": "W19",
      "area": "the SOUP and SEASON letters",
      "stitch": "continental",
      "why": "continental keeps the 2-stitch-wide letter strokes crisp, matching the rule that letters get continental rather than basketweave."
    }
  ],
  "order": [
    {
      "step": 1,
      "do": "Stitch every letter of SOUP in continental, working each stroke as a single clean line; come up in an empty hole and go down into the shared holes at the edge of each stroke so the Tomato fiber sets before any Oat crosses it.",
      "codes": [
        "W19"
      ]
    },
    {
      "step": 2,
      "do": "Stitch every letter of SEASON the same way in continental, keeping tension even across its regions since it has more letters and more thread start-and-stops than SOUP.",
      "codes": [
        "W19"
      ]
    },
    {
      "step": 3,
      "do": "Fill the Oat background in basketweave directly above, below, and between the two lines of text first, coming up through the holes the Tomato stitches already used so the dark fiber is not dragged through the light yarn later.",
      "codes": [
        "W02"
      ]
    },
    {
      "step": 4,
      "do": "Finish the remaining Oat background - the top margin, the bottom margin, and the side margins - in basketweave, working alternating diagonal rows for even canvas tension across the full field.",
      "codes": [
        "W02"
      ]
    }
  ],
  "finishing": {
    "steps": [
      "Steam-block the finished piece face down on a padded board until it is square to 8 x 4.2 in, then let it dry fully before handling it again.",
      "Trim the 12 x 9 in canvas down, leaving about a 1 in blank-canvas margin around the stitched 8 x 4.2 in area for turning under.",
      "Fuse a lightweight iron-on interfacing to the back of the piece so the canvas threads do not fray or shift once it is sewn onto the tote.",
      "Fold and press the blank canvas margin to the back, mitering the corners so they lie flat.",
      "Pin the panel to the front of the canvas tote and topstitch close to the folded edge all the way around to mount it.",
      "If the tote fabric is heavy or textured, back the panel with a scrap of fusible fleece first so the Tomato lettering still reads crisply once it is mounted."
    ],
    "notes": "This is a patch applied to an existing tote front rather than a bag built from the canvas itself, so stabilizing the edges before topstitching matters more than blocking alone."
  },
  "time": {
    "hours_low": 18,
    "hours_high": 28.1,
    "basis": "Matches the page's own estimate for a beginner: basketweave covers 83.5% of 5616 stitches at a beginner's pace, and the 928 Tomato stitches in continental add thread changes across 35 small regions but no decorative stitches, so there is no reason to move off the page's range."
  },
  "tips": [
    "Thread one length of Tomato per letter where you can instead of restarting mid-stroke - with 35 separate regions in only 928 stitches, extra starts and stops are the easiest way to lose time on this chart.",
    "Since there are only two colors and no low-contrast pairs to confuse, park a threaded needle of each color on the back instead of stripping and rethreading between every letter and the background."
  ],
  "warnings": [
    "Do not stitch this in half cross - it leaves the back too thinly covered for a tote that gets handled and grabbed; stick with basketweave for the background and continental for the letters.",
    "When trimming the 12 x 9 in canvas after blocking, leave a full margin around the 8 x 4.2 in stitched area - cutting too close to the Tomato letters risks the canvas threads next to them unraveling before the edge is turned under."
  ]
}

task: plan - a photo of lemons for a pillow (tweak)

Input (decoded for reading; on the wire palette and facts are JSON strings):

{
  "task": "plan",
  "project": "pillow",
  "skill": "intermediate",
  "mesh": 13,
  "source": "photo",
  "slogan": "",
  "subject": "Three lemons and two leaves on blue tiles with white grout",
  "notes": "",
  "size": "104 x 104 stitches, 8 x 8 in at 13 mesh",
  "palette": [
    {
      "code": "W39",
      "name": "Cornflower",
      "hex": "#5b86d6",
      "symbol": "A",
      "stitches": 6340,
      "pct": 58.6,
      "regions": 36,
      "largest_region": 256,
      "lone_stitches": 0,
      "yards_est": 43.5
    },
    {
      "code": "W26",
      "name": "Lemon",
      "hex": "#f7e04a",
      "symbol": "B",
      "stitches": 2398,
      "pct": 22.2,
      "regions": 2,
      "largest_region": 1529,
      "lone_stitches": 0,
      "yards_est": 16.5
    },
    {
      "...": "5 more colours"
    }
  ],
  "facts": {
    "stitches_w": 104,
    "stitches_h": 104,
    "total_stitches": 10816,
    "finished_in": "8 x 8",
    "cut_canvas_in": "12 x 12",
    "lone_stitches_total": 2,
    "small_regions": 33,
    "edge_color": "W01",
    "low_contrast_pairs": [],
    "yards_total_est": 75.5,
    "hours_est": {
      "low": 21.6,
      "high": 33.8
    },
    "tidied": 131,
    "dithered": false,
    "letter_stroke_stitches": 0
  },
  "map": "block = 4x4 stitches, symbols as in palette\nAAAADAAAAAAAAAAAADAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAEAAAAAAAAAAAA\nDAAADAAACAAEEDEEEDAAACAAAA\n..."
}

Reply:

{
  "task": "plan",
  "title": "Three Lemons on Blue Tile",
  "readability": {
    "verdict": "tweak",
    "summary": "At 104 x 104 stitches on 13 mesh (8 x 8 in), the tile-and-lemon chart mostly reads cleanly: the page already tidied 131 stray stitches down to just 2 lone stitches, no low-contrast pairs were flagged, and the lemon fill (22.2 pct, largest region 1529 stitches) and the tile background (58.6 pct) are each a strong, legible shape. The single biggest risk is that two of the three lemons share one connected 1529-stitch region and are marked apart only by two W25 stitches at the seam - lose that and the pair reads as one oversized lemon. W05 Slate is also pure confetti (41 stitches spread over 18 tiny regions) and is better merged into the background before you cut floss for it.",
    "issues": [
      {
        "issue": "W05 Slate is confetti: 41 stitches split into 18 separate regions averaging about 2 stitches each, largest only 5.",
        "where": "scattered along the lemon edges and into the tile background; each patch is too small to show as its own block on the map.",
        "fix": "Merge W05 into the W39 background (see palette_moves). That keeps the lemon silhouette crisp and removes 18 thread starts for a colour that reads as noise at this size, not detail."
      },
      {
        "issue": "The big lemon shape (the 1529-stitch region) is actually two lemons touching, and only two W25 stitches mark the seam between them.",
        "where": "upper-center of the lemon mass, around the block at row 11, columns 13-14 on the map.",
        "fix": "Work those W25 seam stitches in continental before the W26 fill closes in from both sides, and place them precisely. They are the only thing separating two fruit from one oversized lemon at this mesh."
      }
    ]
  },
  "palette_moves": [
    {
      "action": "merge",
      "codes": [
        "W39",
        "W05"
      ],
      "to": "",
      "why": "W05 is 41 stitches over 18 regions of 1-5 stitches each - too fragmented to read as a colour, it just notches the lemon's edge. Folding it into the background keeps the lemon silhouette clean and cuts 18 unnecessary thread starts."
    },
    {
      "action": "keep",
      "codes": [
        "W25"
      ],
      "to": "",
      "why": "1.4 pct across 5 real regions (largest 37 stitches), not confetti - these sit on the lemons as rind highlights and mark the seam between the two touching lemons, so they earn their place."
    },
    {
      "action": "keep",
      "codes": [
        "W38"
      ],
      "to": "",
      "why": "8.6 pct across 38 small regions looks fragmented, but each one marks a tile-corner highlight repeating across the whole grid (about 24 stitches average) - that is the tile pattern reading correctly, not noise."
    }
  ],
  "stitches": [
    {
      "code": "W39",
      "area": "the blue tile background filling the whole field behind the lemons and leaves",
      "stitch": "basketweave",
      "why": "by far the largest area (58.6 pct, 36 flat regions) on a pillow that will be handled - basketweave keeps the canvas square and wears well."
    },
    {
      "code": "W26",
      "area": "the three lemons: the two touching ones across the lower two-thirds and the separate lemon upper right",
      "stitch": "basketweave",
      "why": "the main motif fill (22.2 pct, two large connected regions) needs basketweave so the fruit's curves do not distort the canvas."
    },
    {
      "code": "W38",
      "area": "small highlight flecks on the tile squares, scattered across the background",
      "stitch": "continental",
      "why": "38 regions averaging only about 24 stitches each are too small and irregular for a geometric decorative stitch to sit cleanly - continental keeps each patch crisp."
    },
    {
      "...": "one entry per palette colour"
    }
  ],
  "order": [
    {
      "step": 1,
      "do": "Place the fine lemon-surface details first in continental: the two W01 highlight dots and all five W25 flecks, including the seam fleck where the two touching lemons meet (around row 11, columns 13-14). These are the reference points the lemon fill will butt up against.",
      "codes": [
        "W01",
        "W25"
      ]
    },
    {
      "step": 2,
      "do": "Stitch the two W33 leaves in continental, tips first. Fern is the darkest yarn on this chart, so seat its edge stitches down into the shared holes now rather than letting the background come up through them later.",
      "codes": [
        "W33"
      ]
    },
    {
      "step": 3,
      "do": "If you are not applying the W05 merge, tack down its 18 tiny fragments in continental now, before the lemon fill crosses them, so they do not get buried or skipped.",
      "codes": [
        "W05"
      ]
    },
    {
      "step": 4,
      "do": "Basketweave the three lemons in W26 - the two touching ones first, working out from the seam you already marked, then the separate lemon upper right.",
      "codes": [
        "W26"
      ]
    },
    {
      "step": 5,
      "do": "Work the W01 grout grid and the W38 tile-highlight flecks in continental before the bulk background goes in - both are thin or scattered and easiest to place on bare canvas.",
      "codes": [
        "W01",
        "W38"
      ]
    },
    {
      "step": 6,
      "do": "Basketweave the W39 background last, filling around every detail already placed. Come up through the empty holes left between your marked stitches rather than through holes the dark Fern or Lemon edges already occupy.",
      "codes": [
        "W39"
      ]
    }
  ],
  "finishing": {
    "steps": [
      "Block the finished canvas square: damp it and pin to the 8 x 8 in finished size on a padded board until dry, checking the corners are square.",
      "Trim the 12 x 12 in canvas down to about 1 to 1.5 in beyond the stitched edge on all sides, leaving plain canvas for turning.",
      "Cut a backing fabric square to match the trimmed size. With right sides together, stitch around three sides through the canvas margin, leaving the fourth side open.",
      "Turn right side out, insert an 8 x 8 in pillow form, then slip-stitch the fourth side closed.",
      "Optional: finish the edge with cording or a flange from the backing fabric for a cleaner look, in line with an intermediate skill level."
    ],
    "notes": "A confident finisher can add a zipper for a removable cover; otherwise hand-stitching the last side closed is simplest for a first needlepoint pillow."
  },
  "time": {
    "hours_low": 22,
    "hours_high": 34,
    "basis": "Close to facts.hours_est. No decorative stitches are used, so there is no time saved there; merging W05 trims about 18 thread starts toward the low end, while stitching every scattered W38 and W25 fleck as charted, with many small colour changes, pushes it toward the high end."
  },
  "tips": [
    "Stitch the W33 leaf tips in continental before the background closes in around them - at 13 mesh a one-stitch point blurs if basketweave rounds it off first.",
    "Keep the W25 seam fleck between the two touching lemons (row 11, columns 13-14) visible - it is the only thing telling the eye there are two fruit, not one big lemon."
  ],
  "warnings": [
    "Carrying W39 or W01 behind the lemons over long distances will show as shadow lines through the light W26 or W01 stitches on the front - end and restart thread instead of carrying more than an inch.",
    "This pillow will see regular handling - avoid half cross even for the small W05 or W25 flecks; with only 41 to 149 stitches it is tempting to rush them, but half cross gives thin coverage for something that will get wear."
  ]
}

task: plan - a dithered sunset on a small ornament (rework)

Input (decoded for reading; on the wire palette and facts are JSON strings):

{
  "task": "plan",
  "project": "ornament",
  "skill": "confident",
  "mesh": 18,
  "source": "photo",
  "slogan": "",
  "subject": "Sunset over layered hills",
  "notes": "A Christmas ornament for my sister - she loves this photo.",
  "size": "72 x 60 stitches, 4 x 3.3 in at 18 mesh",
  "palette": [
    {
      "code": "W06",
      "name": "Graphite",
      "hex": "#4a4d54",
      "symbol": "A",
      "stitches": 1089,
      "pct": 25.2,
      "regions": 211,
      "largest_region": 432,
      "lone_stitches": 130,
      "yards_est": 3.5
    },
    {
      "code": "W47",
      "name": "Plum",
      "hex": "#5e2f6b",
      "symbol": "B",
      "stitches": 758,
      "pct": 17.5,
      "regions": 295,
      "largest_region": 252,
      "lone_stitches": 280,
      "yards_est": 2.5
    },
    {
      "...": "6 more colours"
    }
  ],
  "facts": {
    "stitches_w": 72,
    "stitches_h": 60,
    "total_stitches": 4320,
    "finished_in": "4 x 3.3",
    "cut_canvas_in": "8 x 8",
    "lone_stitches_total": 1691,
    "small_regions": 1741,
    "edge_color": "W06",
    "low_contrast_pairs": [],
    "yards_total_est": 14.5,
    "hours_est": {
      "low": 6.3,
      "high": 9.8
    },
    "tidied": 0,
    "dithered": true,
    "letter_stroke_stitches": 0
  },
  "map": "block = 3x3 stitches, symbols as in palette\nBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBB\nFBBBFBBBFBBBFBBBFBBBFBBB\nCCCCCCCCCCCCCCCCCCCCCCCC\n..."
}

Reply:

{
  "task": "plan",
  "title": "Sunset Over Layered Hills",
  "readability": {
    "verdict": "rework",
    "summary": "This chart does not read cleanly at 72x60 stitches on 18 mesh: 1,691 of 4,320 stitches (about 39%) are lone single stitches from an ordered dither, well past the ~15% point where a palette move can rescue it. Three of the eight colours - Mauve, Pine and Coral - barely form real shapes at all (largest contiguous regions of 2, 3 and 6 stitches). The biggest risk is the horizon glow band (Apricot/Coral, rows 8-11 on the map), the part that has to read as a sunset, which is also the most confettied part of the chart. The broad layout - purple sky fading to rose, a graphite hill silhouette below - would read fine; it is the dithering, not the composition, that needs fixing before this is stitched.",
    "issues": [
      {
        "issue": "Heavy, near-canvas-wide dithering: 39% of all stitches are isolated singles, and Mauve (largest region 2 stitches), Pine (3) and Coral (6) exist almost entirely as scattered confetti rather than shapes.",
        "where": "Row 4 (Plum/Mauve transition), rows 8-11 (Apricot/Coral horizon), and the fir-coloured flecks in rows 16 and 18 on the map.",
        "fix": "Ask the page to re-chart with dithering off, or at a larger stitch count or coarser mesh, so each gradient step is a clean band instead of single-stitch noise."
      },
      {
        "issue": "The horizon transition is the most important part of a sunset picture and also the worst-dithered: Apricot and Coral interleave almost pixel for pixel, with no patch bigger than 87 stitches and half of Coral's stitches isolated.",
        "where": "Rows 8-11 on the map, the band directly above the hill line.",
        "fix": "Merge Apricot and Coral into one colour for a clean glow band, or have the page re-chart this transition as a stepped gradient instead of a dither."
      },
      {
        "issue": "Row 13 on the map shows what looks like a solid 7-block run of Lemon, but Lemon's largest region anywhere in the chart is only 5 stitches - the block map is hiding the real (lack of) shape there.",
        "where": "Map row 13, left of centre, inside the hill.",
        "fix": "Do not trust that row as a solid band. Check the stitch-level chart before working it, and treat those Lemon stitches as scattered accents, not a filled stripe."
      }
    ]
  },
  "palette_moves": [
    {
      "action": "merge",
      "codes": [
        "W23",
        "W22"
      ],
      "to": "",
      "why": "Apricot and Coral dither pixel by pixel through the horizon band (Coral's largest region is just 6 stitches, half its stitches are lone) - merging removes hundreds of thread starts without changing the glow."
    },
    {
      "action": "merge",
      "codes": [
        "W47",
        "W48"
      ],
      "to": "",
      "why": "Mauve only appears as a thin dithered line against Plum in row 4 and otherwise scattered noise (largest region 2 stitches, 387 of 391 stitches lone) - it is a transition artifact, not its own shape."
    },
    {
      "action": "merge",
      "codes": [
        "W06",
        "W35"
      ],
      "to": "",
      "why": "Pine's largest region is 3 stitches and 92% of it is lone - it reads as hill-top confetti, not a distinct colour; merging into Graphite removes the noise without changing the hill's silhouette."
    },
    {
      "action": "keep",
      "codes": [
        "W17"
      ],
      "to": "",
      "why": "Rose is the cleanest colour in the chart (largest region 342 stitches, only 3% lone) - it is doing its job as the sunset glow band and should not be touched."
    },
    {
      "action": "keep",
      "codes": [
        "W26"
      ],
      "to": "",
      "why": "Lemon is sparse but reads as a deliberate highlight accent once the row-13 map illusion is discounted - keep it as its own colour rather than folding it into a neighbour."
    }
  ],
  "stitches": [
    {
      "code": "W06",
      "area": "the hill silhouette across the bottom of the chart, rows 12-20 on the map",
      "stitch": "basketweave",
      "why": "the largest contiguous fill in the chart (432-stitch region) and the edge colour - it needs a smooth, distortion-free fill as the dominant shape."
    },
    {
      "code": "W47",
      "area": "the top sky band, rows 1-4",
      "stitch": "basketweave",
      "why": "a large, nearly solid 12-row band is exactly what basketweave is for - even canvas wear, no skew."
    },
    {
      "code": "W17",
      "area": "the sunset glow band in the middle sky, rows 5-8",
      "stitch": "mosaic",
      "why": "the cleanest, most contiguous colour in the chart, so a mosaic texture can add a bit of glow without fighting dither noise the way a dithered colour would."
    },
    {
      "...": "one entry per palette colour"
    }
  ],
  "order": [
    {
      "step": 1,
      "do": "Work the Lemon highlight flecks and Pine specks first, scattered through rows 8-13 and 16-18 of the map, while the holes are still empty: continental for Lemon, french knots for Pine.",
      "codes": [
        "W26",
        "W35"
      ]
    },
    {
      "step": 2,
      "do": "Work the thin Mauve dither line in row 4 next, one stitch at a time (skip this step if you take the Plum/Mauve merge above).",
      "codes": [
        "W48"
      ]
    },
    {
      "step": 3,
      "do": "Fill the top sky in basketweave: Plum across rows 1-4.",
      "codes": [
        "W47"
      ]
    },
    {
      "step": 4,
      "do": "Fill the sunset glow band in mosaic (or basketweave if you skip the decorative texture): Rose across rows 5-8.",
      "codes": [
        "W17"
      ]
    },
    {
      "step": 5,
      "do": "Fill the horizon transition in continental, working across each row rather than down, so the Apricot/Coral dither pattern stays even: rows 8-11.",
      "codes": [
        "W23",
        "W22"
      ]
    },
    {
      "step": 6,
      "do": "Fill the hill last, in basketweave, rows 12-20. This is the edge colour (it touches the border most), so treat it as the background: come up in empty holes and go down into holes the lighter sky colours already used along the hill's top edge, so the dark fibre does not shadow through the light yarn.",
      "codes": [
        "W06"
      ]
    }
  ],
  "finishing": {
    "steps": [
      "Block the stitched piece: dampen it, pin face down to a padded board true to the 4 x 3.3 in finished size, and let it dry fully before unpinning.",
      "Trim the canvas to about 1/2 in beyond the stitching on all sides - the 8 x 8 in cut canvas leaves plenty of margin for this.",
      "Fold the canvas margin to the back and glue or hand-sew it flat, mitring the corners if you are shaping the ornament as anything other than a plain rectangle.",
      "Back the piece with a scrap of felt or coordinating fabric cut to the finished size, whipstitched or glued around the edge.",
      "Add a twisted cord or purchased trim around the outer edge to cover the join, with a hanging loop stitched in at the top."
    ],
    "notes": "At 4 x 3.3 in this is small enough to finish yourself in an evening - a professional finisher is not needed unless you want a stiffer, mat-board-backed ornament."
  },
  "time": {
    "hours_low": 8,
    "hours_high": 13,
    "basis": "The page estimates 6.3-9.8 hours from stitch count and mesh alone. This chart has 1,691 lone stitches out of 4,320 (about 39%) plus 8 colours in near-constant rotation through the sky and horizon, meaning far more thread starts/stops and tails to weave in than a clean chart this size - pushing real time up by roughly a quarter to a third. The one decorative band (Rose in mosaic) and the small french-knot area do not add much, since together they cover under a fifth of the canvas."
  },
  "tips": [
    "If you take the merge moves above (Apricot+Coral, Plum+Mauve, Graphite+Pine), ask the page to re-chart before you buy yarn - you will need far less Mauve, Pine and Coral than the current yards_est shows.",
    "With 8 colours and this much confetti, keep a labelled bobbin per code (W06, W47, etc.) rather than matching hex values by eye - you will be stopping and restarting constantly."
  ],
  "warnings": [
    "Stitched exactly as charted, the dithering will read as speckled confetti rather than a smooth sunset gradient, especially in the horizon band - worth fixing before you commit yarn and hours to a gift piece.",
    "On 18 mesh, dark Graphite carried on the back behind the lighter Rose/Apricot/Coral bands can shadow through the front - do not carry it more than about an inch; end and restart the thread instead."
  ]
}

Build the input yourself

Load the script in node (global.window = {}; require("./stitchkit.js")), then:

const K = StitchKit;
// img = {width, height, data: RGBA bytes} from any decoder
const size = K.gridSize(img.width, img.height, 8, 13);              // 8 in wide on 13 mesh
const q = K.quantize(K.downsample(img, size.w, size.h), size.w, size.h, { colors: 8, dither: 0, tidy: true });
const stats = K.stats(q.grid, 13, "intermediate");
const { input } = K.buildInput({ grid: q.grid, stats, project: "pillow", skill: "intermediate", source: "photo", subject: "my dog", tidied: q.tidied });
const wire = K.toWire(input);                                         // POST this to /estimate and /run
// later: const plan = K.normalize(K.parseReply(job.output.output));
// K.reconcile(plan, stats, "pillow")  -> notes;  K.applyMoves(q.grid, plan.palette_moves).grid -> the revised chart

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://petit-point.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"petit-point"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "plan", "project": "bag", "mesh": 13, "palette": "[...]", "facts": "{...}", "map": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses petit-point:<task>:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="petit-point:plan:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: petit-point:plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json