{"openapi":"3.1.0","info":{"title":"Insertly","description":"Branded, print-ready package inserts for your OpoShop orders — thank-you cards, care cards and reorder offers with scannable QR codes.","version":"1.0.0","contact":{"name":"Found","email":"brandon@tryfound.io"}},"servers":[{"url":"https://app.tryinsertly.com","description":"Primary"}],"tags":[{"name":"auth","description":"Authentication & session"},{"name":"onboarding","description":"First-run setup state & brand autofill"},{"name":"brand","description":"The store's brand: logo, colour, name, signature"},{"name":"templates","description":"Insert designs — size, theme, copy, QR targets, reorder offer"},{"name":"inserts","description":"Generate print-ready insert PDFs from orders"},{"name":"codes","description":"Reorder discount codes this app created in the store"},{"name":"analytics","description":"Dashboard metrics"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"App access token from /api/auth/login or the OAuth callback."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"OrderLineItem":{"type":"object","properties":{"name":{"type":"string"},"sku":{"type":"string"},"variant":{"type":"string"},"quantity":{"type":"integer"},"unitPrice":{"type":"number"}}},"OrderAddress":{"type":"object","properties":{"name":{"type":"string"},"line1":{"type":"string"},"line2":{"type":"string"},"city":{"type":"string"},"region":{"type":"string"},"postalCode":{"type":"string"},"country":{"type":"string"}}},"OrderDetail":{"type":"object","description":"A store order, read live from OpoShop, with the detail an insert needs.","properties":{"_id":{"type":"string","description":"The OpoShop order id."},"everbeeId":{"type":"string"},"orderNumber":{"type":"string"},"status":{"type":"string"},"customerName":{"type":"string"},"customerEmail":{"type":"string","format":"email"},"orderDate":{"type":"string","format":"date-time"},"currency":{"type":"string","example":"USD"},"itemsCount":{"type":"integer"},"totalPrice":{"type":"number"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/OrderLineItem"}},"shippingAddress":{"$ref":"#/components/schemas/OrderAddress"}}},"InsertSettings":{"type":"object","description":"The store brand every insert inherits unless a template overrides it.","properties":{"businessName":{"type":"string","maxLength":80},"logoUrl":{"type":"string","description":"Logo printed on every insert — an https:// image URL or an uploaded data: URL (PNG/JPEG, base64). Other formats cannot be embedded in a PDF."},"accentColor":{"type":"string","example":"#EA580C","description":"Hex colour used for accents on the card."},"storeUrl":{"type":"string","description":"The public storefront. Used as the fallback QR destination when a target has no URL of its own."},"signature":{"type":"string","maxLength":60,"description":"Sign-off line, e.g. \"— The Sunday Girls Club team\"."},"onboardedAt":{"type":"string","format":"date-time"}}},"QrTarget":{"type":"object","required":["kind","label"],"properties":{"kind":{"type":"string","enum":["review","reorder","signup","care","custom"]},"label":{"type":"string","maxLength":40,"description":"Printed above the QR, e.g. \"Leave a review\"."},"caption":{"type":"string","maxLength":70,"description":"One line printed under the QR."},"url":{"type":"string","maxLength":600,"description":"Absolute http(s) destination. May contain merge fields, e.g. `https://shop.com/?promo={{code}}`. Empty falls back to the store URL."}}},"ReorderOffer":{"type":"object","description":"The reorder incentive printed on the card. When `mintUnique` is true Insertly creates a REAL OpoShop discount per order (this is the only write the app makes to a store).","properties":{"enabled":{"type":"boolean","default":false},"percent":{"type":"integer","minimum":1,"maximum":90,"default":10},"expiryDays":{"type":"integer","minimum":1,"maximum":730,"default":60},"mintUnique":{"type":"boolean","default":false,"description":"Create a distinct discount code per order (requires the discounts:write scope). When false, `staticCode` is printed as-is and nothing is written to the store."},"staticCode":{"type":"string","maxLength":40,"description":"Used when mintUnique is false."}}},"InsertTemplate":{"type":"object","required":["name"],"properties":{"_id":{"type":"string","readOnly":true},"name":{"type":"string","maxLength":60},"kind":{"type":"string","enum":["thank-you","care","reorder","custom"],"default":"thank-you"},"size":{"type":"string","enum":["4x6","5x7","a6","a5","business-card"],"default":"4x6","description":"Trim size. The generated PDF page IS this size — send it straight to a print shop."},"layout":{"type":"string","enum":["card","sheet"],"default":"card","description":"'card' = one page per insert at the exact trim size. 'sheet' = cards tiled N-up on US Letter with crop marks, for printing at home."},"theme":{"type":"string","enum":["classic","minimal","bold"],"default":"classic"},"accentColor":{"type":"string","example":"#EA580C","description":"Overrides the store brand colour."},"logoUrl":{"type":"string","description":"Overrides the store logo."},"eyebrow":{"type":"string","maxLength":40},"headline":{"type":"string","maxLength":90},"message":{"type":"string","maxLength":600},"signature":{"type":"string","maxLength":60},"footnote":{"type":"string","maxLength":120},"careInstructions":{"type":"array","maxItems":6,"items":{"type":"string","maxLength":120},"description":"Bullet lines for a care/usage card."},"showOrderMeta":{"type":"boolean","default":true,"description":"Print \"Order 1043 · Jane D.\" in small type so the packer can match card to box."},"qrTargets":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/QrTarget"}},"reorder":{"$ref":"#/components/schemas/ReorderOffer"},"isDefault":{"type":"boolean"},"archived":{"type":"boolean"}}},"InsertCode":{"type":"object","description":"A reorder discount code Insertly created in the store for one order.","properties":{"_id":{"type":"string"},"templateId":{"type":"string"},"orderId":{"type":"string"},"orderNumber":{"type":"string"},"code":{"type":"string"},"percent":{"type":"integer"},"expiresAt":{"type":"string","format":"date-time"},"discountId":{"type":"string","description":"The OpoShop discount id, once read back."},"status":{"type":"string","enum":["pending","active","failed"],"description":"'active' means the discount was created AND read back from OpoShop. Only 'active' codes are ever printed."},"failureReason":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"DashboardStats":{"type":"object","properties":{"templates":{"type":"integer"},"codesCreated":{"type":"integer"},"codesThisMonth":{"type":"integer"},"codesLive":{"type":"integer"},"codesFailed":{"type":"integer"},"onboarded":{"type":"boolean"},"recentCodes":{"type":"array","items":{"$ref":"#/components/schemas/InsertCode"}}}}}},"paths":{"/health":{"get":{"tags":["analytics"],"operationId":"healthCheck","summary":"Liveness probe","security":[],"responses":{"200":{"description":"OK"}}}},"/api/auth/login":{"post":{"tags":["auth"],"operationId":"login","summary":"Email/password login, returns access + refresh tokens","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"Tokens + user"},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/auth/me":{"get":{"tags":["auth"],"operationId":"getCurrentUser","summary":"Current authenticated user + store","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"User"},"401":{"description":"Unauthorized"}}}},"/api/auth/uninstall":{"post":{"tags":["auth"],"operationId":"uninstallApp","summary":"Uninstall webhook — marks the store uninstalled and clears its setup flag","security":[],"responses":{"200":{"description":"Acknowledged"}}}},"/api/onboarding":{"get":{"tags":["onboarding"],"operationId":"getOnboarding","summary":"First-run setup status; when pending, returns brand autofill suggestions from the store theme","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Setup status and (when pending) suggested brand values","content":{"application/json":{"schema":{"type":"object","required":["onboarded"],"properties":{"onboarded":{"type":"boolean"},"suggestions":{"$ref":"#/components/schemas/InsertSettings"}}}}}},"401":{"description":"Unauthorized"}}}},"/api/onboarding/complete":{"post":{"tags":["onboarding"],"operationId":"completeOnboarding","summary":"Save the brand, seed the three starter templates, and mark setup complete","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertSettings"}}}},"responses":{"200":{"description":"Saved brand plus how many starter templates were seeded","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/InsertSettings"},{"type":"object","properties":{"seededTemplates":{"type":"integer"}}}]}}}},"400":{"description":"Invalid brand values","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized"}}}},"/api/insert-settings":{"get":{"tags":["brand"],"operationId":"getBrand","summary":"Get the store's insert brand (creates defaults from the store on first read)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Brand settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertSettings"}}}},"401":{"description":"Unauthorized"}}},"put":{"tags":["brand"],"operationId":"updateBrand","summary":"Update the store's insert brand (upsert)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertSettings"}}}},"responses":{"200":{"description":"Updated brand settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertSettings"}}}},"400":{"description":"Invalid value","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized"}}}},"/api/insert-settings/reset":{"post":{"tags":["brand"],"operationId":"resetSetup","summary":"Clear the first-run flag so the setup wizard runs again","description":"Non-destructive escape hatch offered by the error boundary. Templates and the reorder-code ledger are never touched.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Reset"},"401":{"description":"Unauthorized"}}}},"/api/insert-templates/options":{"get":{"tags":["templates"],"operationId":"getTemplateOptions","summary":"The design vocabulary: print sizes (with exact point dimensions), themes, layouts, QR kinds, merge fields","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Options"},"401":{"description":"Unauthorized"}}}},"/api/insert-templates":{"get":{"tags":["templates"],"operationId":"listTemplates","summary":"List this store's insert templates","security":[{"bearerAuth":[]}],"parameters":[{"name":"archived","in":"query","schema":{"type":"boolean","default":false},"description":"Return archived templates instead of active ones."}],"responses":{"200":{"description":"Templates","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InsertTemplate"}}}}}}},"401":{"description":"Unauthorized"}}},"post":{"tags":["templates"],"operationId":"createTemplate","summary":"Create an insert template","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertTemplate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertTemplate"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized"}}}},"/api/insert-templates/{id}":{"get":{"tags":["templates"],"operationId":"getTemplate","summary":"Get one insert template","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The template's id."}],"responses":{"200":{"description":"Template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertTemplate"}}}},"404":{"description":"Not found"}}},"put":{"tags":["templates"],"operationId":"updateTemplate","summary":"Update an insert template","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The template's id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertTemplate"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertTemplate"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["templates"],"operationId":"archiveTemplate","summary":"Archive a template (soft delete — already-printed cards stay re-renderable)","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The template's id."}],"responses":{"200":{"description":"Archived"},"404":{"description":"Not found"}}}},"/api/insert-templates/{id}/default":{"post":{"tags":["templates"],"operationId":"setDefaultTemplate","summary":"Make this the template used when a generate call doesn't name one","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The template's id."}],"responses":{"200":{"description":"Template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertTemplate"}}}},"404":{"description":"Not found"}}}},"/api/insert-templates/{id}/duplicate":{"post":{"tags":["templates"],"operationId":"duplicateTemplate","summary":"Duplicate a template","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The template's id."}],"responses":{"201":{"description":"The copy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertTemplate"}}}},"404":{"description":"Not found"}}}},"/api/insert-templates/{id}/restore":{"post":{"tags":["templates"],"operationId":"restoreTemplate","summary":"Restore an archived template","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The template's id."}],"responses":{"200":{"description":"Template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsertTemplate"}}}},"404":{"description":"Not found"}}}},"/api/inserts/orders":{"get":{"tags":["inserts"],"operationId":"listOrders","summary":"List this store's orders, live from OpoShop (newest first)","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1},"description":"Page number (1-indexed)."},{"name":"per_page","in":"query","schema":{"type":"integer","default":25},"description":"Items per page."}],"responses":{"200":{"description":"Orders","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrderDetail"}},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}}}}}},"401":{"description":"Unauthorized"}}}},"/api/inserts/orders/{id}":{"get":{"tags":["inserts"],"operationId":"getOrder","summary":"One order's full detail","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderDetail"}}}},"404":{"description":"Not found"}}}},"/api/inserts/orders/{id}/insert.pdf":{"get":{"tags":["inserts"],"operationId":"generateInsertForOrder","summary":"Print-ready insert PDF for one order","description":"Renders the card at its exact trim size. If the template has a reorder offer with `mintUnique`, a real single-use OpoShop discount is created for this order (idempotent — reprinting reuses the same code).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"templateId","in":"query","schema":{"type":"string"},"description":"Template to render. Defaults to the store's default template."}],"responses":{"200":{"description":"The insert PDF. Non-fatal issues (a QR target with no URL, a reorder code that could not be created) are returned in the `X-Insertly-Warnings` response header as a URL-encoded JSON array of strings.","headers":{"X-Insertly-Warnings":{"description":"URL-encoded JSON array of human-readable warnings. Absent when there are none.","schema":{"type":"string"}}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Order or template not found"}}}},"/api/inserts/preview":{"post":{"tags":["inserts"],"operationId":"previewInsert","summary":"Render an unsaved design as a PDF, using the store's most recent real order","description":"Never mints a discount — the preview shows an example code. When the store has no orders yet, example order details are used and that is stated in the warnings header.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"template":{"$ref":"#/components/schemas/InsertTemplate"}}}}}},"responses":{"200":{"description":"The preview PDF. Non-fatal issues (a QR target with no URL, a reorder code that could not be created) are returned in the `X-Insertly-Warnings` response header as a URL-encoded JSON array of strings.","headers":{"X-Insertly-Warnings":{"description":"URL-encoded JSON array of human-readable warnings. Absent when there are none.","schema":{"type":"string"}}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/inserts/generate":{"post":{"tags":["inserts"],"operationId":"generateInserts","summary":"Generate inserts for a chosen list of orders (one combined PDF)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["orderIds"],"properties":{"orderIds":{"type":"array","maxItems":200,"items":{"type":"string"}},"templateId":{"type":"string"}}}}}},"responses":{"200":{"description":"The combined insert PDF. Non-fatal issues (a QR target with no URL, a reorder code that could not be created) are returned in the `X-Insertly-Warnings` response header as a URL-encoded JSON array of strings.","headers":{"X-Insertly-Warnings":{"description":"URL-encoded JSON array of human-readable warnings. Absent when there are none.","schema":{"type":"string"}}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No matching orders, or no template"}}}},"/api/inserts/bulk/preview":{"get":{"tags":["inserts"],"operationId":"previewBulkRange","summary":"How many orders a date range covers — check before generating","description":"Shown before a bulk run so a batch that would create 180 real discount codes is never a surprise. `truncated` is true when the sweep hit its page or size cap.","security":[{"bearerAuth":[]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string"},"description":"ISO date or YYYY-MM-DD (inclusive)."},{"name":"to","in":"query","schema":{"type":"string"},"description":"ISO date or YYYY-MM-DD (inclusive)."}],"responses":{"200":{"description":"Match count and a sample of the orders","content":{"application/json":{"schema":{"type":"object","properties":{"matched":{"type":"integer"},"truncated":{"type":"boolean"},"scanned":{"type":"integer"},"maxPerRun":{"type":"integer"},"orders":{"type":"array","items":{"$ref":"#/components/schemas/OrderDetail"}}}}}}},"400":{"description":"Bad date range"}}}},"/api/inserts/bulk":{"post":{"tags":["inserts"],"operationId":"generateInsertsForRange","summary":"Generate inserts for every order in a date range (one combined PDF)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","description":"ISO date or YYYY-MM-DD (inclusive)."},"to":{"type":"string","description":"ISO date or YYYY-MM-DD (inclusive)."},"templateId":{"type":"string"}}}}}},"responses":{"200":{"description":"The combined insert PDF. Non-fatal issues (a QR target with no URL, a reorder code that could not be created) are returned in the `X-Insertly-Warnings` response header as a URL-encoded JSON array of strings.","headers":{"X-Insertly-Warnings":{"description":"URL-encoded JSON array of human-readable warnings. Absent when there are none.","schema":{"type":"string"}}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Bad date range"},"404":{"description":"No orders in range, or no template"}}}},"/api/inserts/codes":{"get":{"tags":["codes"],"operationId":"listReorderCodes","summary":"Every reorder discount code this app created in the store","description":"An app that mints discounts owes the merchant a ledger of exactly what it gave away.","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1},"description":"Page number (1-indexed)."},{"name":"per_page","in":"query","schema":{"type":"integer","default":25},"description":"Items per page."},{"name":"status","in":"query","schema":{"type":"string","enum":["pending","active","failed"]}}],"responses":{"200":{"description":"Codes","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InsertCode"}},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}}}}}},"401":{"description":"Unauthorized"}}}},"/api/analytics/dashboard":{"get":{"tags":["analytics"],"operationId":"getDashboardStats","summary":"Counts of templates and reorder codes for this store","description":"Every number is counted from real rows this app wrote. No estimates, no projections.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Stats","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardStats"}}}},"401":{"description":"Unauthorized"}}}},"/api/release-notes":{"get":{"tags":["analytics"],"operationId":"getReleaseNotes","summary":"Release notes generated from git history","security":[],"responses":{"200":{"description":"Release notes"}}}}}}