Recipes
Practical Fuxam API v4 patterns — sync users, look up courses by code, safe ETag updates, and idempotent creates.
These recipes are end-to-end patterns built only on the documented v4 conventions: Basic Auth, the { data, meta } envelope, cursor pagination, Idempotency-Key, and ETag / If-Match. Use them as templates in your middleware; swap resource paths as needed and confirm field names on the generated endpoint reference pages.
Base URL: https://fuxam.app/api/v4
Why recipes help
Reference pages show one operation at a time. Real integrations chain operations: list → filter → get → patch, or create → retry. The patterns below encode those sequences so university IT teams can ship reliable sync jobs without rediscovering concurrency and pagination edge cases.
Before you start
- Complete Getting started so credentials work.
- Read Authentication, Pagination and filtering, and Errors and idempotency.
- Keep
CLIENT_IDandCLIENT_SECRETin the environment; never hard-code them in scripts you commit.
Recipe: Sync all users
Goal: Pull every user into an external system using cursor pagination.
When to use: Nightly identity sync with User Management as the source of truth for who exists in Fuxam.
#!/usr/bin/env bash
set -euo pipefail
BASE="https://fuxam.app/api/v4"
CURSOR=""
LIMIT=100
while true; do
if [ -z "$CURSOR" ]; then
URL="${BASE}/users?limit=${LIMIT}&fields=id,email"
else
URL="${BASE}/users?limit=${LIMIT}&fields=id,email&cursor=${CURSOR}"
fi
RESP=$(curl -sS -u "${CLIENT_ID}:${CLIENT_SECRET}" \
-H "Accept: application/json" "$URL")
# Upsert each object in RESP.data into your SIS / directory mirror.
printf '%s\n' "$RESP" | jq -c '.data[]'
HAS_MORE=$(printf '%s' "$RESP" | jq -r '.pagination.has_more')
CURSOR=$(printf '%s' "$RESP" | jq -r '.pagination.next_cursor // empty')
if [ "$HAS_MORE" != "true" ]; then
break
fi
done
Pseudocode:
cursor = null
loop:
response = GET /users?limit=100&fields=id,email[&cursor=cursor]
upsert_all(response.data)
if response.pagination.has_more is false: break
cursor = response.pagination.next_cursor
Recipe: Look up a course by code, then read by id
Goal: Resolve a human-facing course code to a stable id, then fetch the full resource.
When to use: Migrations from v3 (where code addressing was more common), SIS imports that only know course codes, or linking to courses staff manage in Courses Management.
CODE="CS101"
LIST=$(curl -sS -u "${CLIENT_ID}:${CLIENT_SECRET}" \
-H "Accept: application/json" \
"https://fuxam.app/api/v4/courses?code=${CODE}&limit=25")
COURSE_ID=$(printf '%s' "$LIST" | jq -r '.data[0].id // empty')
if [ -z "$COURSE_ID" ]; then
echo "No course found for code ${CODE}" >&2
exit 1
fi
curl -sS -u "${CLIENT_ID}:${CLIENT_SECRET}" \
-H "Accept: application/json" \
-D - \
"https://fuxam.app/api/v4/courses/${COURSE_ID}"
HTTP sequence:
GET /api/v4/courses?code=CS101&limit=25 HTTP/1.1
Host: fuxam.app
Authorization: Basic <base64(clientId:clientSecret)>
Accept: application/json
GET /api/v4/courses/{id} HTTP/1.1
Host: fuxam.app
Authorization: Basic <base64(clientId:clientSecret)>
Accept: application/json
Recipe: Safely update a resource with ETag
Goal: Change a single resource without overwriting someone else’s concurrent edit.
When to use: Any automated PATCH, PUT, or DELETE on a single resource that emits ETag on GET.
USER_ID="usr_123"
# 1) Read current version
HEADERS=$(mktemp)
BODY=$(mktemp)
curl -sS -u "${CLIENT_ID}:${CLIENT_SECRET}" \
-H "Accept: application/json" \
-D "$HEADERS" \
-o "$BODY" \
"https://fuxam.app/api/v4/users/${USER_ID}"
ETAG=$(awk 'BEGIN{IGNORECASE=1} /^ETag:/{print $2}' "$HEADERS" | tr -d '\r')
# 2) Write with If-Match
HTTP_CODE=$(curl -sS -u "${CLIENT_ID}:${CLIENT_SECRET}" \
-o /tmp/patch-response.json -w "%{http_code}" \
-X PATCH \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "If-Match: ${ETAG}" \
-d @patch-body.json \
"https://fuxam.app/api/v4/users/${USER_ID}")
if [ "$HTTP_CODE" = "412" ]; then
echo "precondition_failed — re-GET, merge, retry" >&2
exit 1
fi
Pseudocode:
resource, etag = GET /users/{id} # capture ETag header
try:
PATCH /users/{id} with If-Match: etag
catch 412 precondition_failed:
resource, etag = GET /users/{id} again
merge_changes(resource)
retry PATCH with new etag
catch 428 precondition_required:
ensure If-Match is sent
Recipe: Create with idempotency
Goal: Create a resource once, even if the HTTP client retries after a timeout.
When to use: Provisioning jobs, webhook consumers that may deliver twice, any create that must not duplicate.
IDEMPOTENCY_KEY="$(uuidgen)"
curl -sS \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d @create-body.json \
"https://fuxam.app/api/v4/users"
POST /api/v4/users HTTP/1.1
Host: fuxam.app
Authorization: Basic <base64(clientId:clientSecret)>
Content-Type: application/json
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Accept: application/json
# Body: JSON create payload from the endpoint reference
Behavior to expect:
| Situation | Result |
|---|---|
| First successful create | 201 with the new resource in data |
| Retry with same key and equivalent create | Cached original response (including 201) — no duplicate |
| Conflicting in-flight key | May return 409 — wait and retry with the same key, or inspect conflict handling in your job |
Recipe: Handle rate limits while paging
Goal: Finish a large sync without failing the job when you hit 1000 requests/min.
loop pages:
response = GET list
if status == 429:
sleep(Retry-After)
retry same request
process response.data
advance cursor
# Illustrative fragment — honor Retry-After from response headers
RETRY_AFTER=60
sleep "$RETRY_AFTER"
Pair this with larger limit values so you need fewer requests per full walk. Details: Errors and idempotency.
Choosing a pattern
| Need | Recipe |
|---|---|
| Mirror the directory | Sync all users |
| SIS knows course codes only | Look up by code, then by id |
| Update without clobbering UI edits | Safely update with ETag |
| Provision without duplicates | Create with idempotency |
| Large sync under load | Rate-limit-aware paging |
Best practices across recipes
- Log
meta.requestIdon every response. - Prefer server-side jobs over browser-side calls with long-lived secrets.
- Separate integrations per workload so rate limits and rotations stay isolated (Authentication).
- After coding a recipe, open the matching reference card from the Overview browse section to confirm request/response schemas.
Common pitfalls
| Pitfall | Better approach |
|---|---|
Syncing with limit=1 |
Use up to 100 for bulk jobs |
| Creating without Idempotency-Key | Always set a UUID per logical create |
| Patching without If-Match | GET first; send ETag |
| Assuming code is the primary key for courses | Resolve code → id, then use id |
FAQ
Can I adapt these recipes to curricula or rooms?
Yes — the envelope, pagination, auth, idempotency, and ETag rules are shared. Replace paths with the resource you need from the overview browse cards (for example curricula or rooms), and confirm filters on that endpoint’s reference page.