Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,71 @@ For full documentation, see our `API reference`_.

.. _API reference: https://developer.gocardless.com/api-reference

Handling webhooks
`````````````````

GoCardless supports webhooks, allowing you to receive real-time notifications when
things happen in your account, so you can take automatic actions in response, for example:

* When a customer cancels their mandate with the bank, suspend their club membership
* When a payment fails due to lack of funds, mark their invoice as unpaid
* When a customer's subscription generates a new payment, log it in their "past payments" list

The client allows you to validate that a webhook you receive is genuinely from GoCardless,
and to parse it into Event objects which are easy to work with:

.. code:: python

import gocardless_pro

# When you create a webhook endpoint, you can specify a secret. When GoCardless sends
# you a webhook, it will sign the body using that secret. Since only you and GoCardless
# know the secret, you can check the signature and ensure that the webhook is truly
# from GoCardless.
webhook_endpoint_secret = os.environ['GOCARDLESS_WEBHOOK_ENDPOINT_SECRET']

# In your webhook handler (e.g. Flask route)
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
request_body = request.data
signature_header = request.headers.get('Webhook-Signature')

try:
events = gocardless_pro.Webhook.parse(
request_body,
signature_header,
webhook_endpoint_secret
)

for event in events:
print(event.id)

return '', 200
except gocardless_pro.errors.InvalidSignatureError:
# The webhook doesn't appear to be genuinely from GoCardless
return '', 498

Accessing the webhook ID
''''''''''''''''''''''''

If you need to access the webhook ID for debugging purposes, you can use ``parse_with_meta`` instead:

.. code:: python

result = gocardless_pro.Webhook.parse_with_meta(
request_body,
signature_header,
webhook_endpoint_secret
)
events = result.events
webhook_id = result.webhook_id # e.g. "WB123" - useful for debugging

Note: The webhook ID is intended for debugging and logging purposes only. It should not be
used for deduplication - instead, use the event IDs to deduplicate, as each event has a unique
ID that remains consistent if the same event is sent multiple times.

For more details on working with webhooks, see our `"Getting Started" guide <https://developer.gocardless.com/getting-started/api/introduction/?lang=python>`_.


