ArticleAug 5, 2026

The One Prompt You Need to Add the Slant 3D Printing API to Your App

Anyone on the planet can access the Slant 3D API and get 1000 3D Printers added to their app to print and ship parts.

Now apps that generate 3D Models can sell the actual parts to the other 8 Billion People without 3D Printers.

And it is super easy. At most the Slant 3D API only requires 3 endpoint calls to make an order. Estimate, Draft, and Order.

But if you are vibe-coding here are some resources that will let you one-shot it most of the time.

Here is the link to the API docs: https://slant3dapi.com/documentation/introduction And here is the OpenAPI Spec: https://slant3dapi.com/v2/api/openapi.json

For a simple starting point you can use this prompt We are going to implement the Slant 3D API. So when a user creates a brick it automatically estimates what is costs to print that item. Below is the documentation links Create an estimate order endpoint when the brick is madea Then when the user clicks on a new "Order" Button in the lower right then it takes in thier address and called the Draft Order endpoint If the orders checks out then it will create a stripe session and pay for the brick and then submit the order to the Slant 3D API https://slant3dapi.com/documentation/introduction https://slant3dapi.com/v2/api/openapi.json

Here is a demonstration of the API in Action that used these prompts: brickmaker.slant3d.com BrickMaker lets you generate a custom lego and then order a print of it shipped to you via the Slant 3D API.

If you want full markdown here is a full prompt that you can copy that was used to give an AI full context with examples. You can also download this markdown file (attached to the post) to upload to your AI in every project as an example..

Human-readable layout

What the feature does: a user draws a brick → the app builds an STL in the browser → the STL is uploaded to Slant 3D and priced → the user enters an address → Slant 3D returns a draft order with print + shipping cost → Stripe collects payment → a Stripe webhook tells Slant 3D to actually print and ship it.

Order of operations

```text Browser Server (TanStack server fns) Slant 3D Stripe -------- ----------------------------- -------- ------ draw brick build STL (client-side) │ base64 └─ estimateBrick ─────────► POST /files/direct-upload ─────────► presigned URL PUT bytes to presigned URL ────────► S3 POST /files/confirm-upload ────────► publicFileServiceId POST /files/{id}/estimate ─────────► print cost ◄── { fileId, printCost } show "Print $X" + Order btn

enter address └─ draftBrickOrder ───────► POST /orders (DRAFT) ──────────────► printingCost + deliveryCost INSERT brick_orders (status=pending) ◄── totals click Pay └─ createBrickCheckout ───► stripe.checkout.sessions.create ─────────────────────────► session ◄── clientSecret embedded checkout ──────────────────────────────────────────────────────────────────── pays POST /api/public/payments/webhook ◄──── checkout.session.completed verify sig → status=paid POST /orders/{id} (process) ───────► order submitted status=submitted /order/complete polls getBrickOrderStatus until "submitted" ```

Key design decisions - Two separate Slant calls: a cheap estimate (fires automatically ~1.2 s after drawing stops) and a draft order (needs an address, returns real shipping). - Slant is never called from the browser; the API key lives only in `SLANT3D_API_KEY` on the server. - The database row (`brick_orders`) is the single source of truth linking Stripe session ↔ Slant order, so fulfillment is idempotent and recoverable. - Slant order submission happens only in the webhook, after payment is verified — never optimistically from the client. - A filament id is mandatory on both estimate and order; without it Slant returns `400 Error in price determination`.

---

Prompt for another AI

````markdown # Task: Implement Slant 3D on-demand 3D printing (quote → pay → fulfil) in a TanStack Start app

You are integrating the Slant 3D V2 API (`https://slant3dapi.com/v2/api`) so a user can generate an STL in the browser, get a live print quote, enter a shipping address, pay with Stripe, and have the order automatically submitted for printing after payment clears.

## Non-negotiable rules 1. All Slant 3D calls run server-side. The API key (`SLANT3D_API_KEY`) is read with `process.env["SLANT3D_API_KEY"]` inside the handler, never at module scope (Cloudflare Workers inject env per request). 2. Every Slant item requires an explicit `filamentId`. Omitting it yields `400 {"error":{"message":"Error in price determination"}}`. 3. Slant orders are created as *drafts* by `POST /orders`; they are only committed by a second `POST /orders/{publicId}`. Call the second one only from the verified Stripe webhook, never from the client. 4. Persist an order row before payment. It is the join key between Stripe and Slant, and it makes webhook fulfilment idempotent. 5. Never expose the Slant key, file ids, or order ids as trusted client input for privileged actions; validate every server-fn input with Zod.

## Slant 3D API surface used

