Webhooks #
eCardWidget has two webhook families:
ecard_shared— fires when an eCard is sent/shared (email, link, social). Configured once in the dashboard (Settings → Developers → Webhook URL). See eCard-shared webhook.- Member-change webhooks — fire when your directory changes (member created / updated / deactivated / deleted). Subscribed via the API; signed and retried. See Member-change webhooks.
eCard-shared webhook #
Set a Webhook URL under Settings → Developers and click Save (blank disables it). eCardWidget
sends a POST with header User-Agent: eCardWidget Hook whenever an eCard is shared. The body is a
JSON array of one or more events:
[
{
"event_type": "ecard_shared",
"type": "email",
"sender_request_id": "rmmbps64tn8tyl5ghdft4",
"ecardid": 14744,
"widgetid": 4069,
"vanity_id": "smcxzw64jky8tcg",
"sender_name": "Sender",
"sender_email": "[email protected]",
"recipient_name": "Recipient",
"recipient_email": "[email protected]",
"personal_message": "<p>Thinking of you!</p>",
"locale": "en",
"scheduled_to_be_sent_at": "2026-03-12T15:49:25Z",
"sent": false,
"opened": false,
"merge_tags": "",
"override_props": ""
}
]
typeis the share method:email,facebook,twitter,linkedin,sms,whatsapp, etc.sentisfalsefor pending email sends (the hook fires before dispatch) andtruefor immediate share/social sends. Usescheduled_to_be_sent_atfor the expected send time.- This is the same event behind the Zapier "eCard Sent via Email" trigger (Zapier).
- All deliveries are signed, retried, and visible in the portal — see Delivery, signing & monitoring.
Member-change webhooks #
Get notified when your directory changes — e.g. to push new hires, updates, or terminations back into another system. These are the same events that power the eCardWidget Zapier triggers.
Base path: /v2/api/pub/team-member-actions
Auth: Authorization: Bearer <YOUR_API_KEY> (see getting-started).
Subscribe — POST /subscribe #
curl -X POST https://app.ecardwidget.com/v2/api/pub/team-member-actions/subscribe \
-H "Authorization: Bearer $ECW_API_KEY" -H "Content-Type: application/json" \
-d '{ "event_type": "team_member_updated", "hookUrl": "https://your-app.example.com/hooks/ecw" }'
Returns the subscription id:
{ "success": true, "data": { "id": "<subscription-id>" }, "messages": [] }
Unsubscribe — POST /unsubscribe #
curl -X POST https://app.ecardwidget.com/v2/api/pub/team-member-actions/unsubscribe \
-H "Authorization: Bearer $ECW_API_KEY" -H "Content-Type: application/json" \
-d '{ "id": "<subscription-id>" }'
Event types #
| Event | Fires when |
|---|---|
team_member_created |
a member is added |
team_member_updated |
a member materially changes (idempotent re-syncs do not fire) |
team_member_deactivated |
a member is deactivated |
team_member_deleted |
a member is permanently deleted |
Delivery payload #
Each delivery is a JSON POST to your hookUrl:
{
"event_id": "00000000-0000-0000-0000-000000000000",
"event_type": "team_member_updated",
"timestamp": "2026-01-01T09:00:00Z",
"source": "api",
"changed_fields": ["job_position"],
"member": {
"id": 123,
"email": "[email protected]",
"employee_id": "EMP-100",
"first_name": "Grace",
"last_name": "Hopper",
"job_position": "Engineer",
"departments": ["Engineering"],
"date_of_birth": "1800-04-12",
"hire_date": "2021-03-01",
"profile_image_url": "",
"disabled": 0,
"updatedat": "2026-01-01T09:00:00Z"
}
}
source— where the change came from:api,zapier,ui, orimport.changed_fields— present onteam_member_updated; lists what changed.- Birthday years are privacy-masked (stored as year
1800).
Delivery, signing & monitoring #
Both webhook families share one delivery engine, so signing, retries, and the portal log behave the same way for each. (The bulk spreadsheet importer does not emit member events — they're for ongoing per-record changes.)
Signing #
Every delivery is signed with your account's signing secret (view and rotate it under
Settings → Developers → Webhooks). The signature rides in the X-ECW-Signature header,
Stripe-style:
X-ECW-Signature: t=<unix-timestamp>,v1=<hex HMAC-SHA256>
The signed value is <t>.<raw-request-body>. To verify, recompute the HMAC and compare to v1, and
reject timestamps older than ~5 minutes to block replays. During a secret rotation the header carries
two v1= values (new + previous) for 24 hours so you can swap without missed verifications.
const crypto = require('crypto');
const header = req.get('X-ECW-Signature'); // "t=...,v1=..."
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
// also reject if Math.abs(Date.now() / 1000 - Number(parts.t)) > 300
Retries #
Failed deliveries (any non-2xx, timeout, or connection error) are retried automatically with
exponential backoff — up to 5 attempts, roughly 1, 2, 4, 8, then 16 minutes after the
previous try. A 410 Gone response stops retries immediately (treated as "unsubscribe"). Use
event_id / sender_request_id to de-duplicate, since a retry reuses the same id.
Auto-pause #
If a custom webhook endpoint keeps failing (3 consecutive exhausted deliveries) it is paused and
you're emailed; re-enable it under Settings → Developers once it's fixed. (Zapier destinations are
only paused on 410 Gone, never on transient errors.)
Delivery log, Replay & Test #
Settings → Developers → Webhooks shows a delivery log of every attempt — status (Delivered / Pending / Retrying / Failed), response code, the exact payload we sent, and the response your endpoint returned. From there you can:
- Replay any delivery to run the exact same request again. The replay is recorded as a new attempt; the original entry is kept.
- Send test — fire a sample
testevent to your configured URL to confirm it's reachable. This works even while an endpoint is paused, so you can verify a fix before re-enabling.
Loop awareness #
If you both write to ECW (via upsert) and listen for changes, use the source
field to avoid feedback loops (e.g. ignore events your own integration caused).