heruAPI

Webhooks

How heru tells you that asynchronous work finished, and how to verify it came from heru.

Building Webhook delivery for the v1 routes is in development. The event bodies below are final, so you can build your handler today.

Register one HTTPS URL per environment. heru sends a POST with a JSON event every time a resource reaches a state you need to know about.

  • Each event has a unique id. Deliveries can repeat, so discard ids you have already processed.
  • Reply 2xx within 10 seconds and do the work asynchronously. Anything else counts as a failed delivery.
  • Failed deliveries are retried with exponential backoff for 24 hours.
  • data carries the full resource, in the same shape as its GET. You do not need a second call.

Events

EventSent when
Fiscal validation
fiscal_validation.completedA fiscal validation finished: with the profile, with a partial result, or with a code when the SAT answered there is no profile.
fiscal_validation.failedA fiscal validation could not finish after retries.
Social security (IMSS)
social_security_validation.completedAn NSS lookup or IMSS registration finished: with the NSS, or with a code when the IMSS answered there is none.
social_security_validation.failedAn NSS lookup or registration could not finish after retries.
Workers
worker.updatedA worker’s consent, e.firma, CSD or status changed.
signature.completedThe worker signed, rejected or let a document expire.
Invoices
invoice.stampedAn invoice was stamped. Batches send one per invoice, so you learn each result without waiting for the whole batch.
invoice.cancelledA cancellation was accepted.
Retention certificates
retention_certificate.stampedA retention certificate was stamped. Batches send one per certificate.
retention_certificate.cancelledThe SAT confirmed the cancellation of a retention certificate.
Batches
batch.doneA batch closed: with status done when every valid document has its outcome, or with status cancelled when it was cancelled after process, and what was pending was never processed and counts in cancelled. Its counts and report are final.
Point of sale
merchant.activatedThe merchant uploaded a valid CSD and can invoice.
merchant.csd_expiring30 days before the merchant’s CSD expires.
charge.invoicedThe customer invoiced a charge.

Examples

One example of each event, as heru delivers it to your URL. The examples use https://api.acme-delivery.example/v1/webhooks/compliance as the URL you registered: an endpoint of your own system.

Example events

Pick an event to see the request heru sends.

type fiscal_validation.completedstatus completedcode null
Request to your URL
POST /v1/webhooks/compliance HTTP/1.1Host: api.acme-delivery.exampleContent-Type: application/jsonHeru-Event-Id: evt_01m4ra8x08eq7vth7a13qb5y8fHeru-Event-Type: fiscal_validation.completedHeru-Signature: t=1760281481,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Body
{  "id": "evt_01m4ra8x08eq7vth7a13qb5y8f",  "type": "fiscal_validation.completed",  "created_at": "2026-10-12T15:04:41.203Z",  "data": {    "id": "fv_01m4ra8vqve0xv4yfe92canf5a",    "external_id": "FV-2026-004413",    "status": "completed",    "message": null,    "pending": [],    "result": {      "curp": "HEGL900101HDFRRS06",      "name": "JOSE LUIS HERNANDEZ GARCIA",      "given_names": "JOSE LUIS",      "first_surname": "HERNANDEZ",      "second_surname": "GARCIA",      "rfc": "HEGL900101K44",      "rfc_valid": true,      "rfc_active": true,      "postal_code": "06600",      "regimes": [        {          "code": "625",          "description": "Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas"        }      ],      "efirma": {        "active": true,        "valid_from": "2024-03-02",        "valid_to": "2028-03-02"      },      "csd": {        "active": false,        "valid_from": null,        "valid_to": null      },      "third_party_invoicing": {        "eligible": true,        "reason": null      },      "validation_url": "https://siat.sat.gob.mx/app/qr/faces/pages/mobile/validadorqr.jsf?D1=10&D2=1&D3=89097221992_HEGL900101K44"    },    "created_at": "2026-10-12T15:04:05.115Z",    "updated_at": "2026-10-12T15:04:41.203Z"  }}

Verify the signature

The Heru-Signature header is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t + "." + raw body, keyed with your webhook secret. Compute it over the raw bytes of the body, before any JSON parsing, and reject events older than five minutes.

import crypto from 'node:crypto';

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

A minimal handler

import express from 'express';
import { verify } from './verify.js';

const app = express();

app.post('/v1/webhooks/compliance', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verify(req.body.toString('utf8'), req.get('Heru-Signature') ?? '', process.env.HERU_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  if (await alreadyProcessed(event.id)) return res.sendStatus(200);
  await queue.push(event); // process outside the request
  res.sendStatus(200);
});

Testing without a public URL

While your endpoint is not reachable from the internet, read the resource with GET: its status is the same one the event carries.

© 2026 Kuarto Inc. (heru)heru.appQuestions about your integration? Contact your heru account team.

On this page