This guide covers every common issue with CourtPin. Each entry includes the symptom, the likely cause, and the fix.
If your issue is not listed here, open a GitHub Issue with your log output and environment details.
All CourtPin activity is logged to stdout with a timestamp and level. On Railway: service → Deployments → latest → View Logs.
[2026-03-15T12:00:00.000Z] [INFO] CourtReserve <-> UniFi Access integration starting
[2026-03-15T12:00:00.000Z] [INFO] Email transport: Resend
[2026-03-15T12:00:00.000Z] [INFO] Fetched 2 reservation(s)
[2026-03-15T12:01:00.000Z] [DEBUG] Reservation timing check {"reservationId":"50786225",...}
[2026-03-15T12:01:00.000Z] [INFO] Processing player {"memberId":1788252,...}
[2026-03-15T12:01:02.000Z] [INFO] ✅ Player processed successfully {"pin":"67203419",...}
Log levels: debug, info, warn, error.
Symptom:
❌ Missing required environment variables: CR_ORG_ID, CR_API_KEY
Fix: Open Railway → Variables tab and confirm every required variable has a value. See configuration.md for the full list of required variables.
Symptom:
❌ No email transport configured.
Fix: Set either RESEND_API_KEY (for Resend) or SMTP_HOST + SMTP_USER + SMTP_PASS (for SMTP). At least one must be configured. See email-setup.md.
Symptom: Logs show Fetched 0 reservation(s) but you can see a reservation in CourtReserve.
Most likely cause: wrong timezone. Railway runs in UTC. Without TZ set, CourtPin queries the wrong date and misses all reservations.
Fix:
- Confirm
TZ=America/New_York(or your timezone) is set in Railway Variables - If it was missing, add it — Railway will redeploy automatically
- Check the debug log:
localNowshould show your local time, not UTC
Other possible causes:
- The reservation was already in the past when CourtPin checked
- Wrong
CR_ORG_IDorCR_API_KEY - ReservationReport → Read permission not enabled on the API key
Symptom:
CourtReserve API error: {"ErrorMessage":"Unauthorized",...}
Fix: Check CR_ORG_ID and CR_API_KEY. The org ID is the number in your CourtReserve admin URL after /organization/. The API key is found in Admin → Settings → API Access.
Symptom:
CourtReserve API error: {"ErrorMessage":"Forbidden",...}
Fix: The API key is missing the required permission. In CourtReserve Admin → Settings → API Access, edit your key and ensure ReservationReport → Read is checked.
Symptom:
CourtReserve API error: Currently, this report cannot be run for a period longer than 31 days
Fix: This happens if NOTIFY_MINUTES_BEFORE is set to a very large value (like 1440) and the query range crosses midnight. Set it back to 60 for normal operation. Use 1440 only temporarily for testing.
Symptom: A member receives duplicate PIN emails for the same reservation.
Cause: The state file was reset. This can happen if Railway restarted and cleared /tmp/state.json, or if STATE_FILE was changed to a new path.
Fix: This is harmless — the member just receives an extra notification. Duplicate Visitor records in UniFi are cleaned up automatically after the reservation ends. To reduce frequency, consider a more persistent storage option (see CONTRIBUTING.md for ideas).
Symptom:
Failed to create UniFi visitor {"err":"Create visitor failed: {\"code\":\"CODE_UNAUTHORIZED\",...}"}
Fix: The API token is missing required scopes. Delete it and create a new one in UniFi Portal → Access → Settings → General → Advanced → API Token with both view:credential and edit:visitor checked.
Symptom:
Failed to create UniFi visitor {"err":"timeout of 15000ms exceeded"}
Cause: Railway (or your VPS) cannot reach your UniFi console.
Fix: Work through unifi-setup.md — Making UniFi reachable from the internet. Specifically check:
- Port forwarding rule is saved in UniFi (WAN Port 12445 → Forward Port 12445)
- Firewall rule exists (Internet Local or Internet In, TCP, port 12445)
- portchecker.co shows port
12445as Open on your public IP UNIFI_HOSTin Railway matches your public IP or DuckDNS hostname exactly- If using DuckDNS, the updater is running and your IP is current
Symptom:
Failed to generate/assign PIN {"err":"PIN generation failed:..."}
Fix: The API token is missing view:credential scope. Delete and recreate the token with both required scopes.
Symptom: Old Visitor records remain in UniFi Access long after reservations have ended.
Fix: CourtPin deletes expired visitors every minute, CLEANUP_BUFFER_MINUTES after end time. If records persist check:
- Railway service is still running (Deployments tab)
CLEANUP_BUFFER_MINUTESis set correctly (default: 15)- UniFi API token is still valid and has
edit:visitorpermission - The visitor was actually created by CourtPin. CourtPin's automatic cleanup only removes visitors it created itself (tagged internally via the Remarks field) — visitors added manually in the UniFi Access UI, or by another integration, are left alone by design and must be deleted manually.
Symptom:
Failed to send email {"err":"Connection timeout"}
Cause: Railway blocks outbound SMTP ports (25, 465, 587) on free and hobby tiers.
Fix: Switch to Resend. Set RESEND_API_KEY and leave SMTP_* variables blank. Resend sends over HTTPS (port 443) which Railway never blocks. See email-setup.md.
Symptom:
Failed to send email {"err":"..."}
Fix:
- Log into resend.com and confirm your API key exists and has Sending access
- Confirm your domain shows Verified in Resend → Domains
- Ensure
EMAIL_FROMuses an address at your verified domain - For testing, try
EMAIL_FROM=onboarding@resend.dev
Symptom:
Failed to send email {"err":"Invalid login"}
or
Failed to send email {"err":"535 Authentication failed"}
Fix:
- Confirm
SMTP_USERandSMTP_PASSare correct - If using Gmail, you need an App Password — your regular Google password does not work for SMTP. See email-setup.md
- Confirm
SMTP_HOSTandSMTP_PORTmatch your provider's settings exactly
Symptom: Email arrives but no SMS is sent.
Fix:
- Confirm
TWILIO_ENABLED=true - Confirm
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_FROM_NUMBERare all set correctly - The member must have a phone number stored in CourtReserve — CourtPin skips SMS silently if no phone number is found
- On a Twilio free trial, you can only send to verified numbers. Go to twilio.com/console → Verified Caller IDs to add numbers, or upgrade your account
Symptom: The /admin URL returns an error or does not load.
Fix:
- Confirm
PORT=3000andADMIN_PORT=3000are both set in Railway Variables - Go to Railway → your service → Settings → Networking and confirm a domain has been generated. If not, click Generate Domain.
- Check the Deployments tab — confirm the latest deployment succeeded
UniFi Fabric sites — PIN length locked to 4 digits (hidden setting, not a bug)
Symptom: Assign PIN failed: {"code":"CODE_SYSTEM_ERROR","msg":"Server system error."} when using PIN_MODE=static, and the PIN Length setting in UniFi Access (Settings → General → PIN) is greyed out and stuck on Fixed Length 4-digit.
Cause: This initially looked like a confirmed Ubiquiti platform bug on UniFi Fabric (multi-site management) sites, but it's actually a setting, not a bug. Fabric's Identity Settings default "Smart Door Access" to All, which forces every site in the Fabric to Fixed 4-digit PINs. Switching that setting to Custom exposes the real PIN length/type controls per site.
Fix: In the UniFi console, go to the Fabric's Identity Settings, change Smart Door Access from All to Custom, then select the PIN length/type you want (Variable Length, or a specific fixed length up to 8 digits). Once that's set, STATIC_PIN_LENGTH can be full or whatever length matches your chosen setting — it's no longer forced to 4.
Collision risk: With a short STATIC_PIN_LENGTH (e.g. 4, a 10,000-value keyspace) two members can end up with the same truncated PIN — this is not theoretical, it has happened in production (three collision pairs found across one club's membership export). As of the current version, CourtPin handles this automatically: assignPinWithFallback() retries a colliding static PIN with progressively more digits (5, 6, 7... up to the member's full ID), and if every static-length candidate still collides, falls back to assigning a fully random UniFi-generated PIN instead. No configuration is needed for this — it applies automatically whenever PIN_MODE=static. If you'd rather avoid static PINs altogether, PIN_MODE=random sidesteps the whole class of issue since it never depends on member ID digits.
Symptom: Failed to assign PIN error in logs when PIN_MODE=static is set.
Cause: UniFi Access is configured for a fixed PIN length (4, 6, or 8 digits) but the CourtReserve OrganizationMemberId is 7 digits and does not match.
Fix: In UniFi Access go to Settings → General → PIN and change from Fixed Length to Variable Length, then save. CourtPin will retry on the next cycle.
Symptom: Member enters their member ID at the keypad but access is denied.
Possible causes:
- UniFi PIN mode is still set to Fixed Length — change to Variable Length as above
- Member is entering the wrong number — confirm their
OrganizationMemberIdin CourtReserve Admin → Members PIN_MODEvariable is still set torandomin Railway — confirm it is set tostatic
Symptom: A reservation card shows "PIN not stored for this record."
Cause: This reservation was processed before PIN logging was added (an earlier version of CourtPin), or the state file was reset before the PIN was written.
Fix:
- Check Railway logs and search for the reservation ID — the PIN is logged at processing time:
[INFO] ✅ Player processed successfully {"pin":"67203419",...} - Check UniFi Access → Visitors — the Visitor record may still be active
- To force reprocessing: rename
STATE_FILEto a new path (e.g./tmp/state_new.json) — CourtPin will treat all reservations as unprocessed and re-send PINs
Symptom: Members can enter too early, or the PIN is not active when they arrive.
Fix:
- Confirm
TZmatches your facility's timezone exactly (e.g.America/New_YorknotEST) ACCESS_BUFFER_MINUTEScontrols how early the PIN activates before reservation start (default: 30)- If times are consistently off by a fixed number of hours,
TZis wrong
Symptom: Members receive their PIN at unexpected times.
Fix: NOTIFY_MINUTES_BEFORE controls when the PIN is sent relative to reservation start. Default is 60 (1 hour before). Adjust to your preference.
Symptom: Railway shows frequent restarts in the Deployments tab.
Fix: Check the logs just before each restart for an error message. Common causes:
- A missing required environment variable causes an immediate exit on startup
- An uncaught error in the code (please open a GitHub Issue with the log output)
Symptom: After a Railway restart, previously processed reservations are processed again.
Cause: Railway's /tmp directory is ephemeral — it is cleared when the container restarts.
Fix: This is a known limitation of the current state file approach. The practical impact is minimal — members receive a duplicate PIN email at worst, and the PIN and access window are the same. For a more persistent solution, see the persistent state storage idea in CONTRIBUTING.md.