Farm events pushed to a device or system of yours as they happen, signed and retried.
A sensor sends readings in. A device that acts, an irrigation controller, a valve, an alarm or a system of your own, needs the events coming out. A webhook posts each farm event to an HTTPS endpoint that device or its controller exposes, signed, with retries. This guide shows you how to set one up. You need the Manager role or higher on the farm and an HTTPS endpoint.Create a webhook under Settings → Webhooks in the app.
How Delivery Works
Each stored event is posted to your endpoint as a JSON body with a signature header; a 2xx response counts as delivered, and anything else, or a timeout, is retried on the schedule below.
Events
Event
Fires when
metric.value
A sensor reading is stored
device.offline
A device stops reporting and is marked offline
trigger.fired
One of your automation triggers fires
recommendation.created
An agronomist writes a recommendation
metric.value is high volume. Ten sensors reporting every 15 minutes is around a thousand events a day. For a dashboard that refreshes on a timer, poll the API instead.
Receive The Request
Each delivery is a POST to your endpoint with a JSON body.
POST https://your-endpoint.example.com/agrihubContent-Type: application/jsonUser-Agent: AgriHub360-Webhooks/1.0X-Agrihub-Signature: t=1784369411172,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08X-Agrihub-Delivery: 4d1b2c3e-...
Return any 2xx to acknowledge. The response body isn’t inspected.
Verify The Signature
X-Agrihub-Signature is t=<epoch-ms>,v1=<hex>. v1 is the HMAC-SHA256 of "<t>.<raw-body>", keyed with your signing secret.
Sign the raw request body, not a re-serialised object, or the signature won’t match. Reject any t older than a few minutes, or a captured delivery can be replayed.
import { createHmac, timingSafeEqual } from 'node:crypto'export function verify(rawBody, header, secret, toleranceMs = 5 * 60 * 1000) { const parts = Object.fromEntries( header.split(',').map((p) => { const i = p.indexOf('=') return [p.slice(0, i).trim(), p.slice(i + 1).trim()] }), ) const t = Number(parts.t) if (!Number.isFinite(t) || !parts.v1) return false if (Math.abs(Date.now() - t) > toleranceMs) return false const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex') const a = Buffer.from(expected) const b = Buffer.from(parts.v1) // Constant time: a normal === leaks the correct prefix byte by byte. return a.length === b.length && timingSafeEqual(a, b)}
import express from 'express'const app = express()// express.json() would consume the stream and leave you unable to verify.app.post('/agrihub', express.raw({ type: 'application/json' }), (req, res) => { const raw = req.body.toString('utf8') if (!verify(raw, req.get('X-Agrihub-Signature') ?? '', process.env.WEBHOOK_SECRET)) { return res.status(401).send('bad signature') } const event = JSON.parse(raw) // Acknowledge FIRST, then do the work. We time out after 10 seconds and // retry, so slow processing turns into duplicate deliveries. res.status(200).json({ received: true }) void handle(event)})app.listen(3000)
import hashlib, hmac, json, os, timefrom flask import Flask, request, abortSECRET = os.environ["WEBHOOK_SECRET"].encode()app = Flask(__name__)def verify(raw: bytes, header: str, tolerance_s: int = 300) -> bool: try: parts = dict(p.strip().split("=", 1) for p in header.split(",")) t = int(parts["t"]) v1 = parts["v1"] except (ValueError, KeyError): return False if abs(time.time() * 1000 - t) > tolerance_s * 1000: return False expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1)@app.post("/agrihub")def receive(): # request.get_data() is the raw body; request.json would re-serialise. raw = request.get_data() if not verify(raw, request.headers.get("X-Agrihub-Signature", "")): abort(401) event = json.loads(raw) print(event["type"], event["id"]) return {"received": True}, 200
Retries
Delivery times out after 10 seconds. Acknowledge first, then process.
Failed deliveries retry at 30s, 2m, 10m, 30m, 2h, 6h, 12h: 8 attempts over about 21 hours.
5xx, 408, 429, timeouts and connection failures are retried. Other 4xx aren’t.
After 5 consecutive failed chains the webhook is disabled. Re-enable it in settings. That resets the counter.
The delivery log keeps every attempt, with response status and body, for 30 days.
Deliveries are at-least-once and can arrive concurrently. Deduplicate on the event id. Sort on occurred_at_ms if order matters.
Security
Endpoints must be HTTPS and can’t point at localhost, private ranges or link-local addresses.
Redirects aren’t followed. A 302 is a failed delivery.
To rotate a secret, delete the endpoint and create it again.
The signing secret can be re-read in settings. A device API key can’t.
That’s it. You have signed events arriving at your endpoint. Next: AgriHub360 Sensor.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"AgriHub360","description":"AgriHub360 documentation: what each feature does on the farm, the Sensor and Hub, the Device API, and the science behind every model.1. AgriHub360 documentation: what each feature does on the farm, the Sensor and Hub, the Device API, and the science behind every model.","colors":{"primary":"#146134","light":"#1F9A4D","dark":"#146134"},"logo":{"light":"https://mintcdn.com/agrihub360/wfAnTqlaZU94FIHH/logo/light.svg?fit=max\u0026auto=format\u0026n=wfAnTqlaZU94FIHH\u0026q=85\u0026s=cef992e338466069816ce0a6ae768d72","dark":"https://mintcdn.com/agrihub360/wfAnTqlaZU94FIHH/logo/dark.svg?fit=max\u0026auto=format\u0026n=wfAnTqlaZU94FIHH\u0026q=85\u0026s=9d9363bc6480f989f05d66d92458fc0e","href":"https://www.agrihub360.com"},"favicon":"https://mintcdn.com/agrihub360/wfAnTqlaZU94FIHH/favicon.svg?fit=max\u0026auto=format\u0026n=wfAnTqlaZU94FIHH\u0026q=85\u0026s=728cf3555768d236c7e49d6844c48c97","navbar":{"links":[{"href":"https://www.agrihub360.com/support","label":"Support"}],"primary":{"type":"button","label":"Open the app","href":"https://app.agrihub360.com/login"}},"fonts":{"family":"Inter"},"contextual":{"options":["copy","view"],"display":"header"},"styling":{"latex":true},"navigation":{"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"agrihub360","hasAudioNarration":false,"actualSubdomain":"agrihub360","customDomains":["docs.agrihub360.com"],"gitSource":{"type":"github","owner":"ereynalabs","repo":"agrihub360-docs","deployBranch":"main","contentDirectory":"","isPrivate":false,"mountPath":"","label":""},"entitlements":{"AI_CHAT":{"status":"DISABLED"},"WEB_MCP":{"status":"DISABLED"},"NARRATION":{"status":"DISABLED"}},"buildId":"6ab144efa6f4f373365ca742","clientVersion":"0.0.4142","searchFilterCounts":[{"language":"default","tag":"Guides","count":102},{"language":"default","tag":"How Do I","count":42},{"language":"default","tag":"Devices","count":12},{"language":"default","tag":"Field Monitoring","count":11},{"language":"default","tag":"Farm Management","count":10},{"language":"default","tag":"Models","count":10},{"language":"default","tag":"Decision Windows","count":9},{"language":"default","tag":"Map","count":9},{"language":"default","tag":"Compliance \u0026 Records","count":8},{"language":"default","tag":"API Reference","count":7}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":false,"enableContextualStarterQuestions":false,"enableAskAiSearchBar":true},"basePath":"","hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-rbac-roles","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"rbac-expansion","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-metadata","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-serve","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"live-agents","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[{"filePath":"style.css","content":"#content-area p,#content-area li,.prose p,.prose li{text-align:justify;text-justify:inter-word;hyphens:auto;-webkit-hyphens:auto}#content-area figcaption,#content-area table p,.prose figcaption,.prose table p{text-align:left}#content-area img,.prose img{display:block;margin-left:auto;margin-right:auto}#content-area .mermaid,#content-area .mermaid svg,.prose .mermaid,.prose .mermaid svg{display:block;margin-left:auto;margin-right:auto}#content-area figure,.prose figure{margin-top:2rem;margin-bottom:2.5rem}#content-area figure figcaption,.prose figure figcaption{margin-top:0.75rem}#content-area .refs,.prose .refs{font-size:0.8rem;line-height:1.5;color:var(--gray-600, #4b5563)}#content-area .refs p,#content-area .refs li,.prose .refs p,.prose .refs li{font-size:inherit;text-align:left;margin-top:0.25rem;margin-bottom:0.25rem}#content-area .refs a,.prose .refs a{font-weight:500}img[src^=\"/images/screens/\"]{border:9px solid #1A1714;border-radius:30px;box-shadow:0 10px 30px rgba(26, 23, 20, 0.18), 0 0 0 1px rgba(26, 23, 20, 0.35);background:#1A1714}"}],"page":{"pagePath":"devices/webhooks","slug":"/devices/webhooks","pageMetadata":{"title":"Webhooks","description":"Farm events pushed to a device or system of yours as they happen, signed and retried.","href":"/devices/webhooks"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"Farm events pushed to a device or system of yours as they happen, signed and retried.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,li:`li`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{CodeBlock,CodeGroup,Heading,Note,Table,Warning}=_components;if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!CodeGroup)_missingMdxReference(`CodeGroup`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Note)_missingMdxReference(`Note`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsx(_components.p,{children:`A sensor sends readings in. A device that acts, an irrigation controller, a valve, an alarm or a system of your own, needs the events coming out. A webhook posts each farm event to an HTTPS endpoint that device or its controller exposes, signed, with retries. This guide shows you how to set one up. You need the Manager role or higher on the farm and an HTTPS endpoint.`}),`\n`,_jsxs(_components.p,{children:[`Create a webhook under `,_jsx(_components.strong,{children:`Settings → Webhooks`}),` in the app.`]}),`\n`,_jsx(Heading,{level:`2`,id:`how-delivery-works`,children:`How Delivery Works`}),`\n`,_jsxs(_components.p,{children:[`Each stored event is posted to your endpoint as a JSON body with a signature header; a `,_jsx(_components.code,{children:`2xx`}),` response counts as delivered, and anything else, or a timeout, is retried on the schedule below.`]}),`\n`,_jsx(Heading,{level:`2`,id:`events`,children:`Events`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Event`}),_jsx(_components.th,{children:`Fires when`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`metric.value`})}),_jsx(_components.td,{children:`A sensor reading is stored`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`device.offline`})}),_jsx(_components.td,{children:`A device stops reporting and is marked offline`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`trigger.fired`})}),_jsx(_components.td,{children:`One of your automation triggers fires`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`recommendation.created`})}),_jsx(_components.td,{children:`An agronomist writes a recommendation`})]})]})]}),`\n`,_jsx(Note,{children:_jsxs(_components.p,{children:[_jsx(_components.code,{children:`metric.value`}),` is high volume. Ten sensors reporting every 15 minutes is around a thousand events a day. For a dashboard that refreshes on a timer, poll the API instead.`]})}),`\n`,_jsx(Heading,{level:`2`,id:`receive-the-request`,children:`Receive The Request`}),`\n`,_jsxs(_components.p,{children:[`Each delivery is a `,_jsx(_components.code,{children:`POST`}),` to your endpoint with a JSON body.`]}),`\n`,_jsx(CodeBlock,{numberOfLines:`6`,language:`text`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`text`,children:_jsxs(_components.code,{language:`text`,numberOfLines:`6`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`POST https://your-endpoint.example.com/agrihub`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`Content-Type: application/json`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`User-Agent: AgriHub360-Webhooks/1.0`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`X-Agrihub-Signature: t=1784369411172,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`X-Agrihub-Delivery: 4d1b2c3e-...`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{})})]})})}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`12`,language:`json`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`json`,children:_jsxs(_components.code,{language:`json`,numberOfLines:`12`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`{`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"id\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"evt_6e146482-2f18-4927-8a1c-9d0b7e4f3a21\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"type\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"metric.value\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"farm_id\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"22222222-2222-2222-2222-222222222201\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"occurred_at_ms\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#B5CEA8`},children:`1784369411000`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"data\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: {`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"device_id\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"77777777-7777-7777-7777-777777777703\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"metric_id\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#B5CEA8`},children:`220`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"value\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"37.4\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#9CDCFE`},children:` \"timestamp\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#B5CEA8`},children:`1784369411000`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` }`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`}`})}),`\n`]})})}),`\n`,_jsxs(_components.p,{children:[`Return any `,_jsx(_components.code,{children:`2xx`}),` to acknowledge. The response body isn’t inspected.`]}),`\n`,_jsx(Heading,{level:`2`,id:`verify-the-signature`,children:`Verify The Signature`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`X-Agrihub-Signature`}),` is `,_jsx(_components.code,{children:`t=\u003cepoch-ms>,v1=\u003chex>`}),`. `,_jsx(_components.code,{children:`v1`}),` is the HMAC-SHA256 of `,_jsx(_components.code,{children:`\"\u003ct>.\u003craw-body>\"`}),`, keyed with your signing secret.`]}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[`Sign the raw request body, not a re-serialised object, or the signature won’t match. Reject any `,_jsx(_components.code,{children:`t`}),` older than a few minutes, or a captured delivery can be replayed.`]})}),`\n`,_jsxs(CodeGroup,{children:[_jsx(CodeBlock,{filename:`Node`,numberOfLines:`19`,language:`javascript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`javascript`,children:_jsxs(_components.code,{language:`javascript`,numberOfLines:`19`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`import`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` { createHmac, timingSafeEqual } `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`from`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'node:crypto'`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`export`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` function`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` verify(rawBody, header, secret, toleranceMs = 5 * 60 * 1000) {`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` parts = Object.fromEntries(`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` header.split(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`','`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`).map((p) => {`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` i = p.indexOf(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'='`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` [p.slice(0, i).trim(), p.slice(i + 1).trim()]`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` }),`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` )`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` t = Number(parts.t)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` if`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` (!Number.isFinite(t) || !parts.v1) `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`return`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` false`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` if`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` (Math.abs(Date.now() - t) > toleranceMs) `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`return`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` false`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` expected = createHmac(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'sha256'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, secret).update(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:\"`${t}.${rawBody}`\"}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`).digest(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'hex'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` a = Buffer.`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`from`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(expected)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` b = Buffer.`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`from`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(parts.v1)`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` // Constant time: a normal === leaks the correct prefix byte by byte.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` a.length === b.length \u0026\u0026 timingSafeEqual(a, b)`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`}`})}),`\n`]})})}),_jsx(CodeBlock,{filename:`Express`,numberOfLines:`19`,language:`javascript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`javascript`,children:_jsxs(_components.code,{language:`javascript`,numberOfLines:`19`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`import`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` express `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`from`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'express'`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` app = express()`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`// express.json() would consume the stream and leave you unable to verify.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`app.post(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'/agrihub'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, express.raw({ type: `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'application/json'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` }), (req, res) => {`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` raw = req.body.toString(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'utf8'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` if`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` (!verify(raw, req.get(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'X-Agrihub-Signature'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`) ?? `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`''`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, process.env.WEBHOOK_SECRET)) {`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` res.status(401).send(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`'bad signature'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` }`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` const`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` event = JSON.parse(raw)`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` // Acknowledge FIRST, then do the work. We time out after 10 seconds and`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` // retry, so slow processing turns into duplicate deliveries.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` res.status(200).json({ received: `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`true`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` })`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` void`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` handle(event)`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`})`})}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`app.listen(3000)`})}),`\n`]})})}),_jsx(CodeBlock,{filename:`Python`,numberOfLines:`27`,language:`python`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`python`,children:_jsxs(_components.code,{language:`python`,numberOfLines:`27`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`import`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` hashlib, hmac, json, os, time`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`from`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` flask `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`import`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` Flask, request, abort`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`SECRET = os.environ[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"WEBHOOK_SECRET\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`].encode()`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`app = Flask(__name__)`})}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`def`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` verify(raw: bytes, header: str, tolerance_s: int = 300) -> bool:`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` try`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`:`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` parts = dict(p.strip().split(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"=\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, 1) `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`for`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` p `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`in`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` header.split(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\",\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`))`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` t = int(parts[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"t\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`])`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` v1 = parts[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"v1\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`]`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` except`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` (ValueError, KeyError):`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` False`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` if`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` abs(time.time() * 1000 - t) > tolerance_s * 1000:`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` False`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` expected = hmac.new(SECRET, f`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"{t}.\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.encode() + raw, hashlib.sha256).hexdigest()`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` hmac.compare_digest(expected, v1)`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`@app.post(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"/agrihub\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`def`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` receive():`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` # request.get_data() is the raw body; request.json would re-serialise.`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` raw = request.get_data()`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` if`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` not`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` verify(raw, request.headers.get(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"X-Agrihub-Signature\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)):`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` abort(401)`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` event = json.loads(raw)`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` print(event[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"type\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`], event[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"id\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`])`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` {`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"received\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`: `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`True`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`}, 200`})]}),`\n`]})})})]}),`\n`,_jsx(Heading,{level:`2`,id:`retries`,children:`Retries`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`Delivery times out after `,_jsx(_components.strong,{children:`10 seconds`}),`. Acknowledge first, then process.`]}),`\n`,_jsxs(_components.li,{children:[`Failed deliveries retry at 30s, 2m, 10m, 30m, 2h, 6h, 12h: `,_jsx(_components.strong,{children:`8 attempts over about 21 hours`}),`.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`5xx`}),`, `,_jsx(_components.code,{children:`408`}),`, `,_jsx(_components.code,{children:`429`}),`, timeouts and connection failures are retried. Other `,_jsx(_components.code,{children:`4xx`}),` aren’t.`]}),`\n`,_jsxs(_components.li,{children:[`After `,_jsx(_components.strong,{children:`5 consecutive failed chains`}),` the webhook is disabled. Re-enable it in settings. That resets the counter.`]}),`\n`,_jsxs(_components.li,{children:[`The delivery log keeps every attempt, with response status and body, for `,_jsx(_components.strong,{children:`30 days`}),`.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`Deliveries are at-least-once and can arrive concurrently. Deduplicate on the event `,_jsx(_components.code,{children:`id`}),`. Sort on `,_jsx(_components.code,{children:`occurred_at_ms`}),` if order matters.`]}),`\n`,_jsx(Heading,{level:`2`,id:`security`,children:`Security`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:`Endpoints must be HTTPS and can’t point at localhost, private ranges or link-local addresses.`}),`\n`,_jsxs(_components.li,{children:[`Redirects aren’t followed. A `,_jsx(_components.code,{children:`302`}),` is a failed delivery.`]}),`\n`,_jsx(_components.li,{children:`To rotate a secret, delete the endpoint and create it again.`}),`\n`,_jsx(_components.li,{children:`The signing secret can be re-read in settings. A device API key can’t.`}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`That’s it. You have signed events arriving at your endpoint. Next: `,_jsx(_components.a,{href:`/devices/sensor`,children:`AgriHub360 Sensor`}),`.`]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Webhooks","description":"Farm events pushed to a device or system of yours as they happen, signed and retried.","href":"/devices/webhooks"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"How Delivery Works","slug":"how-delivery-works","depth":2,"children":[]},{"title":"Events","slug":"events","depth":2,"children":[]},{"title":"Receive The Request","slug":"receive-the-request","depth":2,"children":[]},{"title":"Verify The Signature","slug":"verify-the-signature","depth":2,"children":[]},{"title":"Retries","slug":"retries","depth":2,"children":[]},{"title":"Security","slug":"security","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":[],"subdomain":"agrihub360","siteName":"AgriHub360","title":"Webhooks - AgriHub360","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-09-16T15:18:57.776Z","originalFileLocation":"devices/webhooks.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Farm events pushed to a device or system of yours as they happen, signed and retried.\">\u003cmeta data-mint-head name=\"application-name\" content=\"AgriHub360\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Webhooks - AgriHub360\">\u003cmeta data-mint-head property=\"og:description\" content=\"Farm events pushed to a device or system of yours as they happen, signed and retried.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"AgriHub360\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.agrihub360.com/devices/webhooks\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://agrihub360.mintlify.site/_mintlify/api/og/v2?division=Connect+Your+Device\u0026amp;title=Webhooks\u0026amp;description=Farm+events+pushed+to+a+device+or+system+of+yours+as+they+happen%2C+signed+and+retried.\u0026amp;theme=ab602d2acfc87f7680331717\u0026amp;signature=3bb987813836774a27c351de47c9e5762d5533d3eef2c35ce98d72f044fd80bc\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Webhooks - AgriHub360\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Farm events pushed to a device or system of yours as they happen, signed and retried.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://agrihub360.mintlify.site/_mintlify/api/og/v2?division=Connect+Your+Device\u0026amp;title=Webhooks\u0026amp;description=Farm+events+pushed+to+a+device+or+system+of+yours+as+they+happen%2C+signed+and+retried.\u0026amp;theme=ab602d2acfc87f7680331717\u0026amp;signature=3bb987813836774a27c351de47c9e5762d5533d3eef2c35ce98d72f044fd80bc\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/agrihub360/YYgrarxlX8kO7l_F/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"AgriHub360\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#146134\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.agrihub360.com/devices/webhooks\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/devices/webhooks.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.agrihub360.com/#organization\",\"name\":\"AgriHub360\",\"url\":\"https://docs.agrihub360.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/agrihub360/wfAnTqlaZU94FIHH/logo/light.svg?fit=max\u0026auto=format\u0026n=wfAnTqlaZU94FIHH\u0026q=85\u0026s=cef992e338466069816ce0a6ae768d72\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.agrihub360.com#website\",\"name\":\"AgriHub360\",\"url\":\"https://docs.agrihub360.com\",\"publisher\":{\"@id\":\"https://docs.agrihub360.com/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.agrihub360.com/devices/webhooks#webpage\",\"url\":\"https://docs.agrihub360.com/devices/webhooks\",\"name\":\"Webhooks\",\"description\":\"Farm events pushed to a device or system of yours as they happen, signed and retried.\",\"dateModified\":\"2026-09-16T15:18:57.776Z\",\"isPartOf\":{\"@id\":\"https://docs.agrihub360.com#website\"},\"breadcrumb\":{\"@id\":\"https://docs.agrihub360.com/devices/webhooks#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.agrihub360.com/devices/webhooks#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"AgriHub360\",\"item\":\"https://docs.agrihub360.com\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Webhooks\",\"item\":\"https://docs.agrihub360.com/devices/webhooks\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://docs.agrihub360.com/devices/webhooks#article\",\"headline\":\"Webhooks\",\"name\":\"Webhooks\",\"description\":\"Farm events pushed to a device or system of yours as they happen, signed and retried.\",\"url\":\"https://docs.agrihub360.com/devices/webhooks\",\"mainEntityOfPage\":{\"@id\":\"https://docs.agrihub360.com/devices/webhooks#webpage\"},\"dateModified\":\"2026-09-16T15:18:57.776Z\",\"publisher\":{\"@id\":\"https://docs.agrihub360.com/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.agrihub360.com#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"AgriHub360\",\"publisher\":{\"@type\":\"Organization\",\"name\":\"AgriHub360\"},\"provider\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","agentPointerHref":"/devices/webhooks.md","siteName":"AgriHub360","nav":{"global":null,"tabs":[{"tab":"Guides","icon":"book-open","pages":[{"title":"Guides","href":""}]},{"tab":"Devices","icon":"microchip","groups":[{"group":"AgriHub360","pages":[{"title":"Overview","href":"/devices/overview"},{"title":"Sensor","href":"/devices/sensor"},{"title":"Hub","href":"/devices/hub"},{"title":"Open Hardware","href":"/devices/open-hardware"}]},{"group":"Connect Your Sensor","pages":[{"title":"Bring Your Own Sensor","href":"/devices/byod"},{"title":"Your Own Weather Station","href":"/devices/weather-station"},{"title":"Bridge a Vendor API","href":"/devices/bridge-a-vendor-api"},{"title":"Map a LoRaWAN Uplink","href":"/devices/map-a-lorawan-uplink"},{"title":"Metrics","description":"The shared catalogue that says what a reading measures and in which unit.","href":"/devices/metrics"}]},{"group":"Connect Your Device","pages":[{"title":"Webhooks","href":"/devices/webhooks"}]},{"group":"Standards","pages":[{"title":"Device Standards","description":"The radio, data model and hardware standards AgriHub360 devices follow.","href":"/devices/device-standards"},{"title":"API Standards","href":"/devices/api-standards"}]}]},{"tab":"API Reference","icon":"code","pages":[{"title":"API Reference","href":"/quickstart"}]},{"tab":"Models","icon":"chart-line","pages":[{"title":"Models","href":"/models/overview"}]}]},"basePath":"","title":"Webhooks - AgriHub360","buildId":"dpl_G2Qd45vZZ7MkBiGF9yYx8ZsbbKXX","deploymentId":"dpl_G2Qd45vZZ7MkBiGF9yYx8ZsbbKXX","dynamics":[],"chunks":[]}