Guide to Aimfox Webhooks: Features and Best Practices
Aimfox webhooks report what your LinkedIn outreach is doing as it happens: a connection request is accepted, a lead replies, a campaign ends or a LinkedIn account gets logged out. If your team runs campaigns in Aimfox, webhooks are how that activity reaches your CRM without polling the Aimfox API.
This guide covers how Aimfox webhooks are set up, the events and payload shape, how to authenticate deliveries with the Authentication Header, Aimfox's retry schedule, and the practices that keep an Aimfox receiver reliable.
What are Aimfox webhooks?
Aimfox webhooks notify your systems "about events as they occur in your Aimfox workspace". You can add several endpoints to a workspace, in the dashboard or through the API, and each endpoint subscribes to one or more event types. Aimfox marks its webhook documentation as beta.
Every delivery is a JSON object with four fields: an event id, the event_type, the workspace and an event object whose contents depend on the type.
Aimfox doesn't sign webhooks. The only authentication is an optional Authentication Header that you set on the webhook. It "supports Basic Auth or custom headers such as Bearer tokens", and Aimfox sends it with every request for your receiver to compare.
Aimfox webhook features
| Feature | Details |
|---|---|
| Configuration | Workspace settings > Webhooks, or the Webhooks API |
| Subscriptions | Several endpoints per workspace, each with one or more event types |
| Format | JSON |
| Event identifier | event_type |
| Idempotency key | id |
| Authentication | Optional Authentication Header: Basic, or a custom header such as a Bearer token. No signature |
| Retries | Up to 6, at 1 minute, then 5 minutes, 30 minutes, 1 hour, 6 hours and 1 day after the previous attempt |
| Method, timeout and success codes | Not documented |
| Source IPs | Not published |
| Status | Beta |
Common events
Aimfox documents 17 event types:
| Area | Events | Fires when |
|---|---|---|
| Accounts | account_logged_in, account_logged_out | A workspace LinkedIn account logs in and is authenticated, or is logged out manually or by session expiry |
| Connections | new_connection, connect, accepted | A new connection is detected on a workspace account however it was made, a campaign sends a connection request, or a campaign's request is accepted |
| Campaign activity | view, message, inmail | A campaign visits a profile, sends a message, or sends an InMail |
| Replies | reply, campaign_reply, inmail_reply, new_reply | See below |
| Inbox | inbox_event | Any inbox activity on a workspace account. event.type says whether it was sent or received |
| Leads | lead_label_added | A label is applied to a lead |
| Campaigns | campaign_created, campaign_started, campaign_ended | A campaign is created, starts for the first time, or runs to completion |
The reply events overlap, so pick deliberately:
replyfires once per lead, on their first reply to a campaign message.campaign_replyfires on every reply to a campaign message.inmail_replyfires when a lead replies to a campaign InMail.new_replyfires for a reply to any LinkedIn message on a workspace account, whether or not a campaign sent the original.
Here is Aimfox's account_logged_in example:
{
"id": "b82dce4c-34ef-4d08-875b-bf53a7432661",
"event_type": "account_logged_in",
"event": {
"state": "INACTIVE",
"default_session_id": "56235b2f-ff29-403f-9844-42334147bd84",
"urn": "ACoAACjiOMsBxN2eyo8LuQz4xK1D54uGND8jCwg",
"timestamp": "2024-11-24T21:08:27.966Z",
"account": {
"id": 685914315,
"urn": "ACoAACjiOMsBxN2eyo8LuQz4xK1D54uGND8jCwg",
"public_identifier": "kengur-kengurovic-1a3865171",
"first_name": "Kengur",
"last_name": "Kengurovic",
"email": "kengur1111@gmail.com",
"picture_url": "https://cdn.constel.co/linkedin-profile/685914315.jpg"
}
},
"workspace": {
"id": "47973863-0513-4ee6-910a-c5bc55782ed2",
"name": "Aimfox Workspace",
"created_at": 1732541078
}
}
Timestamps come in more than one form. event.timestamp is ISO 8601, workspace.created_at is Unix seconds, and Aimfox's campaign_reply example has a nested message.created_at in milliseconds. Parse each field on its own terms.
Setting up Aimfox webhooks
- Generate a long random secret, for example with
openssl rand -hex 32, and decide on the header, such asAuthorization: Bearer <secret>. - In Aimfox, go to Workspace settings and open the Webhooks tab.
- Click Add webhook and enter a Name, your HTTPS endpoint as the URL, and the header in Authentication Header.
- Choose the events from the dropdown and click Save.
To use Basic Auth instead, generate the Basic ... value yourself (Aimfox links to a generator) and paste it as the header value. It's still a fixed value sent with every request.
Through the API, create the webhook with POST https://api.aimfox.com/api/v3/webhooks and a normal API key:
curl -X POST "https://api.aimfox.com/api/v3/webhooks" \
-H "Authorization: Bearer $AIMFOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Aimfox events",
"events": ["accepted", "campaign_reply"],
"url": "https://example.com/webhooks/aimfox",
"integration": false
}'
Aimfox's create example doesn't include headers, but its edit endpoint, PATCH /api/v3/webhooks/:webhook_, takes a headers object such as {"Authorization": "Bearer 123"}. Aimfox says integration should always be false.
In Hookdeck. Create a source with the Aimfox type, enter the header name in API Key Header and the exact value in API Key, including any Bearer or Basic prefix, and use the source URL as the webhook URL. Hookdeck matches the name case-insensitively and the value exactly. The source accepts POST. If you don't set an Authentication Header, there's nothing to check, so leave verification off on the source, though setting one is the better choice.
For local development, hookdeck listen 3000 aimfox --path /webhooks/aimfox with the Hookdeck CLI gives you a public HTTPS URL, with no account required.
Securing Aimfox webhooks
Look up your header by name and compare the whole value with the secret in constant time, before parsing the body.
const crypto = require('crypto');
const express = require('express');
const app = express();
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on buffers of different lengths
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/webhooks/aimfox', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.AIMFOX_WEBHOOK_SECRET; // e.g. "Bearer 9b0f..."
if (!secret) return res.sendStatus(500); // fail closed when unconfigured
const name = process.env.AIMFOX_WEBHOOK_HEADER || 'Authorization';
const received = req.get(name); // case-insensitive lookup
if (typeof received !== 'string' || !safeEqual(received, secret)) {
return res.sendStatus(401);
}
let payload;
try {
payload = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Store payload.id, then queue payload.event_type / payload.event for a worker.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
def verify_aimfox_request(headers) -> bool:
secret = os.environ.get("AIMFOX_WEBHOOK_SECRET")
if not secret:
return False # fail closed
name = os.environ.get("AIMFOX_WEBHOOK_HEADER", "Authorization")
received = headers.get(name) # case-insensitive in Starlette and Flask
if not received:
return False
return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))
A matching header shows the sender knows your secret. It doesn't show the body is unchanged, and a captured request can be replayed later and still pass, because there's no signature and nothing ties the request to a time. Serve the endpoint over HTTPS only, use a long random value, and update the webhook if it may have leaked.
Make Aimfox webhooks production-ready. Hookdeck Event Gateway checks the Authentication Header on every Aimfox webhook, stores each event durably, and retries delivery to your service on a schedule you control.
Aimfox webhook limitations and pain points
Six retries, then the event is lost
The Problem: An outage that lasts beyond Aimfox's retry schedule loses events, such as replies your sales team needed to see.
Why It Happens: Aimfox retries a failed call "up to 6 times", with the last attempt a day after the one before. After the sixth retry, there's nothing more. Aimfox doesn't document which status codes count as success or how long it waits for a response.
Workarounds:
- Respond with a
2xxquickly and process afterwards. - After an outage, reconcile from the Aimfox API for the period you were down.
How Hookdeck Can Help: Hookdeck accepts each Aimfox webhook as it arrives, so the retry schedule only has to cover the short path to Hookdeck. Hookdeck then retries delivery to your service on your own schedule and lets you replay any event.
Overlapping reply events
The Problem: Subscribing to every reply event creates duplicate notifications for the same message.
Why It Happens: A lead's first reply to a campaign message can match reply, campaign_reply and new_reply, and inbox_event covers all inbox activity.
Workarounds:
- Subscribe to the narrowest event that fits:
campaign_replyfor every campaign reply, orreplyfor first replies only. - If you need several, deduplicate on the message rather than the delivery.
How Hookdeck Can Help: Hookdeck filters can drop the event types a destination doesn't need, and deduplication can drop repeats based on fields you choose, such as the message URN.
The header is optional and static
The Problem: A webhook saved without an Authentication Header sends events with nothing to check, and one saved with it sends the same value every time.
Why It Happens: The field is optional, and Aimfox sends it unchanged. Nothing is computed over the body.
Workarounds:
- Always set an Authentication Header with a long random value.
- Keep the endpoint on HTTPS, and rotate the value if it leaks.
How Hookdeck Can Help: Hookdeck's Aimfox source compares the header you name with the exact value you enter. Hookdeck answers Aimfox with a 200 either way, then records a request without the right header as failed verification, so it never reaches your destination.
Beta documentation
The Problem: Fields or behaviour you rely on may change.
Why It Happens: Aimfox labels webhooks as beta, and leaves the method, success codes and timeout undocumented.
Workarounds:
- Validate only the fields you use, and ignore unknown fields.
- Answer
200to event types you don't recognise, so a new type doesn't count as a failure.
How Hookdeck Can Help: Every request is stored in Hookdeck with its headers and body, so a change in what Aimfox sends shows up in the request history, and you can replay affected events after updating your handler.
Best practices
Deduplicate on id
Retries can deliver an event more than once. Store id before processing and skip repeats. See our guide to webhook idempotency.
Route on event_type
Switch on event_type and treat event as a different shape for each one. For inbox_event, check event.type to tell sent messages from received ones.
Watch account_logged_out
This event means a workspace account has been logged out of LinkedIn, manually or because its session expired. Alert someone when it arrives so the account can be logged back in quickly.
Acknowledge fast and process asynchronously
Verify, store, respond with 200, then update your CRM. See why to process webhooks asynchronously.
Conclusion
Aimfox sends a JSON object for each subscribed event, with id, event_type, workspace and event, and an optional header you set as the only credential. Compare that header in constant time over HTTPS, deduplicate on id, choose reply events carefully, and respond quickly, because Aimfox stops after six retries.
Hookdeck Event Gateway sits in front of your receiver, checks the header, accepts each event as it arrives, and keeps it until your service has processed it.
Get started with Hookdeck for free and handle Aimfox webhooks reliably in minutes.