skip to Main Content

I am trying to start an eBay API in Python and I can’t find a single answer as to how to get an API key with eBay’s new requirements of "Account Deletion/Closure Notifications." Here’s the link: https://developer.ebay.com/marketplace-account-deletion

Specifically, I am told that "Your Keyset is currently disabled" because I have not completed whatever process is needed for this marketplace account deletion/closure notification.

The problems?

  1. I have no idea if I need this.
  2. I have no idea how to actually do this.

Re: 1. It looks like this is for anyone who stores user data. I don’t think that’s me intentionally because I really just want to get sold data and current listings, but is it actually me?

Re: 2. I don’t understand how to validate it and send back the proper responses. I’ve gotten quite good at python but I’m lost here.

eBay forums are completely useless and I see no one with an answer to this. Any help is greatly appreciated.

5

Answers


  1. Re: 1. Same. Here’s my interpretation: In order to use their APIs, you need to provide (and configure) your own API, so they can communicate with you —programatically— and tell you what users have asked to have their accounts/data deleted.

    Re: 2. To handle their GET and POST requests, I guess you’ll need to configure a website’s URL as an API endpoint. In Django, I might use something like this (untested) code:

    import hashlib
    import json
    from django.http import (
        HttpResponse,
        JsonResponse,
        HttpResponseBadRequest
    )
    
    
    def your_api_endpoint(request):
        """
        API Endpoint to handle the verification's challenge code and
        receive eBay's Marketplace Account Deletion/Closure Notifications.
        """
        # STEP 1: Handle verification's challenge code
        challengeCode = request.GET.get('challenge_code')
        if challengeCode is not None:
           # Token needs to be 32-80 characters long 
           verificationToken = "your-token-012345678901234567890123456789"
           # URL needs to use HTTPS protocol
           endpoint_url = "https://your-domain.com/your-endpoint"
           # Hash elements need to be ordered as follows
           m = hashlib.sha256((challengeCode+verificationToken+endpoint_url).encode('utf-8'))
           # JSON field needs to be called challengeResponse
           return JsonResponse({"challengeResponse": m.hexdigest()}, status=200)
        
        # STEP 2: Handle account deletion/closure notification
        elif request.method == 'POST':
            notification_details = json.loads(request.body)
            # Verify notification is actually from eBay
            # ...
            # Delete/close account
            # ...
            # Acknowledge notification reception
            return HttpResponse(status=200)
    
        else:
            return HttpResponseBadRequest()
    
    

    If you find the answer to question number one, please do let me know.

    Login or Signup to reply.
  2. This is how I am dealing with the ebay notification requirement using Python3 cgi. Because bytes are sent, cannot use cgi.FieldStorage()

    import os
    import sys
    import hashlib
    import json
    from datetime import datetime
    from html import escape
    
    import cgi
    import cgitb
    
    import io
    
    include_path = '/var/domain_name/www'
    
    sys.path.insert(0, include_path)
    
    cgitb.enable(display=0, logdir=f"""{include_path}/tmp_errors""") # include_path is OUTDIR
    
    dt_now = datetime.now()
    current_dt_now = dt_now.strftime("%Y-%m-%d_%H-%M-%S")
    
    
    def enc_print(string='', encoding='utf8'):
        sys.stdout.buffer.write(string.encode(encoding) + b'n')
    
    
    html = ''
    
    challengeCode = ''
    
    
    # GET
    myQuery = os.environ.get('QUERY_STRING')
    if myQuery.find('=') != -1: 
        pos = myQuery.find('=')
        
        var_name = myQuery[:pos]
        var_val = myQuery[pos+1:]
        
        challengeCode = var_val
    
    
    # POST  
    if os.environ.get('CONTENT_LENGTH') != None:
        totalBytes=int(os.environ.get('CONTENT_LENGTH'))
        reqbytes=io.open(sys.stdin.fileno(),"rb").read(totalBytes)
    
    
    if challengeCode != '' :
        """
        API Endpoint to handle the verification's challenge code and
        receive eBay's Marketplace Account Deletion/Closure Notifications.
        """
        # STEP 1: Handle verification's challenge code
    
        # Token needs to be 32-80 characters long 
        verificationToken = "0123456789012345678901234567890123456789" #sample token
        # URL needs to use HTTPS protocol
        endpoint = "https://domain_name.com/ebay/notification.py" # sample endpoint
        # Hash elements need to be ordered as follows
        m = hashlib.sha256( (challengeCode+verificationToken+endpoint).encode('utf-8') )
        # JSON field needs to be called challengeResponse
        
        enc_print("Content-Type: application/json")
        enc_print("Status: 200 OK")
        enc_print()
        enc_print('{"challengeResponse":"' + m.hexdigest() + '"}')
        exit()
        
    else :
        
        #html += 'var length:' + str(totalBytes) + 'n'
        html += reqbytes.decode('utf-8') + 'n'
    
    
        # STEP 2: Handle account deletion/closure notification
    
        # Verify notification is actually from eBay
        # ...
        # Delete/close account
        # ...
        # Acknowledge notification reception
        
        with open( f"""./notifications/{current_dt_now}_user_notification.txt""", 'w') as f:
            f.write(html)
    
        enc_print("Content-Type: application/json")
        enc_print("Status: 200 OK")
        enc_print()
        exit()
    
    Login or Signup to reply.
  3. I’ve been trying @José Matías Arévalo code. It works except "STEP 2" branch – Django returns 403 error. This is because of by default Django uses CSRF middleware (Cross Site Request Forgery protection). To avoid 403 error we need to marks a view as being exempt from the protection as described here https://docs.djangoproject.com/en/dev/ref/csrf/#utilities so add couple strings in code:

    from django.views.decorators.csrf import csrf_exempt    
    
    @csrf_exempt
    def your_api_endpoint(request):
    

    And in my case I use url "https://your-domain.com/your-endpoint/" with slash symbol "/" at the end of url. Without this slash eBay doesn’t confirm subscription.

    Login or Signup to reply.
  4. I am using Flask and this is the code I have used:

    from flask import Flask, request
    import hashlib
    
    # Create a random verification token, it needs to be 32-80 characters long
    verification_token = 'a94cbd68e463cb9780e2008b1f61986110a5fd0ff8b99c9cba15f1f802ad65f9'
    endpoint_url = 'https://dev.example.com'
    
    app = Flask(__name__)
    
    # There will be errors if you just use '/' as the route as it will redirect eBays request
    # eBay will send a request to https://dev.example.com?challenge_code=123
    # The request will get redirected by Flask to https://dev.example.com/?challenge_code=123 which eBay will not accept
    endpoint = endpoint_url + '/test'
    
    # The Content-Type header will be added automatically by Flask as 'application/json'
    @app.route('/test')
    def test():
        code = request.args.get('challenge_code')
        print('Requests argument:', code)
    
        code = code + token + endpoint
        code = code.encode('utf-8')
        code = hashlib.sha256(code)
        code = code.hexdigest()
        print('Hexdigest:', code)
    
        final = {"challengeResponse": code}
        return final
    
    ## To run locally first use this:
    # app.run(port=29)
    
    Login or Signup to reply.
  5. Re: 1. You need to comply with eBay’s Marketplace Account Deletion/Closure Notification workflow if you are storing user data into your own database. For example, using eBay’s Buy APIs, you may get access to what users are selling on eBay (for ex. an eBay feed of products). If those eBay sellers decide they want to remove all of their personal data from eBay’s database, eBay is requesting you remove their data from your database as well. If you are NOT storing any eBay user data into your database, you do not need to comply. Here is where you can find more info: https://partnerhelp.ebay.com/helpcenter/s/article/Complying-with-the-eBay-Marketplace-Account-Deletion-Closure-Notification-workflow?language=en_US

    Re: 2. To be honest I’ve spent days trying to figure this out in Python (Django), but I have a solution now and am happy to share it with whoever else comes across this issue. Here’s my solution:

    import os
    import json
    import base64
    import hashlib
    import requests
    import logging
    from OpenSSL import crypto
    from rest_framework import status
    from rest_framework.views import APIView
    from django.http import JsonResponse
    
    logger = logging.getLogger(__name__)
    
    
    class EbayMarketplaceAccountDeletion(APIView):
        """
        This is required as per eBay Marketplace Account Deletion Requirements.
        See documentation here: https://developer.ebay.com/marketplace-account-deletion
        """
    
        # Ebay Config Values
        CHALLENGE_CODE = 'challenge_code'
        VERIFICATION_TOKEN = os.environ.get('VERIFICATION_TOKEN')
        # ^ NOTE: You can make this value up so long as it is between 32-80 characters.
        ENDPOINT = 'https://example.com/ebay_marketplace_account_deletion'
        # ^ NOTE: Replace this with your own endpoint
        X_EBAY_SIGNATURE = 'X-Ebay-Signature'
        EBAY_BASE64_AUTHORIZATION_TOKEN = os.environ.get('EBAY_BASE64_AUTHORIZATION_TOKEN')
        # ^ NOTE: Here's how you can get your EBAY_BASE64_AUTHORIZATION_TOKEN:
        # import base64
        # base64.b64encode(b'{CLIENT_ID}:{CLIENT_SECRET}')
    
        def __init__(self):
            super(EbayMarketplaceAccountDeletion, self).__init__()
    
        def get(self, request):
            """
            Get challenge code and return challengeResponse: challengeCode + verificationToken + endpoint
            :return: Response
            """
            challenge_code = request.GET.get(self.CHALLENGE_CODE)
            challenge_response = hashlib.sha256(challenge_code.encode('utf-8') +
                                                self.VERIFICATION_TOKEN.encode('utf-8') +
                                                self.ENDPOINT.encode('utf-8'))
            response_parameters = {
                "challengeResponse": challenge_response.hexdigest()
            }
            return JsonResponse(response_parameters, status=status.HTTP_200_OK)
    
        def post(self, request):
            """
            Return 200 status code and remove from db.
            See how to validate the notification here:
            https://developer.ebay.com/api-docs/commerce/notification/overview.html#use
            """
            # Verify notification is actually from eBay #
            # 1. Use a Base64 function to decode the X-EBAY-SIGNATURE header and retrieve the public key ID and signature
            x_ebay_signature = request.headers[self.X_EBAY_SIGNATURE]
            x_ebay_signature_decoded = json.loads(base64.b64decode(x_ebay_signature).decode('utf-8'))
            kid = x_ebay_signature_decoded['kid']
            signature = x_ebay_signature_decoded['signature']
    
            # 2. Call the getPublicKey Notification API method, passing in the public key ID ("kid") retrieved from the
            # decoded signature header. Documentation on getPublicKey:
            # https://developer.ebay.com/api-docs/commerce/notification/resources/public_key/methods/getPublicKey
            public_key = None
            try:
                ebay_verification_url = f'https://api.ebay.com/commerce/notification/v1/public_key/{kid}'
                oauth_access_token = self.get_oauth_token()
                headers = {
                    'Authorization': f'Bearer {oauth_access_token}'
                }
                public_key_request = requests.get(url=ebay_verification_url, headers=headers, data={})
                if public_key_request.status_code == 200:
                    public_key_response = public_key_request.json()
                    public_key = public_key_response['key']
            except Exception as e:
                message_title = "Ebay Marketplace Account Deletion: Error calling getPublicKey Notfication API."
                logger.error(f"{message_title} Error: {e}")
                return JsonResponse({}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    
            # 3. Initialize the cryptographic library to perform the verification with the public key that is returned from
            # the getPublicKey method. If the signature verification fails, an HTTP status of 412 Precondition Failed is returned.
            pkey = crypto.load_publickey(crypto.FILETYPE_PEM, self.get_public_key_into_proper_format(public_key))
            certification = crypto.X509()
            certification.set_pubkey(pkey)
            notification_payload = request.body
            signature_decoded = base64.b64decode(signature)
            try:
                crypto.verify(certification, signature_decoded, notification_payload, 'sha1')
            except crypto.Error as e:
                message_title = f"Ebay Marketplace Account Deletion: Signature Invalid. " 
                                f"The signature is invalid or there is a problem verifying the signature. "
                logger.warning(f"{message_title} Error: {e}")
                return JsonResponse({}, status=status.HTTP_412_PRECONDITION_FAILED)
            except Exception as e:
                message_title = f"Ebay Marketplace Account Deletion: Error performing cryptographic validation."
                logger.error(f"{message_title} Error: {e}")
                return JsonResponse({}, status=status.HTTP_412_PRECONDITION_FAILED)
    
            # Take appropriate action to delete the user data. Deletion should be done in a manner such that even the
            # highest system privilege cannot reverse the deletion #
            # TODO: Replace with your own data removal here
    
            # Acknowledge notification reception
            return JsonResponse({}, status=status.HTTP_200_OK)
    
        def get_oauth_token(self):
            """
            Returns the OAuth Token from eBay which can be used for making other API requests such as getPublicKey
            """
            url = 'https://api.ebay.com/identity/v1/oauth2/token'
            headers = {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Authorization': f"Basic {self.EBAY_BASE64_AUTHORIZATION_TOKEN}"
            }
            payload = 'grant_type=client_credentials&scope=https%3A%2F%2Fapi.ebay.com%2Foauth%2Fapi_scope'
            request = requests.post(url=url, headers=headers, data=payload)
            data = request.json()
            return data['access_token']
    
        @staticmethod
        def get_public_key_into_proper_format(public_key):
            """
            Public key needs to have n in places to be properly assessed by crypto library.
            """
            return public_key[:26] + 'n' + public_key[26:-24] + 'n' + public_key[-24:]
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search