Complete reference for the Fignole Metalworks backend APIs powering Ring & Weave, Grain & Tenon, Hide & Stitch, Iron & Forge, and Budget Tracker.
Introduction
All four platforms share the same API surface
Ring & Weave, Grain & Tenon, Hide & Stitch, and Iron & Forge are built on an identical Lambda API — Cognito for auth, DynamoDB for storage. The only differences are the base URL and a handful of platform-exclusive endpoints (noted inline below: Ring & Weave has the Admin Portal and affiliate system; Grain & Tenon has the Community pattern-sharing endpoint). Use the platform selector in the sidebar to switch between base URLs in the examples below.
Request format
All POST request bodies are JSON. Include Content-Type: application/json.
GET requests pass parameters as query string values.
All responses are JSON with appropriate HTTP status codes, except /api/pattern-og which returns HTML.
Successful responses always include at minimum one of: ok, success, url, status, or the relevant data field.
Error responses always have the shape { "error": "human-readable message" }.
Base URLs
All endpoints are served from AWS API Gateway (HTTP API) backed by a single Lambda function per platform.
Every platform (Ring & Weave, Grain & Tenon, Hide & Stitch, Iron & Forge) uses AWS Cognito for auth on its shared endpoints. Each platform has its own Cognito User Pool, but the request shape is identical everywhere. Protected endpoints accept the Cognito JWT in one of two ways:
POST requests: include accessToken as a field in the JSON request body (or as an Authorization: Bearer header — handlers check the header first, falling back to the body field).
GET requests: include Authorization: Bearer {accessToken} header.
Tokens are verified against each platform's own Cognito User Pool on every request. Tokens expire after 1 hour; refresh using the Cognito SDK currentSession().
Ring & Weave Admin Portal — PocketBase-backed, admin token
The Admin Portal endpoints (/api/admin-*) are exclusive to Ring & Weave and back the affiliate program. They're a separate auth path from the Cognito-protected endpoints above — see the Admin Portal section below for the login flow. Affiliate data itself lives in PocketBase (Ring & Weave is the only platform with a live PocketBase backend), accessed server-side only.
Public endpoints
These endpoints do not require authentication:
GET /api/health
GET /api/public-data
POST /api/analytics
POST /api/submit-feedback
POST /api/gift (purchase & verify actions only — redeem requires auth)
GET /api/pattern-og
GET /api/community (Grain & Tenon only — rating also public; share/unshare require auth)
POST /api/stripe-webhook (verified via HMAC-SHA256)
Errors & Rate Limits
HTTP status codes
Status
Meaning
200
Success
202
Accepted — resource not ready yet (used by gift verify while pending)
400
Bad request — missing or invalid parameter
401
Unauthorized — invalid or expired Cognito access token
402
Payment required — subscription expired or purchase not active
403
Forbidden — not the owner of the resource (e.g. unsharing another user's Community pattern)
404
Not found — route or resource does not exist
409
Conflict — gift code already redeemed
429
Too many requests — rate limit exceeded
500
Internal server error
503
Service unavailable — dependency not configured (e.g., VAPID keys missing)
Rate limits (per IP, rolling 60-second window)
Endpoint
Limit
/api/create-checkout-session
5 req / min
/api/customer-portal
5 req / min
/api/delete-account
3 req / min
/api/submit-feedback
5 req / min
/api/gift
5 req / min
/api/verify-checkout
10 req / min
/api/analytics
60 req / min
/api/user-data
30 req / min
/api/public-data
60 req / min
/api/user-profile
30 req / min
/api/push
10 req / min
/api/pattern-og
30 req / min
/api/export-data
3 req / hr
/api/community (rate)
10 req / min · Grain & Tenon only
/api/community (share/unshare)
20 req / hr · Grain & Tenon only
/api/admin-login
10 req / min · Ring & Weave only
/api/admin-affiliates
10–30 req / min · Ring & Weave only
/api/admin-report
10 req / min · Ring & Weave only
/api/admin-payout
5 req / min · Ring & Weave only
/api/health
Unlimited (exempt)
/api/stripe-webhook
Unlimited (exempt)
All other endpoints
20 req / min (default)
Monitoring
GET
Health Check
GET/api/health
Returns the current health status of the API and a server timestamp. Use this endpoint for uptime monitoring and load-balancer health probes. Not rate-limited.
Response
Field
Type
Description
status
string
Always "ok" when the function is running.
timestamp
string (ISO 8601)
Current server UTC time.
# Replace BASE_URL with the platform base URL abovecurl"/api/health"
const res = awaitfetch('/api/health')
const data = await res.json()
// { status: 'ok', timestamp: '2026-05-24T12:00:00.000Z' }
Creates a Stripe Billing Portal session for the authenticated user. The portal allows cancelling subscriptions, updating payment methods, and viewing invoices.
Request body
Parameter
Type
Description
accessToken
string
required
Cognito access token (or pass as an Authorization: Bearer header instead).
Response
Field
Type
Description
url
string
Stripe Billing Portal URL. Redirect the user to this URL.
Checks the current subscription status for an authenticated user. Optionally pass a Stripe session ID to trigger a sync from Stripe if the webhook hasn't fired yet.
Request body
Parameter
Type
Description
accessToken
string
required
Cognito access token (or pass as an Authorization: Bearer header instead).
sessionId
string
optional
Stripe Checkout session ID (cs_live_...). When provided and the user is not yet subscribed in DynamoDB, the API queries Stripe directly and syncs the status.
Response
Field
Type
Description
subscribed
boolean
true if the user has an active subscription.
trialing
boolean
true if the subscription is in a free trial.
trial_ends_at
string | null
ISO 8601 timestamp of when the trial ends, or null.
Note: All gift code actions use the same endpoint POST /api/gift with different action values.
POST
Purchase Gift Code
POST/api/giftaction: "purchase"
Creates a Stripe Checkout session for a gift purchase. On payment completion, the webhook generates a unique gift code. The buyer polls /api/gift with action: "verify" using the returned nonce to retrieve the code once it's ready.
Request body
Parameter
Type
Description
action
string
required
Must be "purchase".
plan
string
required
Gift duration: monthly, yearly, threeyear, or lifetime.
buyerEmail
string
optional
Pre-fills the buyer's email in Stripe Checkout.
Response
Field
Type
Description
url
string
Stripe Checkout URL for the gift purchase.
nonce
string
32-char hex identifier used to retrieve the generated code after payment completes.
POST
Verify Gift Code (poll after purchase)
POST/api/giftaction: "verify"
Polls for the generated gift code after a successful purchase. Returns 202 { pending: true } while the Stripe webhook is still processing. Returns 200 with the code when it's ready.
Request body
Parameter
Type
Description
action
string
required
Must be "verify".
nonce
string
required
The nonce returned from the purchase step.
// Poll until the code is ready (Stripe webhook usually fires within 5s)async functionpollGiftCode(nonce) {
for (let i = 0; i < 12; i++) {
awaitnewPromise(r => setTimeout(r, 2500))
const res = awaitfetch('/api/gift', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'verify', nonce }),
})
if (res.status === 200) {
const { code, plan } = await res.json()
return { code, plan } // e.g. { code: 'GIFT-AB12-CD34-EF56', plan: 'yearly' }
}
}
throw newError('Timed out waiting for gift code')
}
POST
Redeem Gift Code
POST/api/giftaction: "redeem"
Redeems a gift code for the authenticated user. Activates the subscription in DynamoDB and marks the code as used.
Gift codes for lifetime plans grant permanent access; all other plans grant access for their duration (30, 365, or 1095 days).
Request body
Parameter
Type
Description
action
string
required
Must be "redeem".
code
string
required
Gift code in the format GIFT-XXXX-XXXX-XXXX. Case-insensitive.
accessToken
string
required
Cognito access token of the account to apply the gift to (or pass as an Authorization: Bearer header instead).
Response
Field
Type
Description
success
boolean
Always true on 200.
plan
string
The plan that was activated.
giftExpiresAt
string | null
ISO 8601 expiry timestamp, or null for lifetime codes.
Account
GET
User Profile
GET/api/user-profile
Returns the authenticated user's subscription status, plan, trial state, and billing portal availability. Shared across all four platforms.
Request headers
Header
Description
Authorization
Bearer {accessToken} — Cognito JWT.
Response
Field
Type
Description
email
string
User's email address.
subscribed
boolean
true if the user has an active subscription or active gift access.
subscription_plan
string | null
Plan name: monthly, yearly, threeyear, or lifetime.
trialing
boolean
true if the subscription is currently in a free trial.
trial_ends_at
string | null
ISO 8601 trial end timestamp, or null.
cancel_at_period_end
boolean
true if the subscription is set to cancel at the end of the current period.
current_period_end
string
ISO 8601 timestamp of when the current billing period ends.
gift_expires_at
string | null
ISO 8601 expiry of gift access, or null.
has_billing_portal
boolean
true if the user has a Stripe customer and can access the billing portal (not lifetime plans).
Returns a full GDPR data export for the authenticated user as a downloadable JSON file. Shared across all four platforms (the specific data types included vary slightly — see each platform's Lambda for its exact list). Rate limited to 3 requests per hour.
Request headers
Header
Description
Authorization
Bearer {accessToken} — Cognito JWT.
Response
Returns application/json with a Content-Disposition: attachment header (filename pattern varies per platform, e.g. grainandtenon-data-export-{id}.json). The body is a JSON object keyed by data type, each containing an array of records.
Permanently deletes the authenticated user's account from Cognito and DynamoDB. Clears redeemed_by references on any gift codes the user claimed, then deletes the user record. This operation is irreversible.
Request body
Parameter
Type
Description
accessToken
string
required
Cognito access token (or pass as an Authorization: Bearer header instead).
Response
{"success":true}
User Data
Shared across all four platforms. All user data (custom weaves, inventory, projects, preferences, saved items, etc.) is stored in each platform's own DynamoDB table, keyed by Cognito sub.
POST
User Data CRUD
POST/api/user-data
Create, read, update, and delete user data records. All actions are scoped to the authenticated user. Supports 6 data types: custom_weave, inventory, inlay_project, preferences, project, saved_item.
Request body
Parameter
Type
Description
action
string
required
One of: get, list, upsert, delete.
type
string
required
Data type: custom_weave, inventory, inlay_project, preferences, project, or saved_item.
accessToken
string
required
Cognito JWT.
id
string
optional
Record ID. Required for get, upsert, and delete.
data
object
optional
Record payload. Required for upsert.
GET
Public Data
GET/api/public-data?type={type}
Returns public read-only reference data: weaves, tutorials, suppliers, or patterns. No authentication required. Ring & Weave only.
Query parameters
Parameter
Type
Description
type
string
required
One of: weaves, tutorials, suppliers, patterns.
GET
Community Patterns — List
GET/api/community
Returns up to 100 shared wood patterns with their average rating. No authentication required. Grain & Tenon only.
Response
Field
Type
Description
patterns
array
Array of shared pattern objects, each including avgRating (rounded to 1 decimal) and ratingCount.
curl"/api/community"
POST
Community Patterns — Rate / Share / Unshare
POST/api/community
Rates, shares, or unshares a wood pattern, depending on action. Grain & Tenon only.rate is public; share and unshare require auth (and an active subscription or gift access for share).
Request body — action: "rate" (public)
Parameter
Type
Description
action
string
required
Must be "rate".
patternId
string
required
Alphanumeric (plus -/_), max 128 chars.
stars
number
required
Integer 1–5.
Request body — action: "share" / "unshare" (auth required)
Parameter
Type
Description
action
string
required
"share" or "unshare".
patternId
string
required
Alphanumeric (plus -/_), max 128 chars.
data
object
required for share
Pattern fields (name, type, difficulty, materials, steps, etc.). Serialized size capped at 50,000 chars; array fields are truncated (materials/steps to 50 items, tags to 20, tools to 30, woodTypes to 20).
Auth via Authorization: Bearer {accessToken} header. unshare requires the requester to be the pattern's original owner (403 otherwise).
Web Push (VAPID): The push system uses the browser-native Web Push API, not Firebase. The server stores PushSubscription objects and sends notifications via the VAPID protocol.
POST
Subscribe to Push Notifications
POST/api/pushaction: "subscribe"
Registers a browser PushSubscription for the authenticated user. If the same endpoint is already stored, it's replaced (deduplication). Requires notification permission to have been granted in the browser.
Request body
Parameter
Type
Description
action
string
required
Must be "subscribe".
accessToken
string
required
Cognito access token (or pass as an Authorization: Bearer header instead).
subscription
object
required
Browser PushSubscription serialized to JSON, containing endpoint, keys.p256dh, and keys.auth.
Sends a push notification to all registered devices for the authenticated user. Expired or revoked subscriptions (HTTP 410 from the push service) are automatically removed.
Request body
Parameter
Type
Description
action
string
required
Must be "send".
accessToken
string
required
Cognito access token (or pass as an Authorization: Bearer header instead).
title
string
required
Notification title. Truncated to 100 chars.
body
string
required
Notification body text. Truncated to 200 chars.
Response
Field
Type
Description
sent
number
Number of notifications successfully delivered.
Feedback & Analytics
POST
Submit Feedback
POST/api/submit-feedback
Submits user feedback. Sends an email notification to the team via AWS SES — not persisted to a database. If an email address is provided, the user receives an acknowledgement email.
Request body
Parameter
Type
Description
rating
number
required
Integer from 1 to 5.
likes
string
optional
What the user liked about the app.
improvements
string
optional
What could be improved.
suggestions
string
optional
Feature suggestions.
email
string
optional
User's reply-to email. If valid, a confirmation is sent.
curl -X POST "/api/submit-feedback" \
-H "Content-Type: application/json" \
-d '{
"rating": 5,
"likes": "The AR calculator is incredible",
"suggestions": "Dark mode option",
"email": "maker@example.com"
}'
awaitfetch('/api/submit-feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rating: 5,
likes: 'The AR calculator is incredible',
suggestions: 'Dark mode option',
email: 'maker@example.com',
}),
})
POST
Track Event
POST/api/analytics
Records an analytics event in DynamoDB. Event names are sanitized to alphanumeric + underscore characters and truncated at 64 chars. Properties are stored as a JSON string (max 512 bytes). No PII should be passed as event properties.
Request body
Parameter
Type
Description
event
string
required
Event name. Use snake_case. Non-alphanumeric characters are stripped.
properties
object
optional
Arbitrary key/value metadata. Serialized and truncated to 512 bytes.
Returns a static HTML page with generic Open Graph meta tags, then redirects to the in-app pattern viewer via <script>window.location.replace(...)</script>. Used as the social share URL for patterns. Does not currently look up the pattern by id — title/description are fixed generic strings; the id is only used to build the redirect URL. Responses are cached at the edge for 5 minutes (s-maxage=300).
Query parameters
Parameter
Type
Description
id
string
required
Pattern ID used to build the redirect URL. Alphanumeric only, max 20 chars.
Response
Returns text/html with generic OG tags (app name, a fixed description, and a static cover image) and a client-side redirect.
Webhooks
POST
Stripe Webhook
POST/api/stripe-webhook
Receives Stripe webhook events and syncs subscription state to DynamoDB. Register this endpoint in the Stripe Dashboard under Webhooks. Not rate-limited.
HMAC-SHA256 verification required. Every request is verified using the stripe-signature header. Requests with invalid or missing signatures are rejected with 400.
Required headers
Header
Description
stripe-signature
Stripe's HMAC signature header. Generated automatically by Stripe; do not forge.
Content-Type
Must be application/json — sent by Stripe automatically.
Events handled
Event type
Action
customer.subscription.created
Activates subscription, stores plan, trial end, cancellation state, and Stripe IDs. Sends welcome email to subscriber.
customer.subscription.updated
Syncs subscription status, cancel_at_period_end, current_period_end, and trial state changes.
customer.subscription.deleted
Sets subscribed: false and clears plan.
checkout.session.completed
For payment-mode sessions: activates lifetime plans or generates gift codes. Idempotent — duplicate Stripe sessions are ignored.
invoice.payment_failed
Sends payment failure email to subscriber. Fires on attempt 1 and on final failure (when next_payment_attempt is null).
Gift code generation
When a gift purchase checkout session completes, the webhook generates a code with the format GIFT-XXXX-XXXX-XXXX using 48 bits of cryptographic entropy (crypto.randomBytes(6)). Gift codes are stored in each platform's own gift_codes DynamoDB table and linked to the Stripe session ID for idempotency.
Internal use only. These endpoints power the admin.fignolemetalworks.com portal. All admin endpoints require a session token obtained from POST /api/admin-login passed as Authorization: Bearer {token}.
POST
Admin Login
POST/api/admin-login
Authenticates with the admin secret and returns a session token. The token is an HMAC-SHA256 digest of the secret — stateless, no expiry.
Request body
Parameter
Type
Description
password
string
required
The ADMIN_SECRET environment variable value.
Response
{"token":"a3f8b2..."}
GET
List Affiliates
GET/api/admin-affiliates?status={status}
Returns all affiliate records from PocketBase, optionally filtered by status.
Query parameters
Parameter
Type
Description
status
string
optional
Filter by status: pending, active, or rejected.
POST
Manage Affiliates
POST/api/admin-affiliates
Create, approve, or update affiliate records. When approving, automatically creates a Stripe coupon and sends an approval email to the affiliate.
Request body
Parameter
Type
Description
action
string
optional
"approve" to approve an application and create a Stripe coupon. Omit to create or update a record.
id
string
optional
PocketBase record ID. Required for approve and update actions.
couponCode
string
optional
Desired Stripe coupon code. Required when action: "approve". Sanitized to uppercase alphanumeric, max 20 chars.
discountPercent
number
optional
Discount percentage for the Stripe coupon. Defaults to 10.
GET
Commission Report
GET/api/admin-report?month={YYYY-MM}
Returns a monthly commission report for all active affiliates. Calculates earnings by coupon usage, applies the 30-day payout hold, checks the $25 minimum, and includes any rolled-over balances from prior periods.
Query parameters
Parameter
Type
Description
month
string
required
Report month in YYYY-MM format (e.g. 2026-06).
Response (array of rows)
Field
Type
Description
affiliate
object
Full affiliate record from PocketBase.
monthlyCount
number
Monthly plan conversions via this affiliate's coupon.
yearlyCount
number
Yearly plan conversions.
threeyearCount
number
3-year plan conversions.
lifetimeCount
number
Lifetime plan conversions.
earned
number
Earnings for this period (USD).
rolledBalance
number
Rolled-over balance from prior periods below the $25 minimum.
total
number
earned + rolledBalance.
holdCleared
boolean
true when the 30-day hold has passed for this period.
payable
number
Amount ready to pay out. 0 if hold not cleared or below $25 minimum.
alreadyPaid
boolean
true if a payout record already exists for this period.
POST
Run Payout
POST/api/admin-payout
Executes affiliate payouts for a given period. Supports single-affiliate or bulk payAll mode. PayPal payouts are sent automatically via the Payouts API; Stripe payouts are recorded and require a manual balance transfer in the Stripe dashboard. Sends a payout confirmation email to each affiliate paid. Records rolled balances for affiliates under the $25 minimum.
Request body
Parameter
Type
Description
period
string
required
Payout period in YYYY-MM format.
method
string
required
"paypal" or "stripe".
affiliateId
string
optional
Pay a single affiliate. Required unless payAll: true.
payAll
boolean
optional
When true, pays all eligible affiliates for the period in a single batch.
Budget Tracker is a standalone Fignole Metalworks product (a private family budget app) with its own Lambda, its own DynamoDB tables, and its own auth model — a custom HMAC-SHA256 JWT, not Cognito or PocketBase. It does not use the platform selector above; its base URL is fixed.
Platform
Base URL
Budget Tracker
https://budget.fignolemetalworks.com
Auth — custom JWT
Sign in via POST /api/login to get a token (30-day expiry, HMAC-SHA256, password hashed with PBKDF2/SHA-512). Pass it on protected routes as Authorization: Bearer {token}.