Quick Start: Gin Email Webhook
JsonHook delivers every inbound email as a JSON POST request to your webhook endpoint. Setting up a Gin handler takes less than 5 minutes. Start by initializing your project:
go mod init myapp && go get github.com/gin-gonic/gin
Then create your webhook endpoint. The following example shows the minimal code needed to receive and acknowledge a JsonHook delivery:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.POST("/webhook", func(c *gin.Context) {
var payload map[string]interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(400, gin.H{"error": "invalid JSON"})
return
}
email := payload["email"].(map[string]interface{})
c.JSON(200, gin.H{"status": "ok"})
_ = email
})
r.Run(":3000")
}
Point your JsonHook address webhook URL to this endpoint and you will start receiving parsed emails as JSON within seconds of the email arriving.
Full Gin Implementation
The quick start example above is enough to get started, but a production implementation should include signature verification, structured error handling, and proper HTTP response codes. The complete example below demonstrates all of these patterns together.
This implementation verifies the X-JsonHook-Signature header to confirm the request genuinely came from JsonHook, parses the full email payload, and returns the appropriate HTTP status codes to trigger or suppress retries.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"os"
"github.com/gin-gonic/gin"
)
type Attachment struct {
Filename string `json:"filename"`
ContentType string `json:"contentType"`
Size int `json:"size"`
}
type Email struct {
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
TextBody string `json:"textBody"`
HtmlBody string `json:"htmlBody"`
Attachments []Attachment `json:"attachments"`
}
type Payload struct {
Event string `json:"event"`
Timestamp string `json:"timestamp"`
Address string `json:"address"`
Email Email `json:"email"`
}
var secret = []byte(os.Getenv("JSONHOOK_WEBHOOK_SECRET"))
func verifySignature(body []byte, sigHeader string) bool {
if sigHeader == "" { return false }
mac := hmac.New(sha256.New, secret)
mac.Write(body)
computed := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computed), []byte(sigHeader))
}
func webhookHandler(c *gin.Context) {
// Read raw body before Gin parses it
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(500, gin.H{"error": "read error"})
return
}
sig := c.GetHeader("X-JsonHook-Signature")
if !verifySignature(body, sig) {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
var payload Payload
if err := json.Unmarshal(body, &payload); err != nil {
c.JSON(400, gin.H{"error": "invalid JSON"})
return
}
c.JSON(200, gin.H{"status": "ok"})
go func(p Payload) {
log.Printf("[%s] Email at %s from %s | Subject: %s",
p.Timestamp, p.Address, p.Email.From, p.Email.Subject)
for _, att := range p.Email.Attachments {
log.Printf(" Attachment: %s (%d bytes)", att.Filename, att.Size)
}
}(payload)
}
func main() {
r := gin.Default()
r.POST("/webhook", webhookHandler)
r.Run(":3000")
}
The webhook handler returns 200 immediately after queuing the email for processing. Avoid doing expensive work (database writes, API calls) synchronously inside the handler — process the payload in a background job to stay within JsonHook's 10-second response timeout.
Build Your Gin Email Integration
Free API key — start receiving webhooks in 5 minutes.
Get Free API KeyParsing the Webhook Payload
Every JsonHook delivery is an HTTP POST with Content-Type: application/json. The payload follows a consistent schema regardless of the originating email client or provider:
type Payload struct {
Event string `json:"event"`
Timestamp string `json:"timestamp"`
Address string `json:"address"`
Email struct {
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
TextBody string `json:"textBody"`
HtmlBody string `json:"htmlBody"`
Attachments []struct {
Filename string `json:"filename"`
ContentType string `json:"contentType"`
Size int `json:"size"`
} `json:"attachments"`
} `json:"email"`
}
// After reading body with io.ReadAll:
var p Payload
json.Unmarshal(body, &p)
fmt.Println(p.Email.From, p.Email.Subject)
Key fields in the payload:
- event — Always
"email.received"for inbound email events - timestamp — ISO 8601 timestamp of when JsonHook received the email
- address — The JsonHook inbound address that received the email (e.g.,
[email protected]) - email.from — Sender address string, e.g.,
"Alice <[email protected]>" - email.to — Array of recipient address strings
- email.subject — Email subject line
- email.textBody — Plain text body of the email (may be empty if HTML-only)
- email.htmlBody — HTML body of the email (may be empty if plain-text-only)
- email.attachments — Array of attachment objects, each with
filename,contentType,size, andcontentId
Verifying Webhook Signatures
JsonHook signs every webhook delivery using HMAC-SHA256. The signature is included in the X-JsonHook-Signature request header as a hex digest. To verify it, compute the HMAC-SHA256 of the raw request body using your address's webhook secret and compare it to the header value.
Your webhook secret is returned when you create an inbound address via the API (POST /api/addresses). Store it as an environment variable — never hard-code it.
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifyJsonHookSignature(body []byte, sigHeader string, secret []byte) bool {
if sigHeader == "" { return false }
mac := hmac.New(sha256.New, secret)
mac.Write(body)
computed := hex.EncodeToString(mac.Sum(nil))
// hmac.Equal is constant-time
return hmac.Equal([]byte(computed), []byte(sigHeader))
}
Always verify the signature before processing the payload. Return 401 for invalid signatures so that legitimate retries from JsonHook (which always include a valid signature) are distinguishable from spoofed requests.
Error Handling Best Practices
Reliable webhook handling requires careful attention to error responses. JsonHook uses your HTTP response code to decide whether to retry a delivery:
- Return 200 quickly: Acknowledge receipt immediately and process asynchronously. JsonHook will retry any non-2xx response.
- Return 400 for bad requests: If the payload fails your own validation (not signature — use 401 for that), return 400 to prevent retries of malformed deliveries.
- Return 500 to trigger retries: If your downstream system is temporarily unavailable, returning 500 causes JsonHook to retry with exponential backoff (up to 5 attempts over ~1 hour).
- Never return 200 before verifying the signature: Doing so silently accepts spoofed requests.
Gin-specific tips:
- Use
io.ReadAll(c.Request.Body)before callingc.ShouldBindJSON()— Gin's bind methods consume the body, leaving nothing for HMAC computation - Use
hmac.Equal()from thecrypto/hmacpackage for constant-time comparison of the computed and received signatures - Use
go func(p Payload) { ... }(payload)to process the email in a goroutine after sending the JSON 200 response - Register a Gin recovery middleware (
gin.Recovery()) to catch panics in handlers and return 500, which triggers JsonHook retries
Gin Framework Tips
Gin provides several conveniences that make webhook handling cleaner. Here are framework-specific patterns to use when integrating JsonHook:
- Register your webhook route before any authentication middleware — the JsonHook request does not carry user credentials, only the HMAC signature.
- Use raw body access for signature verification. Many Gin frameworks parse the body automatically — make sure you are hashing the raw bytes, not the re-serialized parsed object.
- Use a dedicated route or controller file for webhook handlers to keep the codebase organized as you add more inbound address integrations.
- Log the
addressfield from every payload to track which inbound address received the email — useful for multi-address setups. - Consider using Gin's built-in request validation or a schema library (e.g., Zod, Pydantic, etc.) to validate the payload structure after signature verification.