Integrate with any access control system using Addons/Webhooks
Integrations use webhooks to receive real-time events from Storeganise, such as move-ins, move-outs, transfers, and unit rental updates. The provided webhook handler, hosted on napkin.io, efficiently manages these events and communicates with Access Control systems and Storeganise APIs to perform access control actions.
Webhook events handled
- Move-In Completion
- Description: Handles the completion of unit move-ins.
- Actions: Creates group memberships, updates access codes, and sets up access PINs.
- Move-Out Completion
- Description: Handles the completion of unit move-outs.
- Actions: Revokes group memberships.
- Unit Transfer Completion
- Description: Handles the completion of unit transfers.
- Actions: Revoke group memberships for the old unit and set up access for the new unit.
- Unit Rental Update
- Description: Handles updates to unit rentals, including PIN changes and overlocking status.
- Actions: Updates access codes and overlocking status.
- Unit Overdue Marking/Unmarking
- Description: Handles marking and unmarking units as overdue.
- Actions: Implements access control measures based on overlocking status.
List of security systems we currently work with
- Bearbox
- Noke
- PTI
- SaltoKS
- Sensorberg
- OpenTech Alliance
- Entryfy
Storeganise API key & webhook setup
Getting Started
- Webhook Configuration: Set up a webhook on napkin.io to receive Storeganise events.
- Create a webhook in https://{business}.storeganise.com/admin/settings/developer, with the url of your webhook handler (it can be AWS lambda, napkin.io, other services or a custom server) with events: job.unit_moveIn.completed, job.unit_moveOut.completed, job.unit_transfer.completed, unitRental.markOverdue, unitRental.unmarkOverdue, unitRental.updated
- Create customFields you’ll need in your integration in https://{business}.storeganise.com/admin/settings/custom-fields, for example unit.customFields.rubik_id, unitRental.customFields.rubik_pinCode, unitRental.customFields.rubik_overlocked (we use this convention to prefix all custom fields with the integration name, here “rubik”)
- Credentials: Create API keys in Storeganise and obtain credentials for your access control API
- Create an API key with admin role in https://{business}.storeganise.com/admin/settings/developer
- Contact the commercial support, for example: sales@sensorberg.com,
- Deploy the Handler: Host the provided webhook handler code and configure environment variables. (Storeganise can also host the code upon request)
- Test and Monitor: Thoroughly test the integration and monitor webhook events for real-time access control updates.
Code Snippet - Core template for an integration
The main request handler receives events from Storeganise (move-in, move-out, transfer, overlocking) and call the access control API to synchronize these changes
function sgApi(apiUrl, authKey) { function _fetch(path, opts = {}) { return fetch(`${apiUrl}/v1/admin/${path}`, { method: opts.method, headers: { Authorization: `ApiKey ${authKey}`, ...opts.body && { 'Content-Type': 'application/json' }, }, body: opts.body && JSON.stringify(opts.body), }) .then(async r => { const data = await r.json().catch(() => ({})); if (r.ok) return data; throw Object.assign(new Error('Storeganise Error'), { status: r.status, ...data.error }); }); } return { get: (path, optionalId, params = optionalId || { include: 'customFields' }) => _fetch([path, optionalId].filter(Boolean).join('/') + (params ? '?' + new URLSearchParams(params) : '')), put: (path, body) => _fetch(path, { method: 'PUT', body }), }; } // This is an example custom access control api handler, you'll need to implement this part function myACApi(customFields) { function _fetch(path, opts = {}) { return fetch(`https://some.api.com/${customFields.myAC_siteId}/${path}`, { method: opts.method, headers: { Authorization: `Bearer ${customFields.myAC_clientSecret}`, ...opts.body && { 'Content-Type': 'application/json' }, }, body: opts.body && JSON.stringify(opts.body), }) .then(async r => { const data = await r.json().catch(() => null); if (!r.ok) throw Object.assign(new Error(`MyAC Error ${opts?.method || 'GET'} ${path} ${JSON.stringify(data)}`), { status: r.status }); return data; }); } return { get: (...paths) => _fetch(paths.filter(Boolean).join('/')), post: (path, body) => _fetch(path, { method: 'POST', body }), put: (path, body) => _fetch(path, { method: 'PUT', body }), delete: path => _fetch(path, { method: 'DELETE' }), }; } export default async (req, res) => { const { data = {} } = req.body || {}; const sg = sgApi(data.apiUrl, process.env.API_URL); // For basic webhooks (not using addons) you'll want to retrieve the job then usually the site where you can store your access control API credentials in site.customFields // const job = await sg.get('jobs', data.jobId); // const site = await sg.get('sites', job.result.siteId); // But now with addons, we store credentials in the addon.customFields const addon = await sg.get('addons', data.addonId); const ac = myACApi(addon.customFields); try { switch (req.body.type) { case 'job.unit_moveIn.completed': { const rental = await sg.get('unit-rentals', data.unitRentalId, { include: 'owner,unit,customFields' }); // Your logic here for renting a unit using custom fields // you would call your access control API // e.g. const user = await ac.post('users', { email: rental.owner.email, .. }) // and then you can also call the Storeganise API for setting the unitRental custom fields (access codes for example) // e.g. await sg.put('unit-rentals', rental.id, { customFields: { myAC_accessCode: value } }) break; } case 'job.unit_moveOut.completed': { const rental = await sg.get('unit-rentals', data.unitRentalId, { include: 'owner,unit,customFields' }); // your logic for vacating unit break; } case 'job.unit_transfer.completed': { const newRental = await sg.get('unit-rentals', data.newRentalId, { include: 'owner,unit,customFields' }); const oldRental = await sg.get('unit-rentals', data.oldRentalId, { include: 'unit,customFields' }); // your logic, usually vacate old unit and rent new one, reusing same code from above events break; } case 'unitRental.updated': { const rental = await sg.get('unit-rentals', data.unitRentalId, { include: 'owner,unit,customFields' }); // your logic to update the rental access codes and/or overlocking status using custom fields break; } case 'unitRental.markOverdue': case 'unitRental.unmarkOverdue': { const rentals = await sg.get('unit-rentals', { ids: data.unitRentalIds || data.unitRentalId, include: 'unit,customFields' }); // your logic for updating those rentals overlocking status break; } } res.json({ message: 'ok ' + req.body.type }); } catch(err) { console.error(err); res.status(err.status || 400).json({ message: err.message }); } }