Skip to main content
sLocker

sLocker

Deliver to Superroute smart lockers. sLocker is the SDK for carriers and delivery partners that put parcels into Superroute smart lockers: create a delivery task, open the doors remotely for the driver, confirm the deposit, follow the pickup, and receive and verify the webhooks — with a mock cabinet to rehearse all of it. Superroute SDK License — see LICENSE and open.superroute.ca/developer/license.

Package Platform Install
superroute-slocker (PyPI) Python 3.9+ pip install superroute-slocker
superroute/slocker (Packagist) PHP 8.1+ composer require superroute/slocker
ca.superroute:slocker (Maven) Java 11+ and Android see Java and Kotlin
ca.superroute:slocker-android (Maven) Kotlin coroutines and OkHttp, for Android see Java and Kotlin
github.com/Goodneo-Solutions/slocker-releases/go Go 1.21+ go get github.com/Goodneo-Solutions/slocker-releases/go
Superroute.SLocker (NuGet) .NET 8 dotnet add package Superroute.SLocker
@superrouteca/slocker (npm) Node 20+, browsers, Deno, Bun; TypeScript types npm install @superrouteca/slocker
SLocker (Swift package) iOS 15+, macOS 12+ https://github.com/Goodneo-Solutions/slocker-releases
slocker (pub.dev) Dart 3.5+ and Flutter dart pub add slocker

Every package of a release carries the same version and covers the same operations.

What it covers

The partner locker delivery API of Superroute (/api/v1/third-party-delivery/…), 40 operations, each a method:

  • Delivery tasks — create (idempotent), list, read, cancel, rotate a code.
  • At the cabinet — open the doors remotely with the delivery code, confirm (all or a partial delivery), abort, reassign a package to another compartment, cancel the placement, reopen within the 60-second correction window, open for pickup.
  • Scopes and locations — the grants and sub-scopes, create / update / disable a sub-scope, the locations of a scope, the locations near an address or a point.
  • Usage — summary, daily, by location.
  • Carriers — the carrier profile, intake locations and receiving warehouses, pre-advising packages into a warehouse, the carrier's packages, cancelling, rerouting, and resetting a pickup code.
  • Mock cabinet — create, list and delete mock lockers, force doors open or closed, take a cabinet offline, change its features and compartment sizes, seed parcels, clear them, or declare them taken.

The operations are generated from Superroute's published OpenAPI document (specs/partner-locker-api.json). The same operations exist in Superroute's GraphQL API (specs/partner-locker-graphql.json lists them); sLocker calls REST.

Signing in and choosing the provider

The API takes the bearer token of a Superroute API account (POST /api/v1/user/login; a token lasts a week). Pass a token you already have, or sign in with the client, which keeps the token for the following calls:

import { SLockerClient } from '@superrouteca/slocker';

const sl = new SLockerClient({ providerId: 4611 });          // baseUrl defaults to https://api.superroute.ca
await sl.login({ email: 'api-partner@example.com', password: process.env.SR_PASSWORD });
// later: await sl.refreshToken();  await sl.rotateToken({ gracePeriodMinutes: 60 });  await sl.logout();

providerId is your third-party delivery provider (sent as X-Third-Party-Provider-Id). It may be left out when the account acts for exactly one provider; any call can name another one (providerId in its parameters).

A delivery, end to end

  1. Create the task with the order's own idempotency key. The answer carries delivery_no, the driver's access_code and the recipient's pickup_code. A repeat with the same key and the same content answers the original task (replayed: true); the same key with different content is 409 IDEMPOTENCY_CONFLICT.
  2. At the cabinet, the driver types the code on the screen — or your app opens the doors remotely with openPartnerLockerDelivery, which answers a claim_token and the compartments that opened.
  3. Confirm with the claim token (confirm_all, or accept_partial when some packages did not fit). For a remote open the cabinet settles the deposit by itself once every opened door reports closed; an explicit confirm afterwards answers the same result.
  4. Webhooks tell you the rest: partner_locker.delivery.doors_opened, .delivered (with the pickup code and the compartments), partner_locker.pickup.completed, .failed, .expired, .cancelled, .correction_reopened.
