Webhooks

Webhooks are powerful tools for reacting to events regarding your users' activity. If you have in-house systems that you would like to use to monitor new subscriptions, webhooks are the best way to do it.

This guide will walk you through setting up webhooks with OpenPay. By implementing webhooks, you can automatically respond to important events like new customers and subscription updates, ensuring a seamless experience for your users and efficient management of your business.

link icon Sign in to OpenPay
  1. Click the Developer Icon on the right of the top navigation bar and select Webhooks

  2. Click Create webhook

  3. Enter your webhook endpoint URL, give it a short name, and specify which events you want the endpoint to receive. We select all events for you by default, but you can narrow it down to a specific set of events.

Each request to your webhook endpoint will be of content type application/json with the following content schema:

{
  "id": "Unique ID for this event.",
  "webhook_id": "Unique ID for the webhook endpoint that this event was sent to.",
  "data": "JSON-serialized data for the event."
}

The data key is a JSON-serialized string representation of the object associated with the event. For example, if the event is a subscription creation, data will contain a snapshot of the created subscription object as a JSON string.

Example payloadCopied!

This is an example payload for a customer update event:

{
  "id": "aaaabbbb-cccc-dddd-eeee-ffffgggghhhh",
  "webhook_id": "webhook_endpoint_abcdefg12345678",
  "data": "{\"id\": \"event_dev_abcdefg12345678\", \"object\": \"event\", ...}"
}

The value of data can be parsed into the following JSON object:

{
  "id": "event_dev_abcdefg12345678",
  "object": "event",
  "created_at": "2024-06-02T00:15:10.020202-07:00",
  "updated_at": "2024-06-02T00:15:10.020202-07:00",
  "is_deleted": false,
  "account_id": "account_dev_abcdefg12345678",
  "type": "customer.updated",
  "data": {
    "id": "cus_dev_abcdefg12345678",
    "email": "john.doe@example.ai",
    "object": "customer",
    "address": {
      "city": "Boise",
      "line1": "2512 Walnut Avenue",
      "line2": "",
      "line3": "",
      "state": "ID",
      "country": "US",
      "zip_code": "83716"
    },
    "discount": null,
    "last_name": "Doe",
    "account_id": "account_dev_abcdefg12345678",
    "created_at": "2024-06-02T00:14:57.575457-07:00",
    "first_name": "John",
    "is_deleted": false,
    "updated_at": "2024-06-02T00:14:57.575457-07:00",
    "balance_atom": null,
    "subscriptions": []
  },
  "data_previous": null,
  "request_id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "request_idempotency_key": null,
  "pending_webhooks": 1
}

The exact schema of data depends on the type of event and matches that of the corresponding object in the API reference. For example, the customer event above contains data which follows the customer object schema.

You have the option of specifying which object events your webhook should be notified about. These event types are declared in the type field of the parsed data string in the event payload. In our example in the Webhook payload format tab, it's customer.updated.

We emit created, updated, and deleted events for each of your objects, which means you will receive events such as coupon.created and customer.updated as these operations happen. The supported objects are:

  • account

  • api_token

  • charge

    • In addition to created, updated, and deleted, we also emit succeeded, failed, pending and refunded for different statuses of charge lifecycle.

  • coupon

  • credit_note

  • credit_note_item

  • customer

  • customer_balance_transaction

  • discount

  • invite

  • invite_item_discount

  • invoice

    • In addition to created, updated, and deleted, we also emit upcoming, finalized, paid, past_due, voided and uncollectible for different statuses of invoice lifecycle.

  • invoice_discount

  • invoice_item

  • payment_intent

    • In addition to created, updated, and deleted, we also emit succeeded, processing, requires_action and failed for different statuses of payment_intent lifecycle.

  • payment_method

  • payment_processor

  • price

  • product

  • promotion_code

  • refund

  • customer.subscription

    • In addition to created, updated, and deleted, we also emit trial_will_end, trialing, activated, past_due, paused, resumed and canceled for different statuses of subscription lifecycle.

    • Note: customer.subscription.created will be emitted for mere creation of a subscription in incomplete state. If the intention to listen to an event is customer successfully subscribing to a product, then one should listen for customer.subscription.trialing and customer.subscription.activated according to respective subscription status.

  • subscription_item

  • user

  • user_login

  • user_record

  • user_record_summary

  • webhook_endpoint

For your security, OpenPay signs every webhook request sent to your endpoints. The signature is included in the request's signature-digest header, and it follows the format

t=TIMESTAMP,SIGNATURE_VERSION=SIGNATURE[,SIGNATURE_VERSION=SIGNATURE_2,SIGNATURE_VERSION=SIGNATURE_3,...]
  • TIMESTAMP is an integer representing the POSIX timestamp (seconds since epoch) of the event creation.

  • SIGNATURE_VERSION is v1 right now, but this could change in the future.

  • SIGNATURE is an HMAC SHA256 digest of the string

    TIMESTAMP.DATA
    • DATA is the value of the data key in the payload.

  • There is one signature for every available secret, but you should only need to verify at least one.

Getting the secretCopied!

The signatures are generated using automatically-generated secret keys. You can obtain the secret used to sign payloads for your webhook from the Dashboard.

  1. On the Developers page (where you created your webhook), click on the webhook that you want to get the secret for.

  2. On the webhook’s details page, click the three dots to the right of the webhook name. Select Show Secret from the menu that appears.

  3. You can now copy the secret to clipboard.

If you need to get the secret programmatically (using something like cURL), do the following:

  1. Obtain an OpenPay API token with the ADMIN role. (Webhook secrets can only be accessed using tokens of this role.)

  2. Access the webhook secret from the API:

curl -X GET \
  -H 'Authorization: Bearer SECRET_KEY' \
  https://connto.getopenpay.com/webhook-endpoints/webhook_endpoint_XXXXXXXXXXX/reveal_secret

# JSON response:
# {
#   "id": "webhook_endpoint_XXXXXXXXXXX",
#   // ...
#   "secret": "whsec_XXXXXXXXXXXXXXXX"
# }

Verifying a signatureCopied!

To verify a signature, you will need to generate your own signature using the same data as above and compare the two digests.

from getopenpay.client import ApiKeys, OpenPayClient
from getopenpay.utils.webhook_utils import InvalidSignatureError

client = OpenPayClient(ApiKeys(...))
                       
secret_key = 'whsec_XXXXXXXXXXXXXXXX'
event_data = "{\"id\": \"event_dev_abcdefg12345678\", \"object\": \"event\", ...}"
signature_digest = request.headers.get('signature-digest', None)

try:
  client.webhook_utils.validate_payload(
    event_data=event_data,
    signature_digest=signature_digest,
    secret=secret_key
  )
except InvalidSignatureError:  # or ValueError
  # ...
import hmac
from hashlib import sha256

secret_key = 'whsec_XXXXXXXXXXXXXXXX'

content = f'{TIMESTAMP}.{DATA}'
signature = hmac.new(
    secret_key.encode('utf-8'),
    content.encode('utf-8'),
    digestmod=sha256
).hexdigest()

expected_signature = request.headers.get('signature-digest')
is_valid = hmac.compare_digest(signature, expected_signature)

HTTPS onlyCopied!

Since events can potentially contain sensitive data, such as customer information, we require that all webhooks use SSL (i.e., use HTTPS). You will not be able to add insecure HTTP URLs as webhook endpoints.

POST requests onlyCopied!

OpenPay makes requests to webhook endpoints using the HTTP POST method. Make sure that your webhook endpoint is equipped to listen for this type of request.

Other request types like GET are currently not supported.