/aiopaypal

Async Paypal Client

Primary LanguagePythonMIT LicenseMIT

Logo

Software License Downloads Monthly Downloads Code style: black

Aiopaypal

Async Wrapper for Paypal's REST API

Setup ⚙️

$ pip install aiopaypal

Dependencies

  • aiohttp
  • aiofiles
  • pyopenssl

Usage

Init

from aiopaypal import Paypal

aiopaypal = Paypal(
    mode='live',
    client_id='client_id',
    client_secret='client_secret',
)

Create a user subscription

1. Create a payment experience (Optional) (Do only once):

payment_experience = await aiopaypal.post(
    url='/v1/payment-experience/web-profiles'
    json={
        'name': 'Payment profile name',
        'presentation': {
            'logo_image': 'https://brand-logo.png,
            'brand_name': 'Brand Name'
        },
        'flow_config': {
            'landing_page_type': 'Billing',
            'user_action': 'commit',
            'return_uri_http_method': 'GET'
        },
        'input_fields': {
            'no_shipping': 1,  # No shipping address (digital goods)
        },
        'temporary': False
    }

)

2. Create a billing plan (Where you specify the details of your plan) (Do only once):

billing_plan = await aiopaypal.post(
    url='/v1/payments/billing-plans',
    json={
        "name": 'Name of the plan',
        "description": 'Description of the plan',
        "type": "INFINITE",
        "payment_definitions": [
            {
                "name": 'Name of the payment,
                "cycles": "0",
                "frequency": "MONTH",
                "frequency_interval": "1",
                "type": "REGULAR",
                "amount": {
                    "value": str(123),
                    "currency": 'usd'
                },
            }
        ],
        "merchant_preferences": {
            "setup_fee": {
                "value": str(123),
                "currency": currency
            },
            "auto_bill_amount": "yes",  # Default "NO",
            'return_url': 'https://example.com/payment/success-callback',
            'cancel_url': 'https://example.com/payment/cancel-callback,
            "initial_fail_amount_action": "cancel",  # Default CONTINUE
            "max_fail_attempts": "3",
            "auto_bill_amount": "YES",
        }
    }
)

3. Create webhooks to listen for subscription events (Do only once):

hook_profile = await aiopaypal.post(
    url='/v1/notifications/webhooks',
    json={
        url='https://example.com/webhook/',
        event_types=[
            {'name': 'BILLING.SUBSCRIPTION.CANCELLED'},
            {'name': 'BILLING.SUBSCRIPTION.SUSPENDED'},
            {'name': 'BILLING.SUBSCRIPTION.RE-ACTIVATED'},
        ]
    }
)

4. Create a billing agreement (Where you bind a user to the billing plan created at "2.") and execute it:

async def create_agreement():
    return await aiopaypal.post(
        url='',
        json={
            'name': 'Agreement name',
            'description': 'Agreement Description',
            'start_date': (
                datetime.datetime.utcnow() + \
                datetime.timedelta(days=1)
            ).isoformat()[:-7] + 'Z'  # The start date must be no less than 24 hours after the current date as the agreement can take up to 24 hours to activate.
            'plan': {
                'id': billing_plan['id']
            },
            'payer': {
                'payment_method': 'paypal',
                'payer_info': {
                    'email': 'email@email.email'
                }
            }
        }
    )

def get_execute_from_response(response):
    for link in response['links']:
        if link['rel'] == 'execute':
            return link['href']

4.1 Create an agreement:

@app.route('/create-agreement)
async def create_agreement():
    billing_agreement = await create_agreement()
    return make_user_open(get_execute_from_response(billing_agreement))

4.2 Activate on success:

# Second step (user callback)
@app.route('/success-callback', methods=['GET'])
async def finalize_agreement(request):
    token = request.args.get('token')

    user_id = request['session']['user_id']

    active_agreement = await aiopaypal.post(
        '/v1/payments/billing-agreements/{}/agreement-execute'.format(
            token
        ),
        extra_headers={'Content-Type': 'application/json'}
    )

    if active_agreement['state'].lower() != 'active' and \
        active_agreement['state'].lower() != 'pending':
    else:
        await store_user_agreement_id(user_id, active_agreement['id'])
        activate_premium_product(user_id)

    return_to_user('Payment {}'.format(active_agreement['state']))

5. Listen to agreement changes:

@app.route('/webhook', methods=['POST', 'GET'])
async def hook(request):
    try:
        await aiopaypal.verify_from_headers(
            webhook_id=webhook['id'],  # webhook response from "3."
            event_body=request.body.decode(),
            headers=headers
        )
    except PaypalError as e:
        logger.exception(e)
        return
    else:
        event = request.json

        event_type = event.get('event_type')

        agreement_id = event['resource']['id']

        if event_type == 'BILLING.SUBSCRIPTION.SUSPENDED':
            logger.info('Billing agreement {} suspended'.format(agreement_id))
            await suspend_agreement_by_agreement_id(
                agreement_id
            )

        elif event_type == 'BILLING.SUBSCRIPTION.CANCELLED':
            logger.info('Billing agreement {} cancelled'.format(agreement_id))
            await cancel_agreement_by_id(
                agreement_id
            )

        elif event_type == 'BILLING.SUBSCRIPTION.RE-ACTIVATED':
            logger.info(
                'Agreement with ID: {} REACTIVATED'.format(
                    agreement_id
                )
            )
            await reactivate_agreement_by_id(
                agreement_id
            )

        elif event_type == 'PAYMENT.SALE.PENDING' or \
            event_type == 'PAYMENT.ORDER.CREATED' or \
            event_type == 'BILLING.SUBSCRIPTION.CREATED':
            logger.info('Payment/Subscription Created')

        else:
            logger.critical(
                'Got unexpected event type {}'.format(event['resource']['id'])
            )

    finally:
        # must return 200, else Paypal won't stop sending
        return response.text('OK')

Create a user payment

Figured it out? Help others and make a pull request :)