const created = await sl.api.createPartnerLockerDelivery({
  idempotencyKey: 'FOOD-20260714-1001-v1',                    // your own, stable for this order
  body: {
    external_order_id: 'FOOD-20260714-1001',
    platform_scope_group_id: 12,
    recipient: { phone: '+14165550123' },
    package_count: 2,
  },
});
const { delivery_no, access_code } = created.data;

const opened = await sl.api.openPartnerLockerDelivery({ body: { location_id: 101, access_code } });
const { claim_token, attempt } = opened.data;
if (attempt.can_confirm) {
  await sl.api.confirmPartnerLockerDelivery({ body: { claim_token, decision: 'confirm_all' } });
}
const task = await sl.api.partnerLockerDelivery({ deliveryNo: delivery_no });

The same in Python:

from slocker import SLockerClient, SLockerError

sl = SLockerClient(provider_id=4611)
sl.login('api-partner@example.com', password)
created = sl.create_partner_locker_delivery(
    idempotency_key='FOOD-20260714-1001-v1',
    body={'external_order_id': 'FOOD-20260714-1001', 'platform_scope_group_id': 12,
          'recipient': {'phone': '+14165550123'}, 'package_count': 2})
try:
    opened = sl.open_partner_locker_delivery(body={'location_id': 101, 'access_code': created['data']['access_code']})
    sl.confirm_partner_locker_delivery(body={'claim_token': opened['data']['claim_token'], 'decision': 'confirm_all'})
except SLockerError as e:
    if e.code == 'LOCKER_DELIVERY_IN_USE':
        ...  # another attempt is in progress: wait, or use its claim token
    raise

The sandbox: a mock cabinet

A mock cabinet runs the same pipeline as a real one without hardware; a task created with mock: true only uses mock cabinets, and its webhooks carry livemode: false, an event_id starting PLE-MOCK- and the header X-Webhook-Test: 1 (sent to your sandbox URL when one is set). Up to 5 mock cabinets per provider, 1–60 compartments each.

locker = sl.create_partner_locker_mock_locker(body={'location_id': 101, 'name': 'Dev cabinet', 'compartments': 8})
device_id = locker['data']['locker']['device_id']

task = sl.create_partner_locker_delivery(idempotency_key='TEST-1', body={
    'external_order_id': 'TEST-1', 'platform_scope_group_id': 12, 'recipient': {'email': 'qa@example.com'}, 'mock': True})
opened = sl.open_partner_locker_delivery(body={'location_id': 101, 'access_code': task['data']['access_code']})
sl.confirm_partner_locker_delivery(body={'claim_token': opened['data']['claim_token'], 'decision': 'confirm_all'})
# → webhook partner_locker.delivery.delivered, livemode false

sl.mark_partner_locker_mock_parcels_taken(device_id, body={'compartments': [1]})
# → webhook partner_locker.pickup.completed (a full pickup settlement)

sl.clear_partner_locker_mock_parcels(device_id)              # empties the cabinet without a pickup settlement
sl.set_partner_locker_mock_door(device_id, body={'compartment': 2, 'action': 'open'})   # a door left open
sl.set_partner_locker_mock_compartments(device_id, body={'compartments': [1, 2], 'grid_length_cm': 20,
    'grid_width_cm': 20, 'grid_height_cm': 20, 'weight_limit_kg': 5})                # rehearse "does not fit"
sl.delete_partner_locker_mock_locker(device_id)

Use the location list in the same mode as the task (mock: true on partnerLockerScopeLocations / partnerLockerNearbyLocations): a live location for a mock task answers 409 LOCKER_NO_CABINET_FOR_MODE.

Webhooks

