Seam Access Codes (Lower-level API)
You are an expert Seam integration engineer. Write the integration code directly into the developer's existing codebase.
Approach
- Move fast. Glob for key files (booking/reservation handlers, routes, models), read them, start writing code.
- Write code in existing files. Add Seam calls directly into existing service/handler functions. Don't create wrapper services.
- Minimize changes. Only touch files that need Seam calls + webhook route. Install SDK, add import, add calls.
How Access Codes works
You create time-bound access codes directly on specific devices. Each code has a starts_at and ends_at window. You must store the access_code_id to update or delete it later. Seam programs the code onto the lock and removes it when it expires or you delete it.
1. Install SDK + initialize
Do NOT pin to a specific version.
npm install seam # Node.js
pip install seam # Python
bundle add seam # Ruby
CRITICAL for Next.js: new Seam() at module scope BREAKS next build. Use a lazy getter:
import { Seam } from "seam";
let _seam: Seam;
function getSeam() {
if (!_seam) _seam = new Seam({ apiKey: process.env.SEAM_API_KEY! });
return _seam;
}
For Express / standard Node.js:
import { Seam } from "seam";
const seam = new Seam({ apiKey: process.env.SEAM_API_KEY });
from seam import Seam
seam = Seam(api_key=os.environ["SEAM_API_KEY"])
2. Get the device ID
Access Codes targets a specific device by device_id. Each room/door must map to its own device ID — never use a single global device for all rooms.
Look for device IDs in:
- Environment variables per room:
SEAM_DEVICE_ROOM_A1,SEAM_DEVICE_ID_ROOM_101, etc. - The app's data model (e.g.,
room.seamDeviceId,unit.deviceId)
If the mapping is missing, fail the operation — do not fall back to a global device.
3. Create access code on booking creation
Add directly inside the create function. Store the access_code_id on the booking object.
// Inside createBooking(), after saving the booking:
const deviceId = getDeviceIdForRoom(room); // Must resolve per-room
if (!deviceId) {
throw new Error(`No Seam device configured for room ${room.id}`);
}
try {
const accessCode = await seam.accessCodes.create({
device_id: deviceId,
name: `${member.name} - ${room.name}`,
starts_at: booking.startTime,
ends_at: booking.endTime
});
booking.seamAccessCodeId = accessCode.access_code_id;
booking.accessCode = accessCode.code; // The actual PIN
} catch (err) {
console.error("Seam access code creation failed:", err);
// Consider: should this fail the booking? If access is required, throw.
}
# Inside create_booking(), after saving:
device_id = get_device_id_for_room(room) # Must resolve per-room
if not device_id:
raise ValueError(f"No Seam device configured for room {room.id}")
try:
access_code = seam.access_codes.create(
device_id=device_id,
name=f"{member.name} - {room.name}",
starts_at=booking.start_time,
ends_at=booking.end_time
)
booking.seam_access_code_id = access_code.access_code_id
booking.access_code = access_code.code
except Exception as e:
print(f"Seam access code failed: {e}")
# Consider: should this fail the booking? If access is required, raise.
Gotchas
- Store
access_code_id— you need it for update and delete. Add a field to the booking model if one doesn't exist. If it'snull/undefined, the code failed and needs retry. - Never use a global device fallback — each room must map to its specific device.
device_idis a single string (one device per code), not an array.- Don't hardcode the
codevalue unless you need a specific PIN — Seam generates a random code by default. nameis optional but recommended — it appears in the Seam Console for identification.- Code slot limits — some locks have a max number of codes. Check
device.properties.max_active_codes_supported. - Decide your failure mode: if the room booking requires access (e.g., locked meeting room), throw on Seam failure. If access is optional, log and continue.
4. Update access code on booking changes
if (booking.seamAccessCodeId) {
await seam.accessCodes.update({
access_code_id: booking.seamAccessCodeId,
starts_at: booking.startTime,
ends_at: booking.endTime
});
}
if booking.seam_access_code_id:
seam.access_codes.update(
access_code_id=booking.seam_access_code_id,
starts_at=booking.start_time,
ends_at=booking.end_time
)
5. Delete access code on cancellation
if (booking.seamAccessCodeId) {
await seam.accessCodes.delete({
access_code_id: booking.seamAccessCodeId
});
}
if booking.seam_access_code_id:
seam.access_codes.delete(
access_code_id=booking.seam_access_code_id
)
6. Add webhook endpoint
Follow the existing webhook pattern in the codebase:
router.post("/seam", (req, res) => {
const { event_type, ...data } = req.body;
switch (event_type) {
case "access_code.set_on_device":
// Code is on the lock — safe to share with the user
console.log("Code set:", data.access_code_id);
break;
case "access_code.failed_to_set_on_device":
console.log("Code failed:", data.access_code_id);
break;
case "device.disconnected":
console.log("Lock offline:", data.device_id);
break;
}
res.json({ received: true });
});
7. Make functions async
Make service functions async and update callers to await them.
If something goes wrong, read references/troubleshooting.md. For production readiness, read references/production-checklist.md.