| Call | Purpose | Notes | |---|---|---| | `GET /platforms` | get `platformId` for the account | cache in a module-level variable | | `GET /filaments` | pick a material | filter `available !== false`, prefer `profile==="PLA" && color==="black"` | | `POST /files/direct-upload` | `{ name, platformId }` → `{ presignedUrl, filePlaceholder }` | | | `PUT <presignedUrl>` | raw STL bytes, `Content-Type: application/octet-stream` | not authenticated with the API key | | `POST /files/confirm-upload` | `{ filePlaceholder }` → `{ publicFileServiceId }` | triggers Slant's mesh analysis | | `POST /files/{fileId}/estimate` | `{ options: { filamentId } }` → `{ total }` | print cost only, no shipping | | `POST /orders` | draft order → `{ order: { publicId, printingCost, deliveryCost } }` | needs address for shipping | | `POST /orders/{publicId}` | commits the draft to production | call after payment |

All authenticated calls use `Authorization: Bearer <SLANT3D_API_KEY>` and `Content-Type: application/json`. Responses are wrapped in `{ success, data }`.

## Step 1 — server-only client (`src/lib/slant3d.server.ts`)

```ts const BASE = "https://slant3dapi.com/v2/api";

function apiKey(): string { const key = process.env["SLANT3D_API_KEY"]; if (!key) throw new Error("SLANT3D_API_KEY is not configured"); return key; }

async function slantFetch<T>(path: string, init?: RequestInit): Promise<T> { const response = await fetch(`${BASE}${path}`, { ...init, headers: { Authorization: `Bearer ${apiKey()}`, "Content-Type": "application/json", ...(init?.headers ?? {}), }, }); const text = await response.text(); if (!response.ok) { throw new Error(`Slant 3D request failed [${response.status}] ${path}: ${text}`); } return (text ? JSON.parse(text) : {}) as T; } ```

Cached lookups:

```ts let cachedPlatformId: string | undefined; export async function getPlatformId(): Promise<string> { if (cachedPlatformId) return cachedPlatformId; const r = await slantFetch<{ data?: Array<{ id?: string; platformId?: string }> }>("/platforms"); const id = r.data?.[0]?.id ?? r.data?.[0]?.platformId; if (!id) throw new Error("No Slant 3D platform found on this account"); return (cachedPlatformId = id); }

let cachedFilamentId: string | undefined; export async function getDefaultFilamentId(): Promise<string> { if (cachedFilamentId) return cachedFilamentId; const r = await slantFetch<{ data?: Filament[] }>("/filaments"); const f = (r.data ?? []).filter((x) => x.publicId && x.available !== false); const chosen = f.find((x) => x.profile === "PLA" && x.color === "black") ?? f.find((x) => x.profile === "PLA") ?? f[0]; if (!chosen?.publicId) throw new Error("No Slant 3D filament is available"); return (cachedFilamentId = chosen.publicId); } ```

Three-step upload (presign → PUT → confirm):

```ts export async function uploadStl(bytes: Uint8Array, name: string): Promise<string> { const platformId = await getPlatformId();

const presigned = await slantFetch<{ data?: { presignedUrl?: string; filePlaceholder?: Record<string, unknown> }; }>("/files/direct-upload", { method: "POST", body: JSON.stringify({ name, platformId }) });

const { presignedUrl, filePlaceholder } = presigned.data ?? {}; if (!presignedUrl || !filePlaceholder) throw new Error("Slant 3D did not return an upload URL");

const upload = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": "application/octet-stream" }, body: bytes as unknown as BodyInit, }); if (!upload.ok) throw new Error(`STL upload failed [${upload.status}]: ${await upload.text()}`);

const confirmed = await slantFetch<{ data?: { publicFileServiceId?: string } }>( "/files/confirm-upload", { method: "POST", body: JSON.stringify({ filePlaceholder }) }, ); const fileId = confirmed.data?.publicFileServiceId; if (!fileId) throw new Error("Slant 3D did not return a file id"); return fileId; } ```

Estimate and draft order — both must carry `filamentId`:

```ts export async function estimatePrintCost(fileId: string): Promise<number> { const filamentId = await getDefaultFilamentId(); const r = await slantFetch<{ data?: { total?: number } }>(`/files/${fileId}/estimate`, { method: "POST", body: JSON.stringify({ options: { filamentId } }), }); if (typeof r.data?.total !== "number") throw new Error("Slant 3D did not return an estimate"); return r.data.total; }

export async function createDraftOrder(input: { fileId: string; email: string; address: SlantAddress; }): Promise<{ publicId: string; printingCost: number; deliveryCost: number }> { const platformId = await getPlatformId(); const filamentId = await getDefaultFilamentId(); const r = await slantFetch<{ data?: { order?: Record<string, any> } }>("/orders", { method: "POST", body: JSON.stringify({ platformId, customer: { details: { email: input.email, address: input.address } }, items: [{ type: "PRINT", quantity: 1, publicFileServiceId: input.fileId, filamentId }], }), }); const order = r.data?.order; if (!order?.publicId) throw new Error("Slant 3D did not return a draft order"); return { publicId: order.publicId, printingCost: Number(order.printingCost ?? 0), deliveryCost: Number(order.deliveryCost ?? 0), }; }

export async function processOrder(publicOrderId: string): Promise<void> { await slantFetch(`/orders/${publicOrderId}`, { method: "POST" }); } ```

