Skip to content

Assignment provider apps

An assignment provider is an app that decides who does a task: a cleaning marketplace, a maintenance dispatcher, a staffing service. The host designates your app for a task type (say, cleaning); when such a task is created, Stayblox hands the assignment to you, you pick a worker using your own data, and you write the result back over the API.

The golden rule: your credentials and endpoints live on your server; Stayblox holds only the interface. Core Stayblox contains no provider-specific code; everything below works for any marketplace through the public API. We use Turno as the concrete example, but nothing here is Turno-specific.

For a complete runnable server, see the reference app.

1. Declare your app (manifest)

Register as a remote app that reads and writes tasks, holds the act_as_assignment_provider capability, and subscribes to task.assignment_requested. In your app.toml:

toml
type = "remote"
scopes = ["read_tasks", "write_tasks"]
capabilities = ["act_as_assignment_provider"]
webhooks = ["task.assignment_requested"]
webhook_url = "https://your-app.example.com/stayblox/webhooks"

Add register_task_types to capabilities if you also define your own task types (with their own checklist and custom-field schema). The host consents to scopes and capabilities at install; both are stored on the install and checked on every call (see Scopes & capabilities).

Most real providers also want read_properties (to map your own locations to Stayblox properties ahead of time) and read_bookings (to read the checkout or checkin time so you can schedule the work). Neither is required by the handshake itself, only by whatever scheduling logic you build on top of it.

2. Get designated for a task type

The host designates your app as the assignment provider for a task type in their dashboard (Operations → Task types → Assignment provider). From then on, every task of that type that the platform creates (typically via a host-configured rule, e.g. "on checkout, create a cleaning task") is routed to you for assignment.

3. Receive the assignment request (platform → app)

When such a task is created, Stayblox POSTs a task.assignment_requested webhook to your webhook_url. The request is HMAC-signed; verify it before acting (see Signing & security). The envelope's resource identifies the task:

json
{
  "event_id": "01J…",
  "topic": "task.assignment_requested",
  "occurred_at": "2026-06-27T11:00:00Z",
  "api_version": "2026-01",
  "team": "8cs3o-qe",
  "resource": {
    "type": "task",
    "id": 42,
    "task_type": "cleaning",
    "canonical_status": "open",
    "property_id": 3,
    "booking_id": 88,
    "due_at": "2026-06-27T11:00:00Z"
  }
}

Fetch full task state any time with task(id) (read_tasks). Now schedule a worker on your side; Turno calls its own scheduling API on its own server. Stayblox never sees how you pick; it only sees the assignment you write back.

Webhook delivery is at-least-once, so treat this as a possible redelivery, not a guaranteed single call: dedupe on resource.id (or event_id, if you need per-delivery rather than per-task granularity) before you schedule anything. Verifying a delivery and resolving the install is your app's job: read the team slug from the envelope body, look up the install you stored for that team, and verify the signature with that install's webhook secret (see Signing & security). The platform keeps no delivery-dedupe store either, so nothing upstream filters duplicates for you.

4. Write the assignment back (app → platform)

Call taskAssign with your bearer token to record the assignee. This needs the act_as_assignment_provider capability and moves the task OPEN → ASSIGNED. assigneeType is one of user, app, or external: use external for a real-world worker who isn't a Stayblox user and isn't your app itself, such as a marketplace cleaner (reserve app for cases where your app, not a person it dispatched, is doing the work). Store your own identifiers as namespaced custom fields so you can correlate later.

graphql
mutation Assign($id: ID!, $name: String!) {
  taskAssign(input: { id: $id, assigneeType: "external", assigneeName: $name }) {
    task { id canonicalStatus assigneeName }
    userErrors { field message }
  }
}
bash
curl -s https://api.stayblox.com/developer/api/2026-01/graphql \
  -H "Authorization: Bearer $STAYBLOX_APP_TOKEN" \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{
    "query": "mutation($id: ID!, $name: String!){ taskAssign(input:{ id:$id, assigneeType:\"external\", assigneeName:$name }){ task{ id canonicalStatus } userErrors{ message } } }",
    "variables": { "id": "42", "name": "Sparkle Cleaners" }
  }'

Then attach your identifiers:

