Developers

Webhooks

Markdown

App uninstall & data deletion

Installation webhooks for uninstall, scope revocation and suspension, privacy webhooks for store and customer data deletion, and how to send the data-deletion acknowledgement.

Uninstall & privacy webhook URL

Separately from store webhooks, LARM sends installation webhooks and privacy webhooks to the lifecycle_privacy_url registered in your app version. The URL is registered when the version is approved.

The signing key for these webhooks is generated on the Access tab of the Developer Portal with the "Generate signing key" button, and is shown once. The previous key stays valid for 24 hours. Next to it is the endpoint ID, which is also sent with every webhook in the x-larm-endpoint-id header.

Signature, headers and retries work like store webhooks, with the additional x-larm-endpoint-id header.

Installation webhooks

JSON
{
  "id": "e0c1…",
  "type": "app.uninstalled",
  "payload_version": 1,
  "store_id": "5b1d7a62-…",
  "app_id": "1f2e3d4c-…",
  "installation_id": "c7e4a1f2-…",
  "sequence": 7,
  "data": { "event_type": "app.uninstalled", "app_auth_epoch": 2, "grant_revision": 3, "revoked_scopes": ["orders:read", "customers:read"], "data_deletion_requested": true },
  "occurred_at": "2026-10-15T11:40:00Z"
}
typeWhenWhat to do
app.uninstalledThe merchant uninstalled the app. Tokens and webhook subscriptions were revokedStop all work for this store and delete its tokens. Store data is deleted on shop.redact
scopes.revokedThe merchant revoked optional scopes, along with the scopes that depend on themStop using them. The related endpoints return 403
app.suspendedLARM suspended the appStop sending requests until app.resumed
app.resumedThe app was resumedResync from the API before relying on the data you hold
reconciliation_requiredWebhooks were lost before deliveryRun a full resync for the store

sequence increases per installation. If you receive a number lower than one you have already processed, the webhook is stale.

Privacy webhooks

JSON
{
  "id": "a9b8…",
  "type": "shop.redact",
  "payload_version": 1,
  "privacy_request_id": "6d5c…",
  "store_id": "5b1d7a62-…",
  "app_id": "1f2e3d4c-…",
  "installation_id": "c7e4a1f2-…",
  "subject_ref": null,
  "occurred_at": "2026-10-17T11:40:00Z"
}
typeWhenRequired actionDeadline
shop.redact48 hours after uninstall, or when the store is deletedDelete all data you hold for this store30 days
customers.data_requestA customer requested a copy of their dataSend the merchant what you hold about subject_ref30 days
customers.redactA customer requested deletion of their dataDelete what you hold about subject_ref30 days

Send the data-deletion acknowledgement

A request is not closed until you acknowledge it. After deleting the data, send:

HTTP
POST https://larm.sa/apps/privacy/requests/{privacy_request_id}/ack
Content-Type: application/json
x-larm-endpoint-id: {from the webhook header}
x-larm-event-id: {the webhook id}
x-larm-timestamp: {current Unix seconds}
x-larm-nonce: {43 random base64url characters}
x-larm-ack-signature: {hex}

The body has exactly these keys, sorted alphabetically:

JSON
{"completed_at":"2026-10-18T08:00:00.000Z","event_id":"a9b8…","evidence_digest":"<64 hex>","installation_id":"c7e4a1f2-…","result_code":"DELETED","subject_ref":null}
FieldValue
result_codeDELETED, NO_DATA or REQUIRES_REVIEW
evidence_digestHex SHA-256 of your deletion record; keep the record so you can prove the deletion later

The signature is hex HMAC-SHA256, keyed with the same uninstall & privacy signing key (base64url-decoded), over canonical JSON with sorted keys:

Node.js
import crypto from 'node:crypto'

const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(',')}]`
  : value && typeof value === 'object' ? `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`
  : JSON.stringify(value)

function signAck({ requestId, eventId, endpointId, body, key }) {
  const timestamp = String(Math.floor(Date.now() / 1000))
  const nonce = crypto.randomBytes(32).toString('base64url')
  const basis = canonical({
    domain: 'larm-privacy-ack-v1', method: 'POST', path: `/privacy/requests/${requestId}/ack`,
    requestId, eventId, endpointId, timestamp, nonce,
    digest: crypto.createHash('sha256').update(body).digest('hex'),
  })
  return {
    'x-larm-endpoint-id': endpointId, 'x-larm-event-id': eventId, 'x-larm-timestamp': timestamp, 'x-larm-nonce': nonce,
    'x-larm-ack-signature': crypto.createHmac('sha256', key).update(basis).digest('hex'),
  }
}
  • The signed path is /privacy/requests/{id}/ack, without the /apps prefix, even though the request is sent to /apps/privacy/….
  • The timestamp must be within 35 seconds of LARM's clock, and a nonce cannot be reused (REPLAY_DETECTED).
  • Sending the same acknowledgement again returns the same receipt; a different acknowledgement for the same request returns 409 ACK_CONFLICT.
  • Response: { request_id, event_id, acknowledgement_hash }. Store it.

    ↑↓ moveEnter openEsc close