Inbound middleware
An integration is a middleware: in addition to receiving ShopiMind's lifecycle webhooks and pushing its data via sync, it can expose inbound routes that your own application calls — to trigger an event or push data in real time, without waiting for the next sync cycle.
your application ──(signed call)──► your integration ──(ctx.spm)──► ShopiMindThis is the ideal tool when an event happens on your side (a customer earns points, a review is published, an order is placed in-store) and you want to reflect it immediately in ShopiMind.
Declaring a route — the inbound field
Add an inbound field to your defineIntegration. Each key is an action; each value is a handler that receives the ctx (with ctx.spm already authenticated) and the payload received:
import { SpmEvents } from '@shopimind/integration-kit-js';
export const myIntegration = defineIntegration({
// …
inbound: {
'loyalty.points_earned': async (ctx, payload) => {
const { email, points } = payload as { email: string; points: number };
await SpmEvents.trigger(ctx.spm, 'points_earned', { email, points });
},
'review.created': async (ctx, payload) => {
const review = payload as { email: string; rating: number };
await ctx.customData('reviews').save([
{ email: review.email, rating: review.rating },
]);
},
},
});Each <action> action is exposed by the kit under POST /inbound/{action}. The handler is generic: do whatever you need in it via ctx.spm + SDK statics (SpmEvents.trigger, …), ctx.sendBulk for safe bulk, and ctx.customData(name).save(...) for custom data records.
TIP
ctx.customData(name) resolves the definition declared in provisioning.customData and upserts safely (chunked, transport-safe, surfacing per-record rejections). For path-param bulks, wrap the SDK call with ctx.sendBulk(() => SpmCustomDataRecords.bulkSave(ctx.spm, defId, [/* … */])).
Security: one HMAC signature per installation
Unlike lifecycle webhooks (signed by ShopiMind with one webhook_secret), inbound calls come from your application. The kit therefore assigns each installation its own HMAC secret, exposed under ctx.inboundSecret. Pass it to your application — typically in onActivate:
hooks: {
onActivate: async (ctx) => {
await myApp.registerShopimindEndpoint({
installationId: ctx.installationId, // opaque token, send it back as-is
inboundUrl: `${MY_BASE_URL}/inbound`,
secret: ctx.inboundSecret, // the secret of THIS installation
});
},
}Your application then signs each call, exactly like ShopiMind signs its webhooks, but with this secret and x-integration-* headers:
| Header | Content |
|---|---|
x-integration-installation | The opaque installationId (routes the call and resolves the secret). |
x-integration-timestamp | The timestamp in seconds. |
x-integration-signature | HMAC-SHA256("${timestamp}.${raw_body}", inboundSecret) in hexadecimal. |
x-idempotency-key | (optional) a unique per-call key — see Idempotency. |
// on YOUR application's side
const body = JSON.stringify({ email, points: 50 });
const ts = Math.floor(Date.now() / 1000);
const signature = hmacSha256Hex(`${ts}.${body}`, inboundSecret);
await fetch(`${inboundUrl}/loyalty.points_earned`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-integration-installation': installationId,
'x-integration-timestamp': String(ts),
'x-integration-signature': signature,
'x-idempotency-key': eventId, // optional
},
body,
});The kit verifies the signature over the raw bytes, in constant time, with an anti-replay window (300 s by default). The ShopiMind API token is never exposed to your application: it only knows the installation's inbound secret.
Idempotency
If your call carries an x-idempotency-key header, the kit logs the call and short-circuits any replay of a key already processed successfully (it responds 200 with { "replayed": true }). A call that has not yet succeeded is retried normally. Use a stable key per event (the id of the event on your side) so you can retry without risk of double processing.
Rate-limit
Each installation has a token bucket: beyond the allowed throughput, the kit responds 429 Too Many Requests. Batch your calls or retry after a short delay.
Response codes
| Code | Meaning |
|---|---|
200 | Processed ({ success: true }) — or already-processed replay ({ success: true, replayed: true }). |
400 | Missing installation header, or invalid JSON body. |
401 | Unknown installation, or invalid / too-old signature. |
404 | Unknown action (no matching inbound key). |
429 | Rate-limit reached for this installation. |
500 | The handler failed (you can retry; idempotency deduplicates on success). |
Testing
The kit's test harness signs an inbound call for you:
import { makeTestApp } from '@shopimind/integration-kit-js';
const app = makeTestApp(myIntegration);
// … activate an installation …
const { body, headers } = app.signInbound(installationId, { email: 'a@b.co', points: 50 });
const res = await app.server.inject({ method: 'POST', url: '/inbound/loyalty.points_earned', payload: body, headers });
// res.statusCode === 200Going further
- The integration kit —
ctx.inboundSecret, exposed routes. - Lifecycle & webhooks — the other inbound flow (emitted by ShopiMind).
- Synchronizing data — for batch volumes (initial + incremental).
- Events · Custom data — what your handlers push.