docs: def job(job_id=None):
      '''Submit a job. If no id is provided, a random id will be generated.
  
      :param job_type: Which kind of job should be run. Has to be one of the
          available job types.
      :type job_type: string
      :param api_key: An API key that is needed to execute the job. This could
          be a CKAN API key that is needed to write any data. The key will also be
          used to administer jobs. If you don't want to use a real API key, you can
          provide a random string that you keep secure.
      :type api_key: string
      :param data: Data that is send to the job as input. (Optional)
      :type data: json encodable data
      :param result_url: Callback url that is called once the job has finished.
          (Optional)
      :type result_url: url string
      :param metadata: Data needed for the execution of the job which is not
          the input data. (Optional)
      :type metadata: list of key - value pairs
  
      **Results:**
  
      :rtype: A dictionary with the following keys
      :param job_id: An identifier for the job
      :type job_id: string
      :param job_key: A key that is required to view and administer the job
      :type job_key: string
  
      :statuscode 200: no error
      :statuscode 409: an error occurred
  
      '''
      if not job_id:
          job_id = str(uuid.uuid4())
  
      # key required for job administration
      job_key = str(uuid.uuid4())
  
      ############# ERROR CHECKING ################
      try:
          input = flask.request.json
      except werkzeug.exceptions.BadRequest:
          return json.dumps({"error": "Malformed json"}), 409, headers
  
      # Idk why but this is needed for some libraries that
      # send malformed content types
      if (not input and
              'application/json' in flask.request.content_type.lower()):
          try:
              input = json.loads(flask.request.data)
          except ValueError:
              pass
      if not input:
          return json.dumps({"error": ('Not recognised as json, make '
                                       'sure content type is application/'
                                       'json')}), 409, headers
  
      ACCEPTED_ARGUMENTS = set(['job_type', 'data', 'metadata',
                                'result_url', 'api_key', 'metadata'])
      extra_keys = set(input.keys()) - ACCEPTED_ARGUMENTS
      if extra_keys:
          return json.dumps({"error": (
              'Too many arguments. Extra keys are {}'.format(
                  ', '.join(extra_keys)))}), 409, headers
  
      #check result_url here as good to give warning early.
      result_url = input.get('result_url')
      if result_url and not result_url.startswith('http'):
          return json.dumps({"error": "result_url has to start with http"}), \
              409, headers
  
      job_type = input.get('job_type')
      if not job_type:
          return json.dumps({"error": "Please specify a job type"}), 409, headers
  
      job_types = async_types.keys() + sync_types.keys()
  
      if job_type not in job_types:
          error_string = (
              'Job type {} not available. Available job types are {}'
          ).format(job_type, ', '.join(job_types))
          return json.dumps({"error": error_string}), 409, headers
  
      api_key = input.get('api_key')
      if not api_key:
          return json.dumps({"error": "Please provide your API key."}), 409, headers
  
      metadata = input.get('metadata', {})
      if not isinstance(metadata, dict):
          return json.dumps({"error": "metadata has to be a json object"}), \
              409, headers
      ############# END CHECKING ################
  
      synchronous_job = sync_types.get(job_type)
      if synchronous_job:
          return run_synchronous_job(synchronous_job, job_id, job_key, input)
      else:
          asynchronous_job = async_types.get(job_type)
          return run_asynchronous_job(asynchronous_job, job_id, job_key, input)