Webhooks are a great way to extend the functionality of Reveal by allowing you to create your own integrations and make custom automations possible within days, not months.
You can register to be notified from various points within Reveal, which will call your endpoint when something happens so you can act instantly upon the information.
What is a Webhook?
A Webhook consists mainly of an endpoint (a URL), a list of events and a secret.
So when one of the selected events occurs, Reveal will call the endpoint with a specific message (by using POST).
Securing messages
The request will be signed with the secret, so you may know the message is originating from Reveal and not someone else. You must also compose and return an exact response for Reveal, so it knows you acknowledged the message.
If no correct response is received within a limited time, Reveal will NOT retry the message, but will mark the webhook as failed; if you don’t fix the problem in a reasonable timeframe, Reveal will disable the webhook and won’t try to call it again until you re-enable it.
To keep you posted on the webhook’s activity, you can specify on each webhook a list of email addresses, and Reveal will send 4 types of emails to these:
– a notification on the webhook’s first fail
– a notification that the webhook is still failing (capped at 1 email per 30 minutes per webhook)
– when the webhook was disabled by Reveal due to failing for too long
– when a failed webhook started to work correctly again.
You can manualy enable or disable a webhook anytime.
Possible Webhook events
Reveal can call your webhook for two types of events: customer-related events and job-related events.
Customer Events
You can subscribe for one (or more) of these events to have your endpoint called whenever something happens with/to a customer:
- customer_event – you can subscribe on this event to be called whenever one of the below events is triggered for a customer
- customer_event.nps_partial_response – everytime a customer selects an NPS score from the NPS email, or when he changes his mind on the NPS survey and he updates the score
- customer_event.nps_pre_response – when a customer submits the NPS-Pre survey
- customer_event.nps_response_update – when a customer submits the NPS (Post / on email) survey or when a NPS response is updated
- customer_event.nps_response – when a customer submits the NPS (Post / on email) survey
- customer_event.nps_segment_changed – when, as a result of an NPS (Post / on email) response, the average NPS score of the customer changes between Detractor, Passive, Promoter
- customer_event.rfm_group_changed – when the RFM Score of the customer changes and the new RFM Score is assigned to another RFM Group
- customer_event.rfm_importance_decreased – when the new RFM Group of the customer has a lower rank; every RFM Group has an associated rank to simply order the customers on a scale between least valuable customers to most valuable customers (this simplification by ranking is useful to know when a customer is progressing or regressing)
- customer_event.rfm_importance_increased – when the new RFM Group of the customer has a higher rank
- customer_event.rfm_score_changed – when, as a result of a new delivered order or as a result of the customer inactivity, the RFM Score changes; this may also trigger customer.rfm_group_changed
Job Events
You can subscribe on these events to know when Reveal is processing something:
- job.started – whenever Reveal starts processing a job; any job
- job.exited – whenever Reveal finishes processing a job and the job has errors (Reveal internal errors, or erros with some 3rd party integrations, …); any job
- job.done – whenever Reveal successfully finishes a job; still the job may have warnings and you should check them; the most common jobs with warnings you MUST check are the import ones, every entity (category, product, customer or order) that could not be imported is a warning, so you can have a successfully import job that has only warnings and it didn’t imported anything.
- job.[slug].started – you can also subscribe to know when a specific job starts processing and not any job; simply concatenate job. with the slug of the job you want and with the processing event you want; for example you want to know every time a customer import job has started you should subscribe to job.import_process_customer.started;
- job.[slug].exited – to know when a specific job has failed
- job.[slug].done – to know when a specific job is successfully finished; still you have to check the possible warnings; also if you want to know when a specific job is finished processing, successfully or not, you should subscribe to both .exited and .done events
Event Payload
Reveal will make a POST request to your endpoint, with the following body:
Payload for customer events
{ "_hook_id": "28aaa726-e44d-4659-a25a-fe9e6e117d42", "_payload": { "shop": "demo01", "customer_profile": {Object}, "customer_event": { "customer_eid": "012345_Abc", "customer_email": "[email protected]", "name": "nps_response", "properties": {Object}, "static_id": "nps_response_1476230400_0", "timestamp": "2016-10-12T00:00:00Z" } } }
We have also added the customer_profile to the webhook, in order to add a bit of context. The customer_profile object has the following fields:
"customer_profile": { "customer_eid": "", "email": "", "first_name": null, "last_name": null, "date_registered": "0001-01-01", "country": null, "region": null, "city": null, "gender": null, "yob": null, "accepts_marketing": null, "is_guest": 0, "was_guest": null, "anonymized_at": null, "custom_attributes": null, "cohort": null, "first_delivered_order_at": null, "first_delivered_order_placed_at": null, "first_delivered_order_recency": null, "delivered_orders_count": 0, "delivered_orders_amount": null, "last_delivered_order_at": null, "last_delivered_order_placed_at": null, "last_delivered_order_recency": null, "customer_canceled_orders_count": 0, "customer_canceled_orders_amount": null, "last_customer_canceled_order_at": null, "last_customer_canceled_order_recency": null, "shop_canceled_orders_count": 0, "shop_canceled_orders_amount": null, "last_shop_canceled_order_at": null, "last_shop_canceled_order_recency": null, "returned_orders_count": 0, "returned_orders_amount": null, "last_returned_order_at": null, "last_returned_order_recency": null, "last_order_at": null, "last_order_recency": null, "returned_rate": null, "customer_canceled_rate": null, "shop_canceled_rate": null, "rfm_r_percentile": null, "rfm_f_percentile": null, "rfm_m_percentile": null, "total_spending": null, "total_profit": null, "total_profit_percentile": null, "total_margin": null, "is_profitable": null, "adbt": null, "aov": null, "avg_ol_per_order": 0, "avg_qty_per_order": 0, "predicted_lifetime": null, "predicted_lifetime_value": null, "nps_last_response_score": null, "nps_last_responded_at": null, "nps_last_invited_at": null, "nps_last_invitation_responded": null, "nps_score_aggregated_3x": null, "nps_score_aggregated": null, "nps_segment": null, "nps_invitation_count": 0, "nps_response_count": 0, "nps_response_rate": 0, "nps_promoter_count": 0, "nps_passive_count": 0, "nps_detractor_count": 0, "nps_score_aggregated_3m": null, "nps_segment_3m": null, "nps_invitation_count_3m": 0, "nps_response_count_3m": 0, "nps_response_rate_3m": 0, "nps_promoter_count_3m": 0, "nps_passive_count_3m": 0, "nps_detractor_count_3m": 0, "nps_score_aggregated_6m": null, "nps_segment_6m": null, "nps_invitation_count_6m": 0, "nps_response_count_6m": 0, "nps_response_rate_6m": 0, "nps_promoter_count_6m": 0, "nps_passive_count_6m": 0, "nps_detractor_count_6m": 0, "nps_score_aggregated_12m": null, "nps_segment_12m": null, "nps_invitation_count_12m": 0, "nps_response_count_12m": 0, "nps_response_rate_12m": 0, "nps_promoter_count_12m": 0, "nps_passive_count_12m": 0, "nps_detractor_count_12m": 0, "previous_nps_segment": null, "nps_pre_last_response_score": null, "nps_pre_last_responded_at": null, "nps_pre_score_aggregated_3x": null, "nps_pre_score_aggregated": null, "nps_pre_segment": null, "nps_pre_response_count": 0, "nps_pre_promoter_count": 0, "nps_pre_passive_count": 0, "nps_pre_detractor_count": 0, "nps_pre_score_aggregated_3m": null, "nps_pre_segment_3m": null, "nps_pre_response_count_3m": 0, "nps_pre_promoter_count_3m": 0, "nps_pre_passive_count_3m": 0, "nps_pre_detractor_count_3m": 0, "nps_pre_score_aggregated_6m": null, "nps_pre_segment_6m": null, "nps_pre_response_count_6m": 0, "nps_pre_promoter_count_6m": 0, "nps_pre_passive_count_6m": 0, "nps_pre_detractor_count_6m": 0, "nps_pre_score_aggregated_12m": null, "nps_pre_segment_12m": null, "nps_pre_response_count_12m": 0, "nps_pre_promoter_count_12m": 0, "nps_pre_passive_count_12m": 0, "nps_pre_detractor_count_12m": 0, "previous_nps_pre_segment": null, "r_raw": null, "f_raw": null, "m_raw": null, "bc_profit": null, "rfm_score": null, "r_score": null, "f_score": null, "m_score": null, "rfm_group_id": null, "rfm_group_name": null, "previous_rfm_score": null, "previous_rfm_group_id": null, "rfm_group_last_modified_at": null, "rfm_score_last_modified_at": null, "rfm_last_computed_at": null, "profile_last_modified_at": null }
The customer_event object explained:
- customer_eid – the customer ID as you imported in Reveal; it’s maybe the id from your database, or some other string you can uniquely identify a customer in Reveal database; for Reveal it’s just an external id;
- customer_email – the email of the customer; if it’s anonymized you can identify the customer by customer_eid
- name – the name of the customer_event
- static_id – a unique ID of the event; in case Reveal triggers the same event twice it will have the same ID, and if you want you can store these IDs so you know you have processed them
- timestamp – the timestamp when the event occured, not when it was triggered; for example the nps_response event will have this timestamp the moment the customer submitted the response on survey; Reveal may trigger the event with some delay and the webhook may reach to you with another delay, but you may be sure this timestamp is the moment when the event actually occured in reality;
- properties – this is a different object for each of the customer_events, and it describes the event:
- for customer_event.nps_partial_response:
“properties”: {
“nps_score”: 10,
“order_eid”: “abc123”
}, - for customer_event.nps_pre_response:
“properties”: {
“nps_score”: 8,
“order_eid”: “abc123”
} - for customer_event.nps_response:
“properties”: {
“nps_score”: 8,
“order_eid”: “abc123”
} - for: customer_event.nps_segment_changed:
“properties”: {
“current_nps_segment”: “promoter”,
“previous_nps_segment”: “detractor”
}, - for customer_event.rfm_group_changed:
“properties”: {
“current_rfm_group_name”: “Soulmate”,
“previous_rfm_group_name”: “Breakup”
}, - for customer_event.rfm_importance_decreased:
“properties”: {
“current_rfm_group_name”: “Breakup”,
“current_rfm_group_rank”: 20,
“previous_rfm_group_name”: “Soulmate”,
“previous_rfm_group_rank”: 1
}, - for customer_event.rfm_importance_increased:
“properties”: {
“current_rfm_group_name”: “Soulmate”,
“current_rfm_group_rank”: 1,
“previous_rfm_group_name”: “Breakup”,
“previous_rfm_group_rank”: 20
}, - for customer_event.rfm_score_changed:
“properties”: {
“current_rfm_score”: “555”,
“previous_rfm_score”: “111”
},
- for customer_event.nps_partial_response:
Payload for Job Events
{ "_hook_id": "3ca095eb-9b4e-4892-af4a-cd2ecb6a4871", "_payload": { "shop": "demo01", "job": { "id": 0, "owner": "user", "sequence_id": 0, "sequence_order": 0, "slug": "import_fetch_customer", "input_path": "", "output_path": "", "status": "done", "email": null, "report": "Status: WARN\nMessage: ping\nWarnings: ping1\nping2\n", "created_at": "2016-10-12T00:00:00Z", "started_at": null, "finished_at": null, "canceled_at": null } } }
Example: Handling Security
Reveal authenticates each webhook call by sending the X-Reveal-Signature header. This header looks something like
t=1606769703,v1=d5987d68fdb939181327deec45027602cff2484889bb983370adda93a5a4b420
t – is the timestamp when the call was sent by Reveal; in order to prevent reply attacks you should check this timestamp is no older than 5 minutes let’s say
v1 – is the version of the webhook module (only one version for the moment)
[hash] – is a HMAC, the signature you should check
Checking the HMAC
Let’s say you receive a request with the following raw body:
{"_hook_id":"04292e6c-206b-41e6-850d-97aa4ca2d97d","_payload":{"shop":"demo01","customer_profile":{"customer_eid":"","email":"","first_name":null,"last_name":null,"date_registered":"0001-01-01","country":null,"region":null,"city":null,"gender":null,"yob":null,"accepts_marketing":null,"is_guest":0,"was_guest":null,"anonymized_at":null,"custom_attributes":null,"cohort":null,"first_delivered_order_at":null,"first_delivered_order_placed_at":null,"first_delivered_order_recency":null,"delivered_orders_count":0,"delivered_orders_amount":null,"last_delivered_order_at":null,"last_delivered_order_placed_at":null,"last_delivered_order_recency":null,"customer_canceled_orders_count":0,"customer_canceled_orders_amount":null,"last_customer_canceled_order_at":null,"last_customer_canceled_order_recency":null,"shop_canceled_orders_count":0,"shop_canceled_orders_amount":null,"last_shop_canceled_order_at":null,"last_shop_canceled_order_recency":null,"returned_orders_count":0,"returned_orders_amount":null,"last_returned_order_at":null,"last_returned_order_recency":null,"last_order_at":null,"last_order_recency":null,"returned_rate":null,"customer_canceled_rate":null,"shop_canceled_rate":null,"rfm_r_percentile":null,"rfm_f_percentile":null,"rfm_m_percentile":null,"total_spending":null,"total_profit":null,"total_profit_percentile":null,"total_margin":null,"is_profitable":null,"adbt":null,"aov":null,"avg_ol_per_order":0,"avg_qty_per_order":0,"predicted_lifetime":null,"predicted_lifetime_value":null,"nps_last_response_score":null,"nps_last_responded_at":null,"nps_last_invited_at":null,"nps_last_invitation_responded":null,"nps_score_aggregated_3x":null,"nps_score_aggregated":null,"nps_segment":null,"nps_invitation_count":0,"nps_response_count":0,"nps_response_rate":0,"nps_promoter_count":0,"nps_passive_count":0,"nps_detractor_count":0,"nps_score_aggregated_3m":null,"nps_segment_3m":null,"nps_invitation_count_3m":0,"nps_response_count_3m":0,"nps_response_rate_3m":0,"nps_promoter_count_3m":0,"nps_passive_count_3m":0,"nps_detractor_count_3m":0,"nps_score_aggregated_6m":null,"nps_segment_6m":null,"nps_invitation_count_6m":0,"nps_response_count_6m":0,"nps_response_rate_6m":0,"nps_promoter_count_6m":0,"nps_passive_count_6m":0,"nps_detractor_count_6m":0,"nps_score_aggregated_12m":null,"nps_segment_12m":null,"nps_invitation_count_12m":0,"nps_response_count_12m":0,"nps_response_rate_12m":0,"nps_promoter_count_12m":0,"nps_passive_count_12m":0,"nps_detractor_count_12m":0,"previous_nps_segment":null,"nps_pre_last_response_score":null,"nps_pre_last_responded_at":null,"nps_pre_score_aggregated_3x":null,"nps_pre_score_aggregated":null,"nps_pre_segment":null,"nps_pre_response_count":0,"nps_pre_promoter_count":0,"nps_pre_passive_count":0,"nps_pre_detractor_count":0,"nps_pre_score_aggregated_3m":null,"nps_pre_segment_3m":null,"nps_pre_response_count_3m":0,"nps_pre_promoter_count_3m":0,"nps_pre_passive_count_3m":0,"nps_pre_detractor_count_3m":0,"nps_pre_score_aggregated_6m":null,"nps_pre_segment_6m":null,"nps_pre_response_count_6m":0,"nps_pre_promoter_count_6m":0,"nps_pre_passive_count_6m":0,"nps_pre_detractor_count_6m":0,"nps_pre_score_aggregated_12m":null,"nps_pre_segment_12m":null,"nps_pre_response_count_12m":0,"nps_pre_promoter_count_12m":0,"nps_pre_passive_count_12m":0,"nps_pre_detractor_count_12m":0,"previous_nps_pre_segment":null,"r_raw":null,"f_raw":null,"m_raw":null,"bc_profit":null,"rfm_score":null,"r_score":null,"f_score":null,"m_score":null,"rfm_group_id":null,"rfm_group_name":null,"previous_rfm_score":null,"previous_rfm_group_id":null,"rfm_group_last_modified_at":null,"rfm_score_last_modified_at":null,"rfm_last_computed_at":null,"profile_last_modified_at":null},"customer_event":{"customer_eid":"123aaaa","customer_email":"[email protected]","name":"nps_segment_changed","properties":{"current_nps_segment":"current","previous_nps_segment":"previous"},"static_id":"nps_segment_changed_1606769702_123aaaa","timestamp":"2020-11-30T20:55:02.1857715Z"}}}
and the following X-Reveal-Signature header:
t=1606769703,v1=d5987d68fdb939181327deec45027602cff2484889bb983370adda93a5a4b420
The secret for this webhook is abcd1234 (obviously, please choose a stronger password for real cases).
- you concatenate the timestamp with dot and with rawBody: signedPayload = timestamp + ‘.’ + rawBody
- you compute the expected HMAC using sha256 hash function: expectedHmac = hash_hmac(‘sha256’, signedPayload, secret);
- you compare the expected HMAC with the HMAC received in header; use timing attack safe string comparison methods: isAuthentic = hash_equals(‘d5987d68fdb939181327deec45027602cff2484889bb983370adda93a5a4b420’, expectedHmac);
We also provide a simple method for handling request signature in our Reveal PHP SDK. Here is a small snippet that demonstrates how your app should check the webhook and respond back to Reveal.
https://bitbucket.org/mktz/reveal-sdk/raw/7cc77e2b7812d41e9807ac8c80863629918adb20/src/.examples/webhook_handle.php
Test webhook (PING)
As you develop the integration with Reveal webhooks, you may want to see an actual request or event test. We prepared the Ping functionality for a webhook, just for these cases.
You can ping a webhook, and it your endpoint will be called with an example event, first one in the list of events the webhook is subscribed. Or you can specify an event and we will send you an example of that event, still the event must be in the list the webhook is subscribed to.
Always check the X-REVEAL-TEST header on the request, it tells you if the request is a Ping or a real webhook call.
Things you need to consider
Reveal can trigger millions of events per day, so the system is designed as “best effort” – we don’t afford to store the events and retry later.
So you need to be prepared when Reveal will “flood” you with requests, some of them may be parallel.
Your app should respond to Reveal in less than 5 seconds, or else Reveal will close the request and consider it failed.
If the webhook fails to respond correctly, Reveal will disable the webhook after 24 hours.
If you don’t respond at all or you respond with 4XX HTTP Status Code, Reveal will disable the webhook immediately.
If you respond with 5XX HTTP Status Code, Reveal will allow you 3 days to fix the problem until it will disable the webhook.
The only “benefit of the doubt” Reveal will award, is when the webhook is not successful but it’s not marked as failed yet; Reveal will retry the call after a delay of 5 seconds. If this time everything is OK, it will move on to the next event as nothing happened. But if it fails, the webhook will be marked as failed, you will receive an email (if you have configured it on the webhook), and Reveal will not perform any retries on this webhook until after the webhook is marked as OK. Still, Reveal will call your endpoint on every event, just it will not attempt to make a retry. On the first occasion the webhook responds correctly and in due time, the webhook will be automatically marked as OK.
Additional details needed?
Please don’t hesitate to reach out to our amazing customer support team if you feel you need any additional information, or any kind of help in utilising the webhooks.
We’d also appreciate any feedback. 🙂
Was this post helpful?