Verify every webhook against the raw body exactly as received, before parsing it. sLocker checks the timestamp (±5 minutes by default), the X-Webhook-Content-Digest and the X-Webhook-Signature (v1= base64 HMAC-SHA256 of {timestamp}\n{X-Webhook-Delivery-Id}\n{body} with your endpoint's signing secret), and answers the event. During a key rotation the previous secret stays valid for 24 hours: pass both. An endpoint switched to Standard Webhooks (webhook-id / webhook-timestamp / webhook-signature) or to CloudEvents structured bodies is verified too. The same verification covers the carrier channel's carrier_package.distribution.* events.

Deduplicate on eventId (stable across retries; deliveryId changes on every attempt), answer 2xx within 10 seconds, and keep sandbox events (event.test) apart from production.

import { verifyWebhook, WebhookVerificationError } from '@superrouteca/slocker';

app.post('/webhooks/slocker', express.raw({ type: '*/*' }), async (req, res) => {
  try {
    const event = await verifyWebhook(req.body, req.headers, [process.env.SLOCKER_SECRET, process.env.SLOCKER_OLD_SECRET]);
    if (!event.test) await queue.add(event.eventId, event);
    res.sendStatus(204);
  } catch (e) {
    if (e instanceof WebhookVerificationError) return res.status(400).send(e.reason);
    throw e;
  }
});
Language Verify Sign a test request
Python slocker.verify(body, headers, secrets) slocker.sign(secret, body)
PHP Webhooks::verify(file_get_contents('php://input'), getallheaders(), $secrets) Webhooks::sign($secret, $body)
Java Webhooks.verify(body, headers, secrets) Webhooks.sign(...)
Go slocker.Verify(body, slocker.HeaderMap(r.Header), secrets, nil) slocker.Sign(secret, body, slocker.SignOptions{})
C# Webhooks.Verify(body, headers, secrets) Webhooks.Sign(secret, body)
JavaScript await verifyWebhook(body, headers, secrets) await signWebhook(secret, body)
Swift try Webhooks.verify(body:headers:secrets:) Webhooks.sign(secret:body:)
Dart verifyWebhook(bodyBytes, headers, secrets) signWebhook(secret, bodyBytes)

A refused webhook raises an error whose reason is one of MISSING_HEADER, INVALID_TIMESTAMP, TIMESTAMP_OUT_OF_TOLERANCE, DIGEST_MISMATCH, SIGNATURE_MISMATCH or INVALID_PAYLOAD.

Errors and retries

Every failed call raises one error type with the HTTP status, the server's stable code, its message, the field errors and retryAfter (seconds). Act on the status and the code, never on the message. Codes the SDK adds: UNAUTHENTICATED (401 without a code: sign in again), HTTP_<status> (an answer without a code), NETWORK_ERROR (no answer; status 0) and INVALID_ARGUMENT (a missing path parameter or an unknown operation; nothing was sent).

Only repeatable calls are retried: reads, deletes, and the two creates that carry an idempotency key (createPartnerLockerDelivery, submitCarrierPackages). They are repeated after a timeout, a lost connection, 408, 425, 429 or 5xx — at most maxRetries times (2 by default), waiting what Retry-After asks (at most 60 seconds) or an exponential backoff with jitter. Opening doors, confirming, aborting, reassigning and every other change are never repeated by the SDK: after a timeout, read the task (partnerLockerDelivery) before deciding.

When the SDK makes the idempotency key itself it is kept across its own retries only; give your own key (one per order, unchanged on every retry of yours) so that a retry after a crash finds the original task.

Each language

Python

from slocker import SLockerClient
sl = SLockerClient(token, provider_id=4611)                      # sync; urllib from the standard library
sl.partner_locker_deliveries(status='delivered', per_page=50)
sl.call('partnerLockerDeliveries', {'status': 'delivered'})       # any operation by name

PHP

use Superroute\SLocker\Client;
$sl = new Client($token, providerId: 4611);                       // curl
$sl->partnerLockerDeliveries(status: 'delivered', perPage: 50);

Java and Kotlin

Add the Maven repository https://goodneo-solutions.github.io/slocker-releases/maven, then implementation("ca.superroute:slocker:0.1.0") — or, in an Android app, ca.superroute:slocker-android:0.1.0.

Client sl = Client.builder().token(token).providerId(4611L).build();   // java.net.http, or HttpURLConnection on Android
Object page = sl.partnerLockerDeliveries(Params.create().put("status", "delivered").put("per_page", 50));
val sl = SLockerAsync(Client.builder().token(token).providerId(4611L).transport(OkHttpTransport()).build())
val page = sl.api.partnerLockerDeliveries(status = "delivered", perPage = 50)   // suspend, on Dispatchers.IO

Answers are plain JSON values (Map, List, String, Long, Double, Boolean, null).

Go

sl := slocker.New(slocker.Config{Token: token, ProviderID: 4611})
page, err := sl.PartnerLockerDeliveries(ctx, slocker.Params{"status": "delivered", "per_page": 50})
var e *slocker.Error
if errors.As(err, &e) && e.Code == "LOCKER_RATE_LIMITED" { /* … */ }

C#

var sl = new SLockerClient(new SLockerOptions { Token = token, ProviderId = 4611 });
var page = await sl.PartnerLockerDeliveriesAsync(status: "delivered", perPage: 50);

Swift

let sl = SLockerClient(token: token, providerId: 4611)
let page = try await sl.partnerLockerDeliveries(status: "delivered", perPage: 50)

Dart and Flutter

final sl = SLockerClient(token: token, providerId: 4611);
final page = await sl.partnerLockerDeliveries(status: 'delivered', perPage: 50);

Every client takes its own transport (for tests, proxies or an HTTP stack the app already has), a base URL (white-label API hosts and test environments), the retry count and the wait function.

Development

specs/                       the partner locker slice of Superroute's REST and GraphQL documents, and operations.json
tools/sync_specs.py          copies the slice from a Superroute checkout (and checks no route lacks its Swagger entry)
tools/generate.py            writes every language's operation table and typed methods from the spec
tools/make_client_cases.py   writes tests/cases/client.json: requests, answers, retries, sign-in
tools/make_webhook_vectors.py writes tests/cases/webhooks.json, signed exactly as Superroute signs
tests/cases/                 the cases every language runs
python3 -I tools/sync_specs.py ../Superroute       # then commit specs/ and the generated files
cd python && python3 -m unittest discover -s tests
cd js && npm test
cd php && php tests/run.php
cd go && go test ./...
cd java && ./gradlew :slocker:test :slocker-android:test
dotnet test dotnet/tests/Superroute.SLocker.Tests
cd swift && swift test
cd dart && dart pub get && dart test

Never edit generated files (generated/, Generated/, _operations.py, operations_gen.go, SLockerOperations.kt): change Superroute's spec, sync, and generate again.

Releases

sLocker started at 0.1.0; each release adds 0.0.1 to the last one, skipping every version that contains the digit 3 or 4 (0.1.0, 0.1.1, 0.1.2, 0.1.5, 0.1.6, …, 0.1.9, 0.2.0, …). Tagging a version (0.1.1, no v) runs .github/workflows/release.yml, which tests every language and then publishes:

  • npm (@superrouteca/slocker), PyPI (superroute-slocker), NuGet (Superroute.SLocker) and pub.dev (slocker) directly, by trusted publishing;
  • to the public repository Goodneo-Solutions/slocker-releases: the Swift package (a prebuilt XCFramework attached to that repository's GitHub Release of the version), the Maven repository (maven/, served by GitHub Pages) with ca.superroute:slocker and slocker-android, and the Go module (go/, tag go/v<version>);
  • to the public repository Goodneo-Solutions/slocker-php: the PHP package at its root, tag v<version>, which Packagist reads.

This source repository stays private; the packages are public under the Superroute SDK License (see LICENSE).

Metadata

Release files for superroute-slocker 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for superroute-slocker 0.1.0
File Size Uploaded Provenance Actions
superroute_slocker-0.1.0.tar.gz 26.3 kB With provenance Details for superroute_slocker-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for superroute-slocker 0.1.0
File Interpreter ABI Platform Actions
superroute_slocker-0.1.0-py3-none-any.whl Python 3 none any Details for superroute_slocker-0.1.0-py3-none-any.whl

Total release size: 45.7 kB

Release files / superroute_slocker-0.1.0.tar.gz

Download URL superroute_slocker-0.1.0.tar.gz
Size 26.3 kB
Tags Source
SHA-256 checksum
How to use checksums
da09abc7713fd05eb6de07bad05dfc08979778571313ea0a50fb809aceb44e67
BLAKE2b-256 checksum
How to use checksums
6321cc0fa4c24894ab6985dc251e452d567f2a36b3fc3d300ed851d79787b8f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

Release files / superroute_slocker-0.1.0-py3-none-any.whl

Download URL superroute_slocker-0.1.0-py3-none-any.whl
Size 19.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
50ee45f1ceb7d0cb2da2f117b4b7ba88d522d2ab426c6570ad36ef3d15b9d9e9
BLAKE2b-256 checksum
How to use checksums
9d114b4c7839dcad7b5b4acc4e2b502264de72791a50477b169042eeeebfe482
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

Security for superroute-slocker 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for superroute_slocker-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page