Available resources
```````````````````
Expand Down
2 changes: 1 addition & 1 deletion gocardless_pro/services/mandate_pdfs_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def create(self,params=None, headers=None):

To generate a PDF mandate in another language, set the
`Accept-Language` header when creating the PDF mandate to the relevant
[ISO 639-1](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
[ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
language code supported for the scheme.

| Scheme | Supported languages
Expand Down
2 changes: 1 addition & 1 deletion gocardless_pro/services/outbound_payments_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def approve(self,identity,params=None, headers=None):
"""Approve an outbound payment.

Approves an outbound payment. Only outbound payments with the
pending_approval status can be approved.
"pending_approval" status can be approved.

Args:
identity (string): Unique identifier of the outbound payment.
Expand Down
36 changes: 36 additions & 0 deletions gocardless_pro/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,48 @@
from gocardless_pro.resources.event import Event
from gocardless_pro.errors import InvalidSignatureError


class WebhookParseResult:
"""
Represents the result of parsing a webhook, containing both the events
and the webhook metadata.
"""
def __init__(self, events, webhook_id):
self._events = events
self._webhook_id = webhook_id

@property
def events(self):
"""Returns the list of events included in the webhook."""
return self._events

@property
def webhook_id(self):
"""Returns the webhook ID from the meta field, or None if not present."""
return self._webhook_id


def parse(body, webhook_secret, signature_header):
_verify_signature(body, webhook_secret, signature_header)

events_data = json.loads(to_string(body))
return [Event(attrs, None) for attrs in events_data['events']]


def parse_with_meta(body, webhook_secret, signature_header):
"""
Validates that a webhook was genuinely sent by GoCardless, and then parses
it into a WebhookParseResult containing both the events and the webhook ID
from the meta field.
"""
_verify_signature(body, webhook_secret, signature_header)

parsed = json.loads(to_string(body))
events = [Event(attrs, None) for attrs in parsed['events']]
webhook_id = parsed.get('meta', {}).get('webhook_id')

return WebhookParseResult(events, webhook_id)

def _verify_signature(body, key, expected_signature):
digest = hmac.new(
to_bytes(key),
Expand Down
1 change: 1 addition & 0 deletions tests/code_samples/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Code sample tests
53 changes: 53 additions & 0 deletions tests/code_samples/balances_code_samples_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# WARNING: Do not edit by hand, this file was generated by Crank:
#
# https://github.com/gocardless/crank
#

# Code Sample Tests
# These tests verify that the documentation code samples are syntactically valid
# and can execute against a mocked API without errors.
#
# IMPORTANT: These tests do NOT verify business logic - they only verify that
# the code samples compile and execute without syntax errors.

import re
import sys
from io import StringIO

import responses

import gocardless_pro






@responses.activate
def test_balances_list_code_sample():
# Convert :param placeholders to regex wildcards for flexible matching
stub_url = '/balances'
url_pattern = re.compile('https://api.gocardless.com' + re.sub(r':[\w]+', r'[^/]+', stub_url))
# Mock response - repeat multiple times to handle code samples with multiple API calls
response_body = { 'balances': [{}], 'meta': { 'cursors': {'after': None, 'before': None}, 'limit': 50 } }
for _ in range(5):
responses.add(
'GET',
url_pattern,
json=response_body,
status=200
)

client = gocardless_pro.Client(access_token='SECRET_TOKEN', environment='live')

# Suppress stdout from code samples that use print
old_stdout = sys.stdout
sys.stdout = StringIO()
try:
client.balances.list(params={
"creditor": "CR123",
}).records
finally:
sys.stdout = old_stdout


53 changes: 53 additions & 0 deletions tests/code_samples/bank_account_details_code_samples_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# WARNING: Do not edit by hand, this file was generated by Crank:
#
# https://github.com/gocardless/crank
#

# Code Sample Tests
# These tests verify that the documentation code samples are syntactically valid
# and can execute against a mocked API without errors.
#
# IMPORTANT: These tests do NOT verify business logic - they only verify that
# the code samples compile and execute without syntax errors.

import re
import sys
from io import StringIO

import responses

import gocardless_pro






@responses.activate
def test_bank_account_details_get_code_sample():
# Convert :param placeholders to regex wildcards for flexible matching
stub_url = '/bank_account_details/:identity'
url_pattern = re.compile('https://api.gocardless.com' + re.sub(r':[\w]+', r'[^/]+', stub_url))
# Mock response - repeat multiple times to handle code samples with multiple API calls
response_body = { 'bank_account_details': {} }
for _ in range(5):
responses.add(
'GET',
url_pattern,
json=response_body,
status=200
)

client = gocardless_pro.Client(access_token='SECRET_TOKEN', environment='live')

# Suppress stdout from code samples that use print
old_stdout = sys.stdout
sys.stdout = StringIO()
try:
client.bank_account_details.get("BA123",
headers={'Gc-Key-Id': 'PK123'}
)
finally:
sys.stdout = old_stdout


Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# WARNING: Do not edit by hand, this file was generated by Crank:
#
# https://github.com/gocardless/crank
#

# Code Sample Tests
# These tests verify that the documentation code samples are syntactically valid
# and can execute against a mocked API without errors.
#
# IMPORTANT: These tests do NOT verify business logic - they only verify that
# the code samples compile and execute without syntax errors.

import re
import sys
from io import StringIO

import responses

import gocardless_pro






@responses.activate
def test_bank_account_holder_verifications_create_code_sample():
# Convert :param placeholders to regex wildcards for flexible matching
stub_url = '/bank_account_holder_verifications'
url_pattern = re.compile('https://api.gocardless.com' + re.sub(r':[\w]+', r'[^/]+', stub_url))
# Mock response - repeat multiple times to handle code samples with multiple API calls
response_body = { 'bank_account_holder_verifications': {} }
for _ in range(5):
responses.add(
'POST',
url_pattern,
json=response_body,
status=200
)

client = gocardless_pro.Client(access_token='SECRET_TOKEN', environment='live')

# Suppress stdout from code samples that use print
old_stdout = sys.stdout
sys.stdout = StringIO()
try:
client.bank_account_holder_verifications.create(params={
"type": "confirmation_of_payee",
"links": {
"bank_account": "BA123"
}
})
finally:
sys.stdout = old_stdout




@responses.activate
def test_bank_account_holder_verifications_get_code_sample():
# Convert :param placeholders to regex wildcards for flexible matching
stub_url = '/bank_account_holder_verifications/:identity'
url_pattern = re.compile('https://api.gocardless.com' + re.sub(r':[\w]+', r'[^/]+', stub_url))
# Mock response - repeat multiple times to handle code samples with multiple API calls
response_body = { 'bank_account_holder_verifications': {} }
for _ in range(5):
responses.add(
'GET',
url_pattern,
json=response_body,
status=200
)

client = gocardless_pro.Client(access_token='SECRET_TOKEN', environment='live')

# Suppress stdout from code samples that use print
old_stdout = sys.stdout
sys.stdout = StringIO()
try:
client.bank_account_holder_verifications.get("BAHV123")
finally:
sys.stdout = old_stdout


Loading
Loading