graphql
mutation Tag($id: ID!) {
  taskSetCustomField(taskId: $id, namespace: "turno", key: "project_id", type: "string", value: "proj_42") {
    task { id }
    userErrors { message }
  }
}

5. Push status as the work progresses

As the job moves, transition the canonical status (and add notes via checklist items or custom fields):

graphql
mutation Progress($id: ID!, $status: TaskStatus!) {
  taskTransitionStatus(id: $id, status: $status) {
    task { id canonicalStatus }
    userErrors { field message }
  }
}

Walk ASSIGNED → IN_PROGRESS → COMPLETED as your worker accepts, starts, and finishes. Every transition is validated; illegal moves come back in userErrors. A rejected transition usually means the task moved on without you (already completed or cancelled through another path). Treat that as terminal for the current job: stop pushing further transitions for that task, refetch it to see its actual state, and if you scheduled work on your own side, cancel it there too. Retrying the same transition just returns the same error.

Staying in sync

Two more things come up once this is running for real, beyond the happy path above.

If you also subscribe to task.status_changed (to notice cancellations or changes made outside your app), remember that your own taskTransitionStatus and taskAssign calls trigger that same topic back to you. Keep a record of the status you last wrote for each task, and drop an incoming task.status_changed when it just confirms what you already wrote; otherwise you'll reprocess your own writes.

Retries are capped (see Webhooks), so a sustained outage on your end can leave a task stuck in your last-known state even after you recover. Poll tasks(filter: { updatedSince: <your last cursor> }) on a schedule and replay anything you find through the same logic you use for webhooks. Because that logic is already deduplicated (see step 3), replaying tasks you're already in sync with is safe and should be a no-op.

The fallback

If you don't write an assignment back within the host's SLA window, Stayblox falls back to its own built-in assignment so the task never stalls. That fallback checks whether you already responded before it acts, so racing it by writing back first is always safe. The reverse is guarded too: a late taskAssign on a task that the fallback (or the host, or another app) has already assigned returns a userError instead of succeeding. Treat that error as a lost race: keep the existing assignee, release whatever resources you created for the task on your side, and stand down. Re-assigning a task you assigned yourself is still allowed, so marketplace reassignment (one worker cancels, another takes over) keeps working.

Guest-readiness

A property becomes guest-ready when its cleaning task reaches COMPLETED / VERIFIED, derived purely from canonical status, never from a label you set. So the single thing that flips a unit to "ready" is your taskTransitionStatus to COMPLETED.

Worked example: a cleaning marketplace (Turno)

End to end, with zero Turno code in Stayblox:

  1. The host designates the Turno app as the cleaning provider and keeps the default "on checkout, create a cleaning task" rule.
  2. A guest checks out → the rule creates a cleaning task → Stayblox emits task.assignment_requested to Turno's webhook_url.
  3. Turno schedules a cleaner via its own API on its own server, then calls taskAssign + taskSetCustomField(turno.project_id, …).
  4. As the cleaner accepts → starts → finishes, Turno pushes taskTransitionStatus through ASSIGNED → IN_PROGRESS → COMPLETED, attaching progress notes via custom fields as it goes.
  5. The property flips to guest-ready off the canonical COMPLETED.

Turno's API keys and endpoints stay on Turno's server throughout. Stayblox stores only the assignment and the custom fields Turno chose to write.

Checklist

  • [ ] Manifest registered with type: remote, scopes: [read_tasks, write_tasks], capabilities: [act_as_assignment_provider].
  • [ ] Subscribed to task.assignment_requested; webhook_url set.
  • [ ] Webhook handler verifies the HMAC signature before acting.
  • [ ] Deliveries deduplicated (e.g. by resource.id) before any side-effecting call.
  • [ ] Assignment written back with taskAssign (+ your ids via taskSetCustomField), using assigneeType: "external" for a real-world worker.
  • [ ] Status pushed with taskTransitionStatus as work progresses.
  • [ ] userErrors checked on every mutation, and treated as a signal to stand down, not to retry blindly.
  • [ ] If subscribed to task.status_changed, your own writes are recognized and skipped.
  • [ ] A periodic resync against tasks(filter: { updatedSince }) covers whatever webhooks miss.
  • [ ] Write-back happens within the host's SLA window; a late taskAssign returns a userError when someone else already got the task, and your app treats that as a lost race and stands down.

© Stayblox — Developer Platform