Skip to main content

Booking Self-service Widget — partner onboarding guide

This guide walks an operator and a partner through a complete onboarding cycle: from a fresh Shillinq instance to a live booking on the partner's website. It targets REQ-WSW-008 (operator workflow), REQ-WSW-009 (key lifecycle / audit), and REQ-WSW-005 (partner integration). Each step ends with a runnable command or a visible signal so the operator can verify progress without round-tripping the partner.

Status: phase-1 partner onboarding. The dedicated admin UI tracked under the follow-up bookings-widget-admin-ui slice (Task 18) will replace step 3 with a click-through experience; the OCC / API path below is the authoritative onboarding flow until that ships.

Outcomes

  • One WidgetAccessKey record per partner business, bcrypt-hashed at rest, with the plaintext key handed to the partner exactly once.
  • Partner site embeds the widget through one of the four supported methods (REQ-WSW-004) and serves real availability from the Shillinq instance.
  • Operator has an auditable lifecycle trail (created → rotated → revoked) for every issued key (REQ-WSW-009).

Prerequisites

  • Shillinq installed and enabled on a Nextcloud instance at, for example, https://shillinq.example.com.
  • Administrator account with shell access to the Nextcloud container or REST access to the Shillinq admin API.
  • Partner business already represented as a business record in Shillinq (this is the administrationId foreign key on the access key — set up during the standard Shillinq business onboarding).
  • Partner has confirmed they need bookings (one or more Service and Resource records already configured for that business).

Step 1 — confirm services and resources

Before issuing a key, verify the partner has at least one active Service and one Resource so the widget has slots to render. From the container shell:

docker exec nextcloud bash -c \
"cd /var/www/html/custom_apps/shillinq && \
php occ openregister:object:list shillinq Service \
--filter status=active --filter administrationId=<admin-id>"

Expected output: at least one row. If empty, ask the partner to create their service catalogue first; the widget has nothing useful to show otherwise.

Step 2 — mint the partner access key

Use the OCC bridge that wraps WidgetAuthService::createApiKey(). The operator supplies the administrationId (the partner's business record id) and a stable businessId (the slug shown to partners and stored in the widget config). The plaintext key is returned once in stdout.

docker exec -u www-data nextcloud bash -c \
"cd /var/www/html/custom_apps/shillinq && \
php occ shillinq:widget:key:create \
--administration-id=<admin-id> \
--business-id=salon-001 \
--actor=admin"

Expected output (sample):

{
"businessId": "salon-001",
"apiKey": "bk_live_oH2bC4eU7fW9...truncated",
"apiKeyPrefix":"bk_live_oH2bC4",
"rateLimit": 100,
"status": "active"
}

Operator must capture the apiKey value at this moment. The record in oc_openregister_table_*_widgetaccesskey only stores apiKeyHash + apiKeyPrefix; the plaintext is never queryable again.

If the OCC command is not yet wired, the same operation is available via the admin REST endpoint:

curl -X POST \
"https://shillinq.example.com/index.php/apps/shillinq/api/admin/widget/keys" \
-H "OCS-APIREQUEST: true" \
-u admin:<admin-password> \
-H "Content-Type: application/json" \
-d '{"administrationId":"<admin-id>","businessId":"salon-001"}'

Step 3 — confirm the audit trail

Every key lifecycle event writes a WidgetAccessKeyAuditEntry (REQ-WSW-009). Verify the created entry is present before handing the key to the partner:

docker exec nextcloud bash -c \
"cd /var/www/html/custom_apps/shillinq && \
php occ openregister:object:list shillinq WidgetAccessKeyAuditEntry \
--filter businessId=salon-001"

Expected: at least one entry with action: created, actor: admin, createdAt: <recent timestamp>. If the entry is missing, do NOT hand the key to the partner — investigate first.

Step 4 — hand the key to the partner

Send the partner an out-of-band message (Signal, encrypted email, or password manager share) containing:

  • businessId — the public identifier shown in widget config
  • apiKey — the plaintext key from Step 2
  • apiBasehttps://shillinq.example.com/index.php/apps/shillinq
  • A pointer to Widget Embed methods and the Widget API reference.

Recommend the partner store the key in their secrets manager and never commit it to source control.

Step 5 — partner installs the widget

The partner picks one of the four embed methods (REQ-WSW-004). The quickest end-to-end smoke test is the script-tag method, which works on any plain HTML page:

<!doctype html>
<html>
<body>
<div id="booking-widget"></div>
<script src="https://shillinq.example.com/apps/shillinq/widget.js"></script>
<script>
BookingWidget.init({
businessId: 'salon-001',
apiBase: 'https://shillinq.example.com/index.php/apps/shillinq',
apiKey: 'bk_live_oH2bC4eU7fW9...',
containerId: 'booking-widget',
lang: 'nl',
primaryColor: '#ff6b6b',
})
</script>
</body>
</html>

Expected: the widget renders with the service list pulled from GET /api/widget/services. The partner can navigate service → date+time → details → review and create an appointment.

For the iframe, npm, and web-component methods, see Widget Embed methods.

Step 6 — verify a live booking end-to-end

Operator confirms the widget round-trips through the backend:

docker exec nextcloud bash -c \
"cd /var/www/html/custom_apps/shillinq && \
php occ openregister:object:list shillinq Appointment \
--filter administrationId=<admin-id> \
--order createdAt:desc --limit 1"

Expected: one row whose customerEmail matches the value the partner typed into the widget. If empty, check the Nextcloud log for entries prefixed [WidgetApi] — every server-side error path logs the businessId and exception message (REQ-WSW-007).

Step 7 — set the rotation reminder

API keys rotate every 30 days; the predecessor remains valid for a 7-day grace window (REQ-WSW-009). Schedule a calendar reminder for day 23 to issue the next key.

docker exec -u www-data nextcloud bash -c \
"cd /var/www/html/custom_apps/shillinq && \
php occ shillinq:widget:key:rotate \
--business-id=salon-001 \
--actor=admin"

The command returns a new plaintext key (Step 2 semantics). The previous key transitions from active to rotating and remains valid until the grace window closes.

Troubleshooting

SymptomLikely causeFix
Widget shows "Configuration error"API key was revoked or the businessId is wrongRe-issue the key via Step 2 and update the partner's embed config.
Widget shows "Too many requests"Per-business rate limit (default 100/min) hitInvestigate partner traffic or raise rateLimit on the key record.
Widget shows "This slot was just booked"Real race — slot collision detected (409)Slots auto-refresh; user picks another slot. Expected behaviour.
Slots panel is emptyResource opening/closing window excludes the dateVerify Resource.openingTimes for the partner's business.
No Appointment rows after a bookingOR write rejected — check NC logsdocker exec nextcloud tail -n 200 data/nextcloud.log.
Audit trail entry missingOR persistence error — fail closedRe-run Step 2; investigate oc_openregister_* table state.

Revocation

If a key is leaked or the partner offboards:

docker exec -u www-data nextcloud bash -c \
"cd /var/www/html/custom_apps/shillinq && \
php occ shillinq:widget:key:revoke \
--business-id=salon-001 \
--actor=admin"

Revocation is immediate (no grace window). The audit trail records an entry with action: revoked and the actor.

References

  • Widget embed methods — the four supported integration methods (iframe, script, npm, web component) and CSS customisation surface.
  • Widget API reference — REST contract for GET /services, GET /slots, POST /appointments, error tables.
  • openspec/changes/bookings-self-service-widget/specs/bookings-self-service-widget/spec.md — authoritative requirements (REQ-WSW-*) and scenarios.