Webhooks
Learn how to receive real-time notifications when events occur in your Pelcro account.
Quickstart
Get up and running with Pelcro webhooks in three steps:
1. Create an endpoint that accepts POST requests and returns a 200 status code:
// PHP
// Laravel
Route::post('/webhooks/pelcro', function (Request $request) {
$event = $request->all();
Log::info("Received {$event['type']} event: {$event['id']}");
// Acknowledge receipt immediately
// Process the event asynchronously (e.g., dispatch a queued job)
ProcessWebhookEvent::dispatch($event);
return response('OK', 200);
});# Flask
@app.route('/webhooks/pelcro', methods=['POST'])
def handle_webhook():
event = request.get_json()
print(f"Received {event['type']} event: {event['id']}")
# Acknowledge receipt immediately
# Process the event asynchronously (e.g., enqueue a task)
process_event.delay(event)
return 'OK', 200// Express
app.post('/webhooks/pelcro', (req, res) => {
const event = req.body;
console.log(`Received ${event.type} event: ${event.id}`);
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process the event asynchronously
processEvent(event);
});2. Register your URL in the Pelcro Platform under Settings > Notifications. Create a new webhook endpoint, enter your URL, then select the events you want to receive.
3. Verify signatures to confirm events are from Pelcro (see Signature Verification below).
That's it. Pelcro will now send an HTTP POST request to your endpoint every time a subscribed event occurs.
Configuration
You can register webhook endpoints from Settings > Notifications in the Pelcro Platform. Although you can subscribe to all events, this is not recommended as it will put unnecessary pressure on your server. Configure your webhook to listen to only the events required by your integration. You can also subscribe to webhooks via the API.
When registering the URL, it must respond with successful status code (2xx) to the POST requests in order to pass the initial validation.
Once you register a webhook URL with Pelcro, we will issue an HTTP POST request to the URL specified every time that event occurs, and your endpoint is expected to answer in a timely fashion by returning a successful status code (2xx). Any other status code is considered a failure.
Payload Structure
Every webhook event follows the same envelope structure:
{
"type": "subscription.created",
"id": "evt_ybR5i5QwSVvoWy28ndD4q6hA",
"created": 1542034347,
"data": {
"object": {
"object": "subscription",
"id": 6807,
"user_id": 40864,
"plan_id": 386,
"cancel_at_period_end": false,
"canceled_at": null,
"current_period_end": "2024-02-01T00:00:00.000000Z",
"current_period_start": "2024-01-01T00:00:00.000000Z"
}
}
}| Field | Type | Description |
|---|---|---|
type | string | The event type (e.g., subscription.created). |
id | string | A unique event identifier (format: evt_ + 24 characters). Use this for idempotency — if your endpoint receives the same id twice, you can safely skip the duplicate. |
created | integer | Unix timestamp of when the event was created. |
data.object | object | The resource that triggered the event. The object field inside identifies the resource type. |
For events that include change tracking (e.g., subscription.changed, customer.updated), the payload also includes a data.previous_attributes object with the field values before the change.
The webhook is sent with the following headers:
Content-Type: application/json
X-Pelcro-Hmac-SHA256: pdvcCMQA5298lNhqp7i52StRjHEKgY7Z77yU+f+lfGQ=
Events Ordering
The order in which events are delivered to endpoints is not related to the order they were triggered on our system. For example, the logical flow of events triggered while creating a subscription might be the following:
- subscription.created
- invoice.created
- charge.created
Your integration shouldn't assume delivery of events in the order specified in the example above, and needs to handle processing accordingly.
Delivery and Retries
Timeouts
Your endpoint must respond quickly. Pelcro enforces the following timeouts:
| Description | Timeout |
|---|---|
| Maximum time for your endpoint to accept the connection | 5 seconds |
| Maximum time for your endpoint to return a successful status code (2xx) | 20 seconds |
Your endpoint should acknowledge receipt of the event immediately by returning a 2xx status code, then process the event asynchronously in a background job or queue. Do not perform business logic (API calls, database operations, emails) before responding.
Retry Behavior
If your endpoint fails to return a 2xx status code, Pelcro will retry the delivery up to 3 attempts. Retries are processed immediately through our queue system with no delay between attempts. After all attempts are exhausted, the delivery is marked as permanently failed.
No exponential backoffRetries happen in quick succession. If your endpoint is temporarily down, all 3 attempts may fail before it recovers. Make sure your endpoint is highly available, and monitor your webhook delivery statuses in the Pelcro Platform.
Statuses
Various circumstances can cause webhook event delivery to enter a specific status. Refer to the table below for information on each status.
| Webhook status | Description |
|---|---|
| OK | The endpoint returned a successful status code (2xx). |
| ERR - 3xx | The endpoint attempted to redirect the request to another location, which is not handled and thus is considered a failure. |
| ERR - 4xx | The endpoint can't or won't process the request. This might occur when the server detects an error (400), when it has access restrictions (401, 403), or when the endpoint URL doesn't exist (404). |
| ERR - 5xx | The endpoint encountered an error while processing the request. |
| ERR - TLS | A secure connection could not be established to the endpoint. These errors can be caused by an issue with the SSL/TLS certificate or an intermediate certificate in the destination server's certificate chain. Performing a SSL server test might help identifying the cause of this error. |
| ERR - Unable to connect | A connection with the endpoint could not be established. This might happen if the endpoint cannot be resolved, or if the destination server did not accept the connection in a timely manner. |
| ERR - Timed out | The endpoint did not send back a successful status code (2xx) in a timely manner. |
| ERR | An unspecified, unhandled or unrecognized error occurred. |
Best Practices
- Return 2xx immediately, then process events in a background queue.
- Use the
idfield for idempotency. Your endpoint may receive the same event more than once. Store processed event IDs and skip duplicates. - Subscribe only to events you need. This reduces load on your server and simplifies your integration.
- Monitor delivery statuses in the Pelcro Platform to catch endpoint failures early.
Signature Verification
Pelcro signs every webhook event by including an HMAC-SHA256 signature in the X-Pelcro-Hmac-SHA256 header. This allows you to verify that events were sent by Pelcro, not by a third party.
How it works
- Extract the base64-encoded signature from the
X-Pelcro-Hmac-SHA256header. - Compute an HMAC-SHA256 hash using your webhook signing secret as the key and the raw JSON request body as the message.
- Compare your computed signature with the one from the header.
- Return a
2xxstatus code to acknowledge receipt. Pelcro will retry up to 3 times on failure.
Code Examples
// PHP
$payload = file_get_contents('php://input');
$secret = 'your_webhook_secret';
$expected = base64_encode(
hash_hmac('sha256', $payload, $secret, true)
);
$signature = $_SERVER['HTTP_X_PELCRO_HMAC_SHA256'] ?? '';
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
http_response_code(200);
// Process the event
$event = json_decode($payload, true);const crypto = require('crypto');
app.post('/webhooks/pelcro', (req, res) => {
const payload = JSON.stringify(req.body);
const secret = 'your_webhook_secret';
const signature = req.headers['x-pelcro-hmac-sha256'];
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('base64');
if (expected !== signature) {
return res.status(401).send('Invalid signature');
}
res.status(200).send('OK');
// Process the event
const event = req.body;
});import hmac
import hashlib
import base64
payload = request.get_data() # raw request body
secret = b'your_webhook_secret'
signature = request.headers.get('X-Pelcro-Hmac-SHA256', '')
expected = base64.b64encode(
hmac.new(secret, payload, hashlib.sha256).digest()
).decode()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
return 'OK', 200require 'openssl'
require 'base64'
payload = request.body.read
secret = 'your_webhook_secret'
signature = request.env['HTTP_X_PELCRO_HMAC_SHA256']
expected = Base64.strict_encode64(
OpenSSL::HMAC.digest('sha256', secret, payload)
)
unless Rack::Utils.secure_compare(expected, signature)
halt 401, 'Invalid signature'
end
status 200
Use constant-time comparisonAlways use a constant-time string comparison function (like
hash_equalsin PHP,hmac.compare_digestin Python, orRack::Utils.secure_comparein Ruby) to prevent timing attacks.
Events
Below is a complete list of all events we currently send to your configured webhook endpoint. Select an event's resource page to see the full JSON payload. Please note that we may change, delete, or add more at any time. While developing and maintaining your code, keep an eye on the changelog.
| Event | Description |
|---|---|
| address.created | A new address is created and added to a customer's account. |
| address.updated | An existing address is updated or modified on a customer's account. |
| Event | Description |
|---|---|
| bill.created | A new bill is created for a vendor. |
| bill.finalized | A draft bill is finalized and moved to open status. |
| bill.paid | A bill is marked as paid. |
| bill.updated | A bill is updated. |
| bill.deleted | A bill is deleted. |
| Event | Description |
|---|---|
| campaign.triggered | A campaign with a notification action has started processing. |
| campaign.run_completed | A campaign run has completed processing all matching subscriptions. |
| Event | Description |
|---|---|
| charge.failed | A charge attempt on a customer's payment method has failed. |
| charge.refunded | A charge has been refunded (fully or partially). |
| charge.succeeded | A customer's payment method has been successfully charged. |
| Event | Description |
|---|---|
| customer.created | A new customer registers and a user record is created. |
| customer.updated | A customer's profile information is updated. |
| customer.deleted | A customer record is deleted. |
| customer.PasswordResetRequestSubmitted | A customer requests a password reset. Contains the password reset token. |
| customer.password_update.succeeded | A customer successfully updates their password. |
| customer.email_verification_request | A customer is requested to verify their email after account creation. |
| customer.email_verification_succeeded | A customer successfully verifies their email. |
| customer.passwordless_login_request | A customer requests to log in via passwordless login. |
| Event | Description |
|---|---|
| export.requested | A data export job was submitted and processing has started. |
| export.completed | A data export has been generated and is ready for download. |
| export.downloaded | A data export file has been downloaded. |
| export.failed | A data export has failed to generate. |
| Event | Description |
|---|---|
| fraud_prevention.triggered | A fraud prevention rule with a Notify action has been triggered. |
| Event | Description |
|---|---|
| invoice.created | An invoice is created (e.g., when a subscription is created or renewed). |
| invoice.payment_action_required | An invoice payment attempt requires further customer action to complete. |
| invoice.payment_failed | An invoice payment attempt fails. |
| invoice.payment_succeeded | An invoice payment attempt succeeds. |
| invoice.upcoming | The configured number of days before an invoice is scheduled to be created is reached. |
| invoice.updated | An invoice is updated. |
| Event | Description |
|---|---|
| member.activated | A member activates their membership under a subscription they were invited to. |
| member.created | A member is invited to join a subscription. |
| member.updated | A member's information is updated. |
| member.deleted | A member is removed from a subscription. |
| member.subscription_updated | The subscription associated with a member is updated. |
| Event | Description |
|---|---|
| newsletter.created | A newsletter subscription is created for a customer. |
| newsletter.updated | A newsletter subscription is updated for a customer. |
| Event | Description |
|---|---|
| order.created | A new e-commerce order is created. |
| order.payment.succeeded | An order payment is successfully processed. |
| order.payment.failed | An order payment attempt fails. |
| Event | Description |
|---|---|
| source.created | A new payment method is added to a customer's account. |
| source.updated | A customer's payment method is updated. |
| source.expired | A customer's payment method has expired. |
| source.expiring | A customer's payment method is expiring within 30 days. |
| Event | Description |
|---|---|
| plan.created | A new subscription plan is created. |
| plan.updated | A subscription plan is updated. |
| Event | Description |
|---|---|
| product.created | A new product is created. |
| product.updated | A product is updated. |
| product.deleted | A product is deleted. |
| product_sku.created | A new product SKU (variant) is created. |
| product_sku.updated | A product SKU is updated. |
| product_sku.deleted | A product SKU is deleted. |
| Event | Description |
|---|---|
| subscription.created | A new subscription is created. |
| subscription.updated | A subscription is updated or transitions between statuses. |
| subscription.changed | A subscription is changed (plan or billing cycle modification). |
| subscription.canceled | A subscription is canceled. |
| subscription.expired | A subscription has expired. |
| subscription.renewed | A subscription renews and a new billing period begins. |
| subscription.trial_will_end | A subscription's trial period is ending within three days, or a trial is ended immediately. |
| subscription.gift_notification | It's time to notify the gift recipient, according to the gift date set during creation. |
| Event | Description |
|---|---|
| vendor.created | A new vendor is created. |
| vendor.updated | A vendor's profile is updated. |
| vendor.deleted | A vendor is deleted. |
Security
For added security, your integration should make sure the events it receives originate from Pelcro's infrastructure. See the list of outgoing IP addresses to whitelist.
Updated 14 days ago
