.. _oauth_service: OAuth Service ============= OAuth Service for REST allows for an integrator to authenticate a guest in a merchant program. Once authenticated, that guest may immediately obtain access to a variety of account-specific information via follow-up REST calls such as accountInformation, editUser, or checkinAccount. When integrating to applications in a B2C environment, this method is preferable for the following reasons: * Requiring guest authentication prior to access lessens the ability for fraud, errors, or questionable activity. * :http:post:`oauth/requestGuestToken.json` allows for card login without use of the registration code. This is particularly necessary for card batches that exist in Paytronix with no reg code. * Guests may now log in without knowing their full card numbers by instead giving several pieces of their previous registration info. Uniqueness conflicts may also be resolved by challenging the guest for additional information. .. contents:: Topics Key Terms --------- B2B Business to Business environment, where the integration to the PXS is originating in a secure and controlled environment, e.g. a merchant web site or in certain cases a store. B2C Business to Consumer environment, where the integration to the PXS is originating from a relatively uncontrolled or easily compromised environment, such as a mobile application running on a user's phone, or a 3\ :sup:`rd` party web site where the functionality is implemented via client-side scripting. OAuth Open authorization standard used for B2C and B2B authentication on the web. REST Colloquial term for integration protocol using JSON or XML over HTTPS. See http://en.wikipedia.org/wiki/REST for more detail on "true" REST. Access token Token passed to resources to convey authorization to access them. Generated by the Paytronix :http:post:`oauth/requestGuestToken.json` endpoint. The OAuth 2 standard indicates that these tokens should be relatively short-lived, e.g. 30 minutes, for enhanced security. Refresh token Token that can be used to obtain a new access token when an existing access token times out. Authorization Grant Short lived token (5 minutes) issued by Paytronix on behalf of the user. Is used to obtain an access and refresh token pair, which can be in turn used to perform CRUD operations on behalf of the user. Scope A combination of keywords denoting the context(s) an access token is able to be used. A token's scope is granted using information gathered from the requested scope and the maximum allowable OAuth scope configured for the requesting integration. Values are: ``user_read``, ``user_write``, ``account_read``, ``account_write``, ``register`` Endpoints --------- requestGuestToken ~~~~~~~~~~~~~~~~~ .. http:post:: oauth/requestGuestToken.json Requests an access token for a given user and/or account. This token will then allow the requestor to act on behalf of the guest, such as requesting balance information, editing account information or checking in to a location. The following authentication methods are allowed for this endpoint: * :ref:`anonymous_auth` * :ref:`b2b_auth` :jsonparam Integer merchantId: *(required)* Paytronix-assigned merchant ID to perform the operation in :jsonparam String grant_type: *(required)* Token grant type, used to determine what guest information is being sent to authenticate - see :ref:`guest_identification` :jsonparam String scope: *(required)* Space-separated sequence of values representing the requested access scope. Values are: ``user_read``, ``user_write``, ``account_read``, ``account_write``, ``register`` .. note:: Additional parameters are required based on the ``grant_type`` specified. See following sections. .. http:response:: success Indicates that the operation succeeded. :jsonparam String result: *(required)* ``success`` :jsonparam String type: *(required)* Either ``successAccessRefreshTokenResponse`` or ``successAccessTokenResponse``, noting the type of Reply Object :jsonparam Object successToken: *(required)* See either See :http:jsonentity:`SuccessAccessRefreshTokenResponse` or :http:jsonentity:`SuccessAccessTokenResponse` for format of the object. .. _guest_identification: Guest Identification ~~~~~~~~~~~~~~~~~~~~~ When requesting an access token, there are three different methods for authenticating, each of which requires different fields in the call to the requestGuestToken endpoint: #. For registered guests, username and password can be sent - see :ref:`grant_by_username_and_password` #. For registered guests coming from an insecure context, username and password can be sent - see :ref:`grant_by_authorization_grant` #. For both registered and unregistered guests, a set of user fields can be sent to uniquely identify the guest - see :ref:`grant_by_user_fields` #. A new access token can always be generated using the refresh token that came with the last access token - see :ref:`grant_by_refresh_token` .. _grant_by_username_and_password: Grant by Username and Password ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If the guest is registered and knows their Paytronix username and password, the combination of these two values can be used to generate an access token. This removes the requirement that the client software store the guest's Paytronix password locally. If the guest changes their password or resets their password, this will invalidate their access token. When sending a :http:post:`oauth/requestGuestToken.json` request using this authentication type, the following fields are required: .. http:jsonentity:: Grant by username and password :jsonparam String grant_type: *(required)* ``grant_type`` must equal ``password`` :jsonparam String username: *(required)* Username of the guest on whose behalf the access token is being requested :jsonparam String password: *(required)* Password of the guest on whose behalf the access token is being requested Example request: .. code:: javascript { "authentication": "anonymous", "grant_type": "password", "merchantId": 10101010, "scope": "user_read account_read", "username": "test_1010101090000104", "password": "test1234" } Example reply: .. code:: javascript { "access_token": "CjyxRJwTbA26_gUP__O6jUk1pbErFrxgQ90UYMcL01", "expires_in": 1800, "refresh_token": "1fVhxclTkc8gE7ycMexxjjAVHOuQACxOL_SbTkYqrK", "scope": "user_read account_read", "token_type": "bearer", "username": "test_1010101090000104" } .. _grant_by_authorization_grant: Grant by Authorization Grant ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If an authorization grant was generated via the guest logging in to the Paytronix guest website, then this grant can in turn be used to obtain an access and refresh token pair. See `Single Sign On <.././single_sign_on/index.html#flow>`_ for information on generating an authorization grant. This removes the requirement that the client software either prompt for or store the guest's Paytronix password, and will continue to allow access even if the guest changes their password. When sending a :http:post:`oauth/requestGuestToken.json` request using this authentication type, the following fields are required: .. http:jsonentity:: Grant by authorization grant :jsonparam String grant_type: *(required)* ``grant_type`` must equal ``authorization_code`` :jsonparam String code: *(required)* Authorization grant that has been returned to the integrator via a GET parameter in an HTTP redirect. :jsonparam String redirect_uri: *(optional)* Redirect URI that exactly matches the allow-listed SSO URL in the PXS. :jsonparam String scope: Space-separated sequence of values representing the requested access scope. Must be a subset of the scope of the authorization grant. Values are: ``user_read``, ``user_write``, ``account_read``, ``account_write``. Example request: .. code:: javascript { "authentication": "anonymous", "grant_type": "authorization_code", "merchantId": 10101010, "scope": "user_read account_read", "code": "APA91bFdV3CWmJpMors50gWwQqtmmwxYKpyy1-zQyeT0bp5nmrN_X4Mwlea..." } Example reply: .. code:: javascript { "access_token": "CjyxRJwTbA26_gUP__O6jUk1pbErFrxgQ90UYMcL01", "expires_in": 1800, "refresh_token": "1fVhxclTkc8gE7ycMexxjjAVHOuQACxOL_SbTkYqrK", "scope": "user_read account_read", "token_type": "bearer", "username": "test_1010101090000104" } .. _grant_by_refresh_token: Grant by Refresh Token ^^^^^^^^^^^^^^^^^^^^^^ Access tokens expire after a pre-determined amount of time (a number of seconds indicated in the ``expires_in`` field in the response message), so you will need to generate a new access token every so often. Tokens will generally need to be refreshed every thirty minutes, but this could vary depending on the integration. The request is authenticated by sending the previous ``refresh_token`` string to the PXS, avoiding the need to persist storage of guest passwords or user field data in order to re-authenticate the guest every half hour. If the guest changes their password or resets their password, this will invalidate their refresh token. When sending a :http:post:`oauth/requestGuestToken.json` request using this authentication type, the following fields are required: .. http:jsonentity:: Grant by refresh token :jsonparam String grant_type: *(required)* grant_type must equal "refresh_token" :jsonparam String refresh_token: *(required)* Refresh token returned by an earlier call to the requestGuestToken endpoint Example request: .. code:: javascript { "authentication": "anonymous", "grant_type": "refresh_token", "merchantId": 10101010, "scope": "user_read account_read", "refresh_token": "1fVhxclTkc8gE7ycMexxjjAVHOuQACxOL_SbTkYqrK", } Example reply: .. code:: javascript { "access_token": "Y0lb_QgRSq8e6KVvMAzYiJ_AysPE_eXlnMoUdrSYLj", "expires_in": 1800, "refresh_token": "PJegVMnmGy9XtxZCyBnTpGj3qwP1797pyxrPuVrY19", "scope": "user_read account_read", "token_type": "bearer", "username": "test_1010101090000107" } .. _grant_by_user_fields: Grant by User Fields ^^^^^^^^^^^^^^^^^^^^ Requests an access token using a cross-section of user information provided by the cardholder to uniquely identify them and verify that identity. This type of token request is the most complicated, but also the most flexible, allowing guests to authenticate with a variety of information that may change from user to user. Merchants can configure which fields are required to authenticate for a given program, and multiple intersections of user data may be usable for a single program, e.g. a single loyalty program could require guests to provide either email address and date of birth OR phone number and last name OR email and phone and first name, etc. In addition, it is important to note that that in all possible configurations at least one of the following fields will always be present and required for User Field-based Authentication to work: email, phone, or mobile phone. When sending a :http:post:`oauth/requestGuestToken.json` request using this authentication type, the following fields are required: .. http:jsonentity:: Grant by user fields :jsonparam String grant_type: *(required)* ``grant_type`` must equal `http://paytronix.com/oauth/fieldset` :jsonparam List[String] fields: *(required)* See :http:jsonentity:`OAuthUserFields` below :jsonparam Integer cardTemplateCode: *(required)* Paytronix-assigned card template code representing what program (e.g. gift card, loyalty card, etc.) to search for the guest in Fields ++++++ The "fields" field can contain a number of user demographic sub-fields, such as first name, last name, email address, and many more. The user fields sent by the client are then validated against the merchant's guests' information, previously entered into the Paytronix system (generally during card registration) in the Paytronix database, in order to identify the guest. In cases where the given information matches no accounts, or cases where the information matches multiple accounts, the server may indicate that additional information is needed to authenticate the guest. While the server is equipped to return detailed information on what set(s) of fields might be used to uniquely identify the guest, in general the client software should be programmed with knowledge of the possible field sets that are applicable. Due to technical constraints, all possible sets of user fields used to authenticate will always include at least one of the following fields: email, phone, mobilePhone. All field names and values are expected to be passed as strings. .. http:jsonentity:: OAuthUserFields :jsonparam String optIn: User's email opt-in preference :jsonparam String salutation: Guest's salutation. Allowed values are "Mr.", "Ms.", "Mrs.", "Dr.", and "Rev." :jsonparam String firstName: Guest's first name. Max 30 characters :jsonparam String lastName: Guest's last name. Max 30 characters :jsonparam String companyName: Guest's company name. Max 50 characters :jsonparam String email: Guest's email address. Max 100 characters :jsonparam String address1: Guest's address line 1. Max 100 characters :jsonparam String address2: Guest's address line 2. Max 100 characters :jsonparam String city: Guest's city. Max 50 characters :jsonparam String stateProvince: Guest's state or (Canadian) province, using the state's two-character abbreviation (e.g. "MA" for Massachusetts) :jsonparam String postalCode: Guest's postal code :jsonparam String country: Guest's country of residence. Allowed values are "US" and "CA" :jsonparam String phone: Guest's phone number. Must be exactly 10 digits without punctuation :jsonparam String fax: Guest's fax number. Must be exactly 10 digits without punctuation :jsonparam String mobilePhone: Guest's mobile phone number. Must be exactly 10 digits without punctuation :jsonparam String dateOfBirth: Guest's date of birth in ``yyyy-mm-dd`` format. :jsonparam String anniversaryDate: Date of guest's enrollment in the program in ``yyyy-mm-dd`` format. :jsonparam String custom1: Guest's answer to custom question #1. Max 100 characters. For some programs, additional validation may apply to this field :jsonparam String custom2: Guest's answer to custom question #2. Max 100 characters. For some programs, additional validation may apply to this field :jsonparam String custom3: Guest's answer to custom question #3. Max 100 characters. For some programs, additional validation may apply to this field :jsonparam String custom4: Guest's answer to custom question #4. Max 100 characters. For some programs, additional validation may apply to this field :jsonparam String custom5: Guest's answer to custom question #5. Max 100 characters. For some programs, additional validation may apply to this field :jsonparam String custom6: Guest's answer to custom question #6. Max 100 characters. For some programs, additional validation may apply to this field :jsonparam String customerNumber: Used by some merchants to record arbitrary customer data :jsonparam String username: Guest's username. Min 6 characters, max 60 characters, not having leading or trailing whitespace :jsonparam String password: Guest's password. Min 6 characters :jsonparam String printedCardNumber: Number printed on guest's card :jsonparam String registrationCode: Registration code, generally found printed somewhere on guest's card :jsonparam String externalAccountCode: An external account code (like a Facebook ID). The ``externalIdentifier`` field must also be specified with this field. :jsonparam String externalIdentifier: An external account type. The ``externalAccountCode`` field must also be specified with this field. Contact Paytronix to understand what the possible values are. :jsonparam String externalAccessToken: Access token which can be used to validate the externalIdentifier value. The ``externalAccountCode`` and ``externalIdentifier`` fields must also be specified with this field. :jsonparam String externalTokenSecret: The secret that goes with the ``externalAccessToken`` which can be used to validate the externalIdentifier value. The ``externalAccountCode``, ``externalIdentifier``, and ``externalAccessToken`` fields must also be specified with this field. This field is only required if the externalIdentifier is for Twitter Example requestGuestToken request which includes all fields: .. code:: javascript { "authentication": "anonymous", "grant_type": "http://paytronix.com/oauth/fieldset", "merchantId": 10101010, "scope": "user_read account_read", "fields": { "optIn": "true", "salutation": "Dr.", "firstName": "Test", "lastName": "User", "companyName": "Paytronix Systems, Inc.", "email": "example@paytronix.com", "address1": "307 Waverley Oaks Rd.", "address2": "Suite 309", "city": "Waltham", "stateProvince": "MA", "postalCode": "02452", "country": "US", "phone": "6176493300", "fax": "6178120725", "mobilePhone": "9995551212", "dateOfBirth": "1980-01-01", "anniversaryDate": "2005-01-01", "custom1": "Blue", "custom2": "Coffee", "custom3": "Main St.", "custom4": "Small", "custom5": "Twice a week", "custom6": "2000", "username": "testuser", "password": "test1234", "customerNumber": "ABC123456789", "printedCardNumber": "123456789", "registrationCode": "123456", "externalAccountCode": "1234567890123456", "externalIdentifier": "xhDE_Ea4QhsfC8h3pUtHJnLZ9lRXYgysJVXnR4XNO0" } } requestAuthorizationGrant ------------------------- .. http:post:: oauth/requestAuthorizationGrant.json Requests an authorization grant for a given user and/or account. This grant will then allow the requestor to act on behalf the request of the guest, such as requesting balance information, editing account information or checking in to a location. The follow authentication methods are allowed for this endpoint: * :ref:`oauth_auth` * :ref:`b2b_auth` :jsonparam String thirdPartyIntegration: *(required)* client_id for the third party on behalf of which this authorization grant is being generated :jsonparam String username: *(optional)* username for the guest on behalf of whom this authorization grant is being generated :jsonparam String printedCardNumber: *(optional)* printed card number for the business on behalf of whom this authorization grant is being generated. :jsonparam Integer merchantId: *(required)* Paytronix-assigned merchant ID to perform the operation in :jsonparam String response_type: *(required)* Should always be ``code`` :jsonparam String scope: *(required)* Space-separated sequence of values representing the requested access scope. Values are: ``user_read``, ``user_write``, ``account_read``, ``account_write`` :jsonparam String redirectUri: *(optional)* Redirect URI that exactly matches the allow-listed SSO URL in the PXS. .. note:: Either a ``username`` or ``printedCardNumber`` must be supplied .. note:: The scope provided must be a subset of the scope available to the access token being used to authenticate this request Example request: .. code:: javascript { "authentication": "oauth", "thirdPartyIntegration": "abc123xyz890", "username": "johnDoe", "merchantId": 10101010, "response_type": "code", "scope": "user_read account_read" } Example reply: .. code:: javascript { "authorizationGrant": "CjyxRJwTbA26_gUP__O6jUk1pbErFrxgQ90UYMcL01", "expires_in": 1800, "scope": "user_read account_read", "redirect_url": "https://example.com" } Reply Objects ------------- Success Access Refresh Token Response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. http:jsonentity:: SuccessAccessRefreshTokenResponse Includes a token pair and information about the tokens :jsonparam String accessToken: *(required)* A unique Paytronix-generated field to identify the access token associated with a guest which can be used to authenticate further requests :jsonparam String tokenType: *(required)* A field denoting the type of token generated, always "bearer" :jsonparam Int expiresIn: *(optional)* Lifetime of a token (measured in seconds) :jsonparam String refreshToken: *(required)* A token used to generate new access tokens upon their expiration. Refresh tokens may expire/be revoked, so integrations should be designed to handle this. :jsonparam String scope: *(optional)* Denotes for which scope(s) the access token has been granted :jsonparam String username: *(optional)* Identifies the username associated with the generated access token :jsonparam String printedCardNumber: *(optional)* Paytronix specific code to identify the printed card number associated with the generated access token Success Access Token Response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. http:jsonentity:: SuccessAccessTokenResponse Includes an access token only and information about the access token .. note:: This response will only be returned in a specific flow for inactive cards. Contact a Paytronix representative to learn more about this response. :jsonparam String accessToken: *(required)* A unique Paytronix-generated field to identify the access token associated with a guest which can be used to authenticate further requests :jsonparam String tokenType: *(required)* A field denoting the type of token generated, always "bearer" :jsonparam Int expiresIn: *(optional)* Lifetime of a token (measured in seconds) :jsonparam String scope: *(optional)* Denotes for which scope(s) the access token has been granted :jsonparam String username: *(optional)* Identifies the username associated with the generated access token :jsonparam String printedCardNumber: *(optional)* Paytronix specific code to identify the printed card number associated with the generated access token Authorization Grant Success ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. http:jsonentity:: AuthorizationGrantSuccess Includes an authorization grant and information about the grant :jsonparam String authorizationGrant: *(required)* A unique Paytronix-generated field to identify the authorization grant associated with a guest which can be used to authenticate further requests :jsonparam Int expiresIn: *(optional)* Lifetime of a token (measured in seconds) :jsonparam String scope: *(optional)* Denotes for which scope(s) the access token has been granted :jsonparam String redirectURL: *(optional)* URL the user will be redirected to after logging in via the grant Authorization Grant Failure ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. http:jsonentity:: AuthorizationGrantFailure :jsonparam String errorCode: *(required)* A Paytronix specific code to identify the type of error being sent :jsonparam String errorMessage: *(required)* A human readable message to explain to caller why request failed Example use of access token to authenticate with other services --------------------------------------------------------------- Authorization ~~~~~~~~~~~~~ Every REST call to the Paytronix API requires two parameters: ``client_id`` and ``client_secret``. Because these parameters represent an integrator's credentials they must be transmitted securely. As a result, the Paytronix API will not accept calls that make these credentials parameters in ``GET`` or ``DELETE`` methods. Although they may be included in the JSON body of a ``POST`` request, Paytronix recommends that you include these credentials in all of your requests using the `Basic access authentication `__ method. This should be a base64 encoded string of ``client_id`` and ``client_secret`` separated by a colon. (e.g. ``client_id:client_secret``). See the linked wikipedia article for specific details. In our API and documentation, the ``client_id`` is sometimes referred to as the ``Integration Identifier``. EnrollmentService - editUser endpoint ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once an access token is granted, a REST call may be made using the value of the received ``access_token`` in the message. Access tokens may be used in both POST and GET requests. If the access token is expired or does not match the user, the response will indicate that authentication has failed. Example POST request to https://m123.api.pxslab.com/rest/|release|/enrollment/editUser.json: .. code:: javascript { "authentication": "oauth", "access_token": "CjyxRJwTbA26_gUP__O6jUk1pbErFrxgQ90UYMcL01", "merchantId": 123, "username": "test_1010101090000104", "enforceUniqueFields": [], "setUserFields": { "style": "typed", "firstName": ["editedFirstName"], "lastName": ["editedLastName"], "username": ["editedUsername"] } } GuestService - userInformation endpoint ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Example GET request to :http:get:`guest/userInformation.json`: https://api.pxslab.com/rest/|release|/guest/userInformation.json? access_token=CjyxRJwTbA26_gUP__O6jUk1pbErFrxgQ90UYMcL01& username=editedUsername& authentication=oauth&merchantId=10101010 Expired access tokens ~~~~~~~~~~~~~~~~~~~~~ When an access token has expired, requests using authentication of ``oauth`` will be denied with a 401 HTTP status code and an ``error`` field value of ``invalid_token``. The ``WWW-Authenticate`` header in the response should be examined in order to gain additional information. When a token is expired, header should look like this: WWW-Authenticate Bearer realm="Paytronix REST", error="invalid_token", error_description="Token expired", expired="true" When this occurs, the client should immediately call requestGuestToken using the most recent ``refresh_token`` value. If this call also results in an error, the client should not automatically send any additional requests to any Paytronix endpoints. Errors ------ .. _insufficient_information_to_authenticate: Insufficient information to authenticate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When not enough information is provided, or no matching users, the server returns an "error" field with a value of ``insufficient_information_to_authenticate``. As noted :ref:`above `, the server will also return a ``tryMethods`` field containing a list of possible sets of fields to try, however it is not likely that most integrations will be required to parse this field's value. Rather, integrators should communicate with Paytronix to determine the appropriate fields to query the guest for. .. note:: If one of the required fields returned is ``externalAccount``, then that actually means the pair of fields ``externalAccountCode`` and ``externalIdentifier``. There is no field called ``externalAccount``. Example: .. code:: javascript { "error": "insufficient_information_to_authenticate", "tryMethods": [ { "confirmationRequired": false, "optionalFields": [], "requiredFields": [ "username", "password" ], "type": "registeredUser" }, { "confirmationRequired": false, "optionalFields": [], "requiredFields": [ "firstName", "lastName", "email", "postalCode" ], "type": "custom" }, { "confirmationRequired": false, "optionalFields": [], "requiredFields": [ "firstName", "lastName", "phone", "postalCode" ], "type": "custom" } ] } Error Codes ~~~~~~~~~~~ The following are the possible codes and messages that can be returned by the Oauth Service. There are other system-level errors which may be returned which may not be documented here. The caller of the endpoint can use the returned error message value to display to the end user or, if different wording is desired, can provide their own mapping of code to message. +------------------------------------------------------+----------------------------------------------------------------------------------------+ | Code | Message | +======================================================+========================================================================================+ | ``authorization.invalid_token`` | authorization.invalid_token | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``authentication.invalid_grant`` | token expired | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``authentication.invalid_grant`` | token invalid | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``authentication.invalid_scope`` | new scope exceeds original grant | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``authentication.invalid_client`` | client invalid | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``authentication.too_many_matching_guests`` | too_many_matching_guests | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``authentication.inactive_card_could_be_registered`` | inactive_card_could_be_registered | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``authentication.reg_code_needed`` | registration code missing | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``auth_grant.card_not_found`` | The supplied printedCardNumber was not found. Please try again. | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``auth_grant.both_user_and_pcn_supplied`` | Both username and printedCardNumber were received. Please supply only one. | +------------------------------------------------------+----------------------------------------------------------------------------------------+ | ``auth_grant.no_identifier_supplied`` | Neither username or printedCardNumber were received. Please supply an identifier. | +------------------------------------------------------+----------------------------------------------------------------------------------------+