`SlantAddress` = `{ name, line1, line2?, city, state, zip, country }` where `country` is a 2-letter ISO code.

## Step 2 — database table

```sql CREATE TABLE public.brick_orders ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), slant_order_id text NOT NULL, slant_file_id text NOT NULL, email text NOT NULL, address jsonb NOT NULL, printing_cost numeric NOT NULL DEFAULT 0, delivery_cost numeric NOT NULL DEFAULT 0, amount_cents integer NOT NULL, currency text NOT NULL DEFAULT 'usd', status text NOT NULL DEFAULT 'pending', -- pending → paid → submitted | submit_failed | payment_failed stripe_session_id text, environment text NOT NULL DEFAULT 'sandbox', -- sandbox | live error text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() );

GRANT ALL ON public.brick_orders TO service_role; -- written only by server code ALTER TABLE public.brick_orders ENABLE ROW LEVEL SECURITY; -- No anon/authenticated policies: the table is service-role only (contains PII). ```

## Step 3 — server functions (`src/lib/brick.functions.ts`)

Use `createServerFn` (typed RPC), one per step. Import the `.server` helpers *inside* the handler with `await import(...)` so they never enter the client bundle. Return `{ ...ok } | { error: string }` instead of throwing, so the UI can render failures.

```ts export const estimateBrick = createServerFn({ method: "POST" }) .inputValidator((d: { stl: string }) => z.object({ stl: z.string().min(1).max(12_000_000) }).parse(d)) .handler(async ({ data }) => { try { const { uploadStl, estimatePrintCost } = await import("@/lib/slant3d.server"); const bytes = Uint8Array.from(Buffer.from(data.stl, "base64")); const fileId = await uploadStl(bytes, `brick-${Date.now()}.stl`); return { fileId, printCost: await estimatePrintCost(fileId) }; } catch (e) { return { error: e instanceof Error ? e.message : "Estimate failed" }; } }); ```

`draftBrickOrder` validates `{ fileId, email, address, environment }` with Zod, calls `createDraftOrder`, computes `totalCents = round((printingCost + deliveryCost) * 100)`, rejects totals under Stripe's 50-cent minimum, inserts the `brick_orders` row with `status: 'pending'`, and returns the row id plus the cost breakdown.

`createBrickCheckout` loads the order by id and environment, refuses if `status !== 'pending'`, creates an embedded Stripe Checkout session with `metadata: { brickOrderId }` and `unit_amount: order.amount_cents`, saves `stripe_session_id`, and returns `client_secret`. Prices come from the DB row, never from the client.

`getBrickOrderStatus` returns `{ status, slantOrderId }` for the confirmation page poll.

## Step 4 — client flow

- Debounce ~1.2 s after the drawing stops, serialise the mesh to a binary STL, base64 it in 8 KB chunks (`String.fromCharCode(...subarray)` to avoid stack overflow), call `estimateBrick`, and guard against races with a monotonically increasing request id — ignore any response whose id is stale. - Clear `fileId`/`printCost` whenever the design changes, and disable "Order" until a `fileId` exists. - The order dialog is a three-state machine: address form → (`draftBrickOrder`) → cost breakdown + Pay → (`createBrickCheckout`) → `<EmbeddedCheckout>` with `return_url = ${origin}/order/complete?order=${orderId}`.

## Step 5 — webhook fulfilment (`src/routes/api/public/payments/webhook.ts`)

`/api/public/*` bypasses site auth, so the handler must verify the caller itself.

1. Verify the Stripe signature: parse `t=` and `v1=` from `stripe-signature`, reject if the timestamp is older than 300 s, HMAC-SHA256 `${timestamp}.${rawBody}` with the webhook secret via `crypto.subtle`, and compare hex against the `v1` values. Read the body with `await request.text()` before parsing JSON. 2. On `checkout.session.completed` (with `payment_status !== "unpaid"`) or `checkout.session.async_payment_succeeded`: - look up the order by `metadata.brickOrderId`, falling back to `stripe_session_id`; - return early if `status === 'submitted'` (idempotency guard against Stripe retries); - set `status = 'paid'`; - `await processOrder(order.slant_order_id)`; - set `status = 'submitted'`, or `'submit_failed'` plus the error message on failure. 3. On `async_payment_failed`, set `status = 'payment_failed'`. 4. Always return `200` for handled events; `400` only for signature/verification failures.

## Failure modes to handle explicitly - `400 Error in price determination` → missing/invalid `filamentId`, or Slant hasn't finished analysing the file (confirm-upload must have succeeded first). - Slant `POST /orders` requires a deliverable address; surface its message verbatim in the UI. - Payment succeeds but Slant submission fails → order is left in `submit_failed` with the error text so it can be retried manually; never silently swallow it. - Stripe test mode declines all real cards; use `4242 4242 4242 4242`. ````

Do you want me to also save this as a markdown file in the repo (e.g. `docs/slant3d-integration.md`) so it lives alongside the code?

Downloads

apimodel generation