Webhooks allow your application to receive real-time HTTP callbacks when email events occur, such as deliveries, bounces, opens, and clicks.
How Webhooks Work
You register a webhook URL with Ark
When an event occurs, Ark sends an HTTP POST to your URL
Your server processes the event and returns a 2xx response
If delivery fails, Ark retries with exponential backoff
Creating a Webhook
Python
Node.js
Ruby
Go
cURL
from ark import Ark
client = Ark()
webhook = client.webhooks.create(
name = "My Webhook" ,
url = "https://yourapp.com/webhooks/ark" ,
events = [ "MessageSent" , "MessageBounced" , "MessageDeliveryFailed" , "MessageLinkClicked" , "MessageLoaded" ]
)
print ( f "Webhook created: { webhook.data.id } " )
import Ark from 'ark' ;
const client = new Ark ();
const webhook = await client . webhooks . create ({
name: 'My Webhook' ,
url: 'https://yourapp.com/webhooks/ark' ,
events: [ 'MessageSent' , 'MessageBounced' , 'MessageDeliveryFailed' , 'MessageLinkClicked' , 'MessageLoaded' ],
});
console . log ( `Webhook created: ${ webhook . data . id } ` );
require "ark_email"
client = ArkEmail :: Client . new
webhook = client. webhooks . create (
name: "My Webhook" ,
url: "https://yourapp.com/webhooks/ark" ,
events: [ "MessageSent" , "MessageBounced" , "MessageDeliveryFailed" , "MessageLinkClicked" , "MessageLoaded" ]
)
puts "Webhook created: #{ webhook. data . id } "
client := ark . NewClient ()
webhook , err := client . Webhooks . Create ( ctx , ark . WebhookCreateParams {
Name : ark . String ( "My Webhook" ),
URL : ark . String ( "https://yourapp.com/webhooks/ark" ),
Events : ark . StringSlice ([] string {
"MessageSent" , "MessageBounced" , "MessageDeliveryFailed" ,
"MessageLinkClicked" , "MessageLoaded" ,
}),
})
if err != nil {
log . Fatal ( err )
}
fmt . Printf ( "Webhook created: %s \n " , webhook . Data . ID )
curl -X POST https://api.arkhq.io/v1/webhooks \
-H "Authorization: Bearer $ARK_API_KEY " \
-H "Content-Type: application/json" \
-d '{
"name": "My Webhook",
"url": "https://yourapp.com/webhooks/ark",
"events": ["MessageSent", "MessageBounced", "MessageDeliveryFailed", "MessageLinkClicked", "MessageLoaded"]
}'
Event Types
Event Description MessageSentEmail successfully delivered to recipient’s server MessageDelayedTemporary delivery issue, will retry MessageDeliveryFailedPermanent delivery failure after all retries MessageBouncedHard bounce - recipient rejected the email MessageHeldEmail held for manual review MessageLoadedRecipient opened the email (tracking pixel loaded) MessageLinkClickedRecipient clicked a link in the email DomainDNSErrorDNS configuration issue with your sending domain
Webhook Payload
All webhooks include this structure:
{
"event" : "MessageSent" ,
"timestamp" : 1704672000.123456 ,
"uuid" : "abc123-def456-ghi789" ,
"payload" : {
"message" : {
"id" : 12345 ,
"token" : "abc123" ,
"direction" : "outgoing" ,
"message_id" : "<[email protected] >" ,
"to" : "[email protected] " ,
"from" : "[email protected] " ,
"subject" : "Welcome to our service" ,
"timestamp" : 1704672000.0 ,
"spam_status" : "NotSpam" ,
"tag" : "onboarding"
},
"status" : "Sent" ,
"details" : "250 OK" ,
"output" : "Message accepted" ,
"sent_with_ssl" : true ,
"timestamp" : 1704672000.123456 ,
"time" : 0.234
}
}
Event-Specific Data
{
"event" : "MessageBounced" ,
"timestamp" : 1704672000.123456 ,
"uuid" : "abc123-def456-ghi789" ,
"payload" : {
"original_message" : {
"id" : 12345 ,
"token" : "abc123" ,
"to" : "[email protected] " ,
"from" : "[email protected] " ,
"subject" : "Welcome!"
},
"bounce" : {
"id" : 12346 ,
"token" : "def456" ,
"to" : "[email protected] " ,
"from" : "[email protected] " ,
"subject" : "Delivery Status Notification"
}
}
}
{
"event" : "MessageLoaded" ,
"timestamp" : 1704672000.123456 ,
"uuid" : "abc123-def456-ghi789" ,
"payload" : {
"message" : {
"id" : 12345 ,
"token" : "abc123" ,
"to" : "[email protected] " ,
"from" : "[email protected] " ,
"subject" : "Welcome!"
},
"ip_address" : "192.168.1.1" ,
"user_agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
}
{
"event" : "MessageLinkClicked" ,
"timestamp" : 1704672000.123456 ,
"uuid" : "abc123-def456-ghi789" ,
"payload" : {
"message" : {
"id" : 12345 ,
"token" : "abc123" ,
"to" : "[email protected] " ,
"from" : "[email protected] " ,
"subject" : "Welcome!"
},
"url" : "https://example.com/welcome" ,
"token" : "link-token-123" ,
"ip_address" : "192.168.1.1" ,
"user_agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
}
Verifying Signatures
All webhooks are cryptographically signed using RSA-SHA256 for security. Each webhook request includes two headers:
Header Description X-Ark-SignatureBase64-encoded RSA-SHA256 signature of the request body X-Ark-Signature-KIDKey ID identifying which public key was used to sign
Getting the Public Key
Ark exposes its public key via a standard JWKS (JSON Web Key Set) endpoint:
GET https://mail.arkhq.io/.well-known/jwks.json
Cache the public key in your application to avoid fetching it on every webhook. The key rarely changes, but you should refresh it if signature verification fails.
Verification Examples
import crypto from 'crypto' ;
// Cache this - fetch from https://mail.arkhq.io/.well-known/jwks.json
let cachedPublicKey = null ;
async function getPublicKey () {
if ( cachedPublicKey ) return cachedPublicKey ;
const response = await fetch ( 'https://mail.arkhq.io/.well-known/jwks.json' );
const jwks = await response . json ();
// Convert JWK to PEM format
const jwk = jwks . keys [ 0 ];
cachedPublicKey = crypto . createPublicKey ({ key: jwk , format: 'jwk' });
return cachedPublicKey ;
}
function verifyWebhookSignature ( payload , signatureBase64 , publicKey ) {
const signature = Buffer . from ( signatureBase64 , 'base64' );
const verifier = crypto . createVerify ( 'RSA-SHA256' );
verifier . update ( payload );
return verifier . verify ( publicKey , signature );
}
// Express.js example
app . post ( '/webhooks/ark' , express . raw ({ type: 'application/json' }), async ( req , res ) => {
const signature = req . headers [ 'x-ark-signature' ];
if ( ! signature ) {
return res . status ( 401 ). send ( 'Missing signature' );
}
try {
const publicKey = await getPublicKey ();
const isValid = verifyWebhookSignature ( req . body . toString (), signature , publicKey );
if ( ! isValid ) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
const event = JSON . parse ( req . body );
// Acknowledge receipt immediately
res . status ( 200 ). send ( 'OK' );
// Process event asynchronously
switch ( event . event ) {
case 'MessageSent' :
console . log ( `Email ${ event . payload . message . token } sent` );
break ;
case 'MessageBounced' :
console . log ( `Email ${ event . payload . original_message . token } bounced` );
// Add to suppression list
break ;
case 'MessageLoaded' :
console . log ( `Email ${ event . payload . message . token } opened` );
break ;
}
} catch ( error ) {
console . error ( 'Webhook verification failed:' , error );
return res . status ( 401 ). send ( 'Verification failed' );
}
});
Retry Policy
If your endpoint doesn’t respond with a 2xx status code, Ark will retry delivery:
Attempt Delay 1 Immediate 2 2 minutes 3 3 minutes 4 6 minutes 5 10 minutes 6 15 minutes
After 6 failed attempts, the webhook is marked as failed and won’t be retried.
Return a 2xx response quickly, then process the event asynchronously. This prevents timeouts and ensures reliable delivery.
Testing Webhooks
Use the test endpoint to verify your integration:
Python
Node.js
Ruby
Go
cURL
result = client.webhooks.test( "whk_abc123" , event = "MessageSent" )
print ( f "Success: { result.data.success } " )
print ( f "Status code: { result.data.status_code } " )
print ( f "Duration: { result.data.duration } ms" )
const result = await client . webhooks . test ( 'whk_abc123' , {
event: 'MessageSent' ,
});
console . log ( `Success: ${ result . data . success } ` );
console . log ( `Status code: ${ result . data . statusCode } ` );
console . log ( `Duration: ${ result . data . duration } ms` );
result = client. webhooks . test ( "whk_abc123" , event: "MessageSent" )
puts "Success: #{ result. data . success } "
puts "Status code: #{ result. data . status_code } "
puts "Duration: #{ result. data . duration } ms"
result , _ := client . Webhooks . Test ( ctx , "whk_abc123" , ark . WebhookTestParams {
Event : ark . String ( "MessageSent" ),
})
fmt . Printf ( "Success: %v \n " , result . Data . Success )
fmt . Printf ( "Status code: %d \n " , result . Data . StatusCode )
fmt . Printf ( "Duration: %d ms \n " , result . Data . Duration )
curl -X POST https://api.arkhq.io/v1/webhooks/{webhookId}/test \
-H "Authorization: Bearer $ARK_API_KEY " \
-H "Content-Type: application/json" \
-d '{"event": "MessageSent"}'
The response will include:
success - Whether your endpoint responded with a 2xx status
statusCode - The HTTP status code returned
body - The response body (truncated if too long)
duration - Response time in milliseconds
Best Practices
Never process webhooks without verifying the RSA signature first. This ensures the webhook genuinely came from Ark.
Return a 2xx within 5 seconds. Queue events for async processing if needed.
Webhooks may be delivered more than once. Use the uuid field for idempotency.
Webhook URLs must use HTTPS for security.
Fetch the JWKS once and cache it. Only refresh if signature verification fails.
Set up alerts for webhook delivery failures in your monitoring system.
Next Steps