End-to-end example
Upload a file, wait for a ready model, then quote, order and pay in one script.
Before you start
You need a business API key with every scope below and administrator spending authorization, a saved card or a customer who can finish hosted Checkout, a GLB, OBJ or ZIP model and a US shipping address saved as JSON.
Server environment
bash
export API_BASE_URL="https://api.ottomfg.com"
export OTTOMFG_API_KEY="YOUR_API_KEY"
export MODEL_FILE="figurine.zip"
export SHIPPING_ADDRESS_FILE="address.json"
# Optional: pay with a saved card instead of hosted Checkout.
export PAYMENT_METHOD_ID="pm_example"Run the whole flow
The script registers and uploads the file, creates a model, waits until its status is ready, quotes the cheapest shipping option, creates the order and pays it. It saves each idempotency key before the request that uses it, so you can rerun it after a failure without creating duplicates.
File to paid order
#!/usr/bin/env bash
# Bash + curl 7.76+ + jq + uuidgen. Upload a file, wait for a ready model, quote, order and pay.
# Key scopes: files:write, models:write, models:read, quotes:write, quotes:read,
# orders:write, orders:read and payments:write with administrator spending authorization.
# Set PAYMENT_METHOD_ID to pay with a saved card; otherwise hosted Checkout is used.
set -euo pipefail
: "${API_BASE_URL:?Set API_BASE_URL}" "${OTTOMFG_API_KEY:?Set OTTOMFG_API_KEY}"
: "${MODEL_FILE:?Set MODEL_FILE}" "${SHIPPING_ADDRESS_FILE:?Set SHIPPING_ADDRESS_FILE}"
base="${API_BASE_URL%/}/api/v1"
# Keys and IDs are saved before each request, so a rerun resumes without duplicates.
state=end-to-end.env
touch "$state" && source "$state"
remember() { printf '%s=%q\n' "$1" "$2" >> "$state"; printf -v "$1" '%s' "$2"; }
call() { # method path [step] [json-body]
local args=(--fail-with-body --silent --show-error --max-time 60 -X "$1" "$base$2"
-H "Authorization: Bearer $OTTOMFG_API_KEY")
if [ -n "${3:-}" ]; then
[ -n "${!3:-}" ] || remember "$3" "$(uuidgen)"
args+=(-H "Idempotency-Key: ${!3}")
fi
[ -n "${4:-}" ] && args+=(-H "Content-Type: application/json" --data "$4")
curl "${args[@]}"
}
wait_for() { # path jq-condition; about 15 minutes at most
local delay=2
for _ in $(seq 60); do
value=$(call GET "$1")
if jq -e "$2" >/dev/null <<<"$value"; then printf '%s' "$value"; return; fi
sleep "$delay"
delay=$(( delay < 10 ? delay * 3 / 2 + 1 : 15 ))
done
echo "Still waiting for $1; rerun to resume." >&2; exit 1
}
# 1. Register the file, then send its bytes straight to storage.
if [ -z "${file_id:-}" ]; then
name=$(basename "$MODEL_FILE")
file=$(call POST /files file_key "$(jq -n --arg n "$name" --argjson s "$(wc -c <"$MODEL_FILE")" '{filename:$n, size_bytes:$s}')")
form=()
while IFS=$'\t' read -r field value; do form+=(-F "$field=$value"); done \
< <(jq -r '.upload.fields | to_entries[] | [.key, .value] | @tsv' <<<"$file")
curl --fail-with-body --silent --show-error -X "$(jq -r .upload.method <<<"$file")" \
"${form[@]}" -F "file=@$MODEL_FILE" "$(jq -r .upload.url <<<"$file")"
remember file_id "$(jq -r .id <<<"$file")"
fi
# 2. Create a model and wait until it can be priced.
model=$(call POST /models model_key "$(jq -n --arg f "$file_id" '{source:{file_id:$f}, longest_dimension_mm:100}')")
model=$(wait_for "/models/$(jq -r .id <<<"$model")" '.status != "processing"')
[ "$(jq -r .status <<<"$model")" = ready ] || { jq '{status, status_reasons}' <<<"$model"; exit 1; }
# 3. Quote manufacturing and shipping. A quote holds its terms for 30 minutes.
quote=$(call POST /quotes quote_key "$(jq -n --arg m "$(jq -r .id <<<"$model")" \
--slurpfile a "$SHIPPING_ADDRESS_FILE" \
'{items:[{model_id:$m, longest_dimension_mm:100, quantity:1}], shipping_address:$a[0]}')")
option=$(jq -c '.shipping_options | min_by(.totals.total)' <<<"$quote")
echo "Total $(jq .totals.total <<<"$option") cents; repair: $(jq -r '.items[0].repair.status' <<<"$quote")"
# 4. Create the order. It waits for payment until expires_at.
order=$(call POST /orders order_key "$(jq -n --arg q "$(jq -r .id <<<"$quote")" \
--arg o "$(jq -r .id <<<"$option")" '{quote_id:$q, shipping_option_id:$o}')")
order_id=$(jq -r .id <<<"$order")
# 5. Pay the same order. HTTP 200 does not mean paid: read payment.status.
if [ -n "${PAYMENT_METHOD_ID:-}" ]; then
body=$(jq -n --arg p "$PAYMENT_METHOD_ID" '{type:"saved_method", payment_method_id:$p}')
else
body='{"type":"checkout"}'
fi
result=$(call POST "/orders/$order_id/payments" payment_key "$body")
jq -r 'if .next_action then "Send the customer to: " + .next_action.url else empty end' <<<"$result"
# 6. The order is confirmed once the server verifies payment.
order=$(wait_for "/orders/$order_id" \
'.status != "awaiting_payment" or (.payment.status | IN("failed", "cancelled"))')
jq -r '"Order \(.id) \(.status) payment \(.payment.status)"' <<<"$order"
A model that ends in action_required or failed stops the script; read status_reasons. A quote holds its terms for 30 minutes; after that, delete the saved quote and order keys and run again. HTTP 200 from payment does not mean paid: the script waits until the order is confirmed or the payment fails.
What each step does
| Step | Endpoint | Guide |
|---|---|---|
| 1. Upload | Create a file, then POST the bytes to the returned URL | Upload your first file |
| 2. Model | Create a model; get it until status is not processing | Create and poll a model |
| 3. Quote | Create a quote with the model, size, quantity and address | Create a shipping quote |
| 4. Order | Create an order from the quote and a shipping option | Place and pay for an order |
| 5. Pay | Pay an order with a saved card or hosted Checkout | Place and pay for an order |
| 6. Confirm | Get the order until it is confirmed, or subscribe to order.confirmed | Webhooks |
API v1 preview