Greenstamp webhooks let you receive real-time, signed event notifications the moment things happen in your account — invoice stamped, bill paid, certificate expiring — without polling the API.
Webhooks are push-based: you register an HTTPS endpoint, and Greenstamp POSTs a signed JSON payload to it whenever a subscribed event occurs. This is the inverse of the REST API, where you pull data on demand.
If you need to query historical events or fill a gap after downtime, you can reconcile against the pull-based Events API — use X-Webhook-ID as a correlation key.
webhook_endpoints REST resource. Provide a public HTTPS URL and select which event types to subscribe to.X-Webhook-Signature header (see Verifying signatures) and return 200 OK.Endpoint management CRUD (create, list, update, delete, rotate secret) is documented in the REST API Reference under webhook_endpoints. This guide covers the consumer side — what you receive and how to process it.
Every delivery is an HTTP POST with Content-Type: application/json. The following headers are always present:
| Header | Example value | Purpose |
|---|---|---|
X-Webhook-ID |
wdel_AbCdEfGh1234567890abcd |
Unique delivery ID. Use this to deduplicate retries. |
X-Webhook-Timestamp |
1718200000 |
Unix timestamp (seconds) of when the delivery was sent. Use to reject stale replays. |
X-Webhook-Signature |
a3f9b2... (64 hex chars) |
HMAC-SHA256 signature. See Verifying signatures. |
User-Agent |
Greenstamp-Webhook/1.0 |
Identifies the sender. |
Content-Type |
application/json |
Body is always JSON. |
Every event envelope has the same top-level shape. The data field contains event-specific attributes.
{
"id": "evt_AbCdEfGh1234567890abcdef", // unique event ID (same as X-Webhook-ID event part)
"type": "invoice.stamped", // event type — see Event catalog
"created_at": "2024-06-07T10:30:00Z", // ISO 8601 UTC
"api_version": "v1",
"entity": {
"id": "entt_...", // the entity this event concerns
"legal_name": "Acme SA de CV"
},
"data": { ... } // event-specific payload (see below)
}
Example — invoice.stamped
{
"id": "evt_AbCdEfGh1234567890abcdef",
"type": "invoice.stamped",
"created_at": "2024-06-07T10:30:00Z",
"api_version": "v1",
"entity": {
"id": "entt_Xy9mNpQ3rsTu",
"legal_name": "Acme SA de CV"
},
"data": {
"id": "inv_Lm4kP8vWzXqR",
"folio": "A-0042",
"status": "stamped",
"total": 116000,
"currency": "MXN",
"counterparty": {
"id": "cpty_Rn7tBxYcVdEs",
"name": "Globex Corp"
}
}
}
Always verify signatures. Without verification, your endpoint accepts any POST as a legitimate Greenstamp event — an attacker could forge deliveries and trigger business logic in your system.
Greenstamp signs each delivery as follows. You must reproduce this exactly — a single-character difference produces a completely different signature.
SHA256(raw_secret). The raw secret is the 64-character hex string you copied at creation time.
key = SHA256_hex(raw_secret) # 64-char hex → 64-char hex
X-Webhook-Timestamp value, a literal dot, and the raw request body bytes (do not re-serialize the JSON):
message = X-Webhook-Timestamp + "." + request_body
expected = HMAC-SHA256(key, message) # as lowercase hex
Why SHA256(raw_secret) and not the raw secret? Greenstamp never stores your raw secret — only secret_digest = SHA256(raw_secret). The server signs using secret_digest as the HMAC key. Your receiver must perform the same derivation step before computing the HMAC.
require 'openssl'
require 'digest'
# raw_secret — the 64-char hex string shown at webhook creation
# timestamp — X-Webhook-Timestamp header value (string)
# body — raw request body (string, NOT re-parsed JSON)
# sig_header — X-Webhook-Signature header value
def verify_webhook(raw_secret, timestamp, body, sig_header)
# Step 1: derive the signing key the same way the server does
key = Digest::SHA256.hexdigest(raw_secret)
# Step 2: build the signed string
message = "#{timestamp}.#{body}"
# Step 3: compute HMAC-SHA256
expected = OpenSSL::HMAC.hexdigest('SHA256', key, message)
# Step 4: constant-time compare (Rails / ActiveSupport)
ActiveSupport::SecurityUtils.secure_compare(sig_header, expected)
end
# In a Rails controller:
def create
timestamp = request.headers['X-Webhook-Timestamp']
signature = request.headers['X-Webhook-Signature']
body = request.raw_post
# Reject if timestamp is more than 5 minutes old
if (Time.now.to_i - timestamp.to_i).abs > 300
head :forbidden and return
end
unless verify_webhook(ENV['GREENSTAMP_WEBHOOK_SECRET'], timestamp, body, signature)
head :forbidden and return
end
event = JSON.parse(body)
# process event...
head :ok
end
const crypto = require('crypto');
// rawSecret — the 64-char hex string shown at webhook creation
// timestamp — X-Webhook-Timestamp header value (string)
// body — raw request body (string, NOT re-parsed JSON)
// sigHeader — X-Webhook-Signature header value
function verifyWebhook(rawSecret, timestamp, body, sigHeader) {
// Step 1: derive the signing key the same way the server does
const key = crypto.createHash('sha256').update(rawSecret).digest('hex');
// Step 2: build the signed string
const message = `${timestamp}.${body}`;
// Step 3: compute HMAC-SHA256
const expected = crypto.createHmac('sha256', key).update(message).digest('hex');
// Step 4: constant-time compare
try {
return crypto.timingSafeEqual(
Buffer.from(sigHeader, 'hex'),
Buffer.from(expected, 'hex')
);
} catch {
return false; // mismatched lengths or bad hex
}
}
// Express middleware example:
app.post('/webhooks/greenstamp', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.headers['x-webhook-timestamp'];
const signature = req.headers['x-webhook-signature'];
const body = req.body.toString(); // raw bytes → string
// Reject if timestamp is more than 5 minutes old
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.sendStatus(403);
}
if (!verifyWebhook(process.env.GREENSTAMP_WEBHOOK_SECRET, timestamp, body, signature)) {
return res.sendStatus(403);
}
const event = JSON.parse(body);
// process event...
res.sendStatus(200);
});
The X-Webhook-Timestamp header lets you detect and reject replayed requests. The examples above reject any delivery older than 5 minutes — adjust the window to fit your SLA, but keep it short enough to prevent replay attacks.
For an additional layer of replay protection, store processed X-Webhook-ID values and reject duplicates (see Best practices).
Greenstamp emits 28 event types across 6 domains. The list below is generated from the model constants — it updates automatically when new event types are added.
| Event type | Description |
|---|---|
| invoice.created | Invoice record created; not yet submitted for stamping. |
| invoice.submitted | Stamp request submitted to the tax authority. |
| invoice.stamped | Successfully stamped by the tax authority; UUID assigned. |
| invoice.stamp_failed | Stamp request rejected by the tax authority. |
| invoice.filing_failed | Filing step failed after stamping. |
| invoice.cancellation_requested | Cancellation request submitted to the tax authority. |
| invoice.canceled | Invoice successfully canceled. |
| invoice.payment_received | A payment was applied to the invoice. |
| invoice.paid | Invoice marked as fully paid. |
| invoice.issued | Invoice issued to the counterparty. |
| Event type | Description |
|---|---|
| bill.created | AP bill record created. |
| bill.received | Bill received from supplier. |
| bill.validated | Bill passed validation checks. |
| bill.validation_failed | Bill failed one or more validation checks. |
| bill.rejected | Bill rejected. |
| bill.paid | Bill marked as fully paid. |
| Event type | Description |
|---|---|
| invoice_credit_note.created | Credit note created against an invoice. |
| invoice_credit_note.stamped | Credit note successfully stamped by the tax authority. |
| invoice_credit_note.stamp_failed | Credit note stamp request rejected. |
| invoice_credit_note.canceled | Credit note canceled. |
| Event type | Description |
|---|---|
| payment.recorded | Payment recorded. |
| payment.complement_stamped | Mexico CFDI payment complement successfully stamped. |
| payment.complement_failed | Payment complement stamping failed. |
| Event type | Description |
|---|---|
| entity.certificate_expiring | Entity's e-signing certificate is expiring within 30 days. |
| entity.certificate_expired | Entity's e-signing certificate has expired. |
| entity.certificate_invalid | Entity's e-signing certificate is invalid. |
| Event type | Description |
|---|---|
| counterparty.created | New counterparty created. |
| supplier.created | New supplier created. |
Greenstamp considers any 2xx response a success. Any other response — including timeouts — is treated as a failure and triggers the retry schedule.
Each delivery gets up to 5 attempts total (1 initial + 4 retries). If all 5 fail, the delivery is marked failed permanently.
| Attempt | Delay after previous failure |
|---|---|
| 1st (initial) | Immediate |
| 2nd | 1 minute |
| 3rd | 5 minutes |
| 4th | 30 minutes |
| 5th (final) | 2 hours |
If a destination accumulates 10 consecutive failures with no successful delivery in between, it is automatically disabled with reason "Automatically disabled after 10 consecutive failures". You will need to re-enable it manually from the Webhooks UI or via the API after the issue is resolved.
Greenstamp waits up to 30 seconds for a response (10-second connection timeout). If your handler needs more time, acknowledge immediately with 200 OK and do the work asynchronously — see Best practices.
Every attempt is logged in the Webhooks UI: status, response code, response body, and latency. Use this to debug failed deliveries and inspect exact payloads.
200 OK immediately, process async.
Greenstamp waits 30 seconds max. If your handler touches a database, calls downstream APIs, or sends emails, offload that work to a background job and return 200 right away. Timeouts count as failures and consume retry slots.
X-Webhook-ID.
Retries send the same delivery ID. Store processed IDs and skip re-processing if you see a duplicate. This makes your handler idempotent against both retries and any unexpected duplicates.
200 for unknowns rather than 400 — returning non-2xx for an unknown event type causes it to be retried unnecessarily and can contribute to auto-disable.
X-Webhook-Timestamp is more than 5 minutes old (or your chosen window). This prevents an attacker who has intercepted a delivery from replaying it later.
https:// endpoints with a valid TLS certificate. Self-signed certificates are rejected. Greenstamp also blocks delivery to private IP ranges (RFC 1918) and loopback addresses.
Related: REST API Reference · MCP / Agent Tools