Skip to content

dreamcommerce/appstore-example-payment-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 

Repository files navigation

Shoper App Store — Example Payment Application

A Symfony-based template/example for building payment applications for the Shoper App Store. This project provides a minimal but complete scaffold with webhook endpoints, request authentication, and stub services ready to be implemented with your payment provider.

Note: This project is a simplified version of a production application. All business logic is provided as documented stubs — you need to implement the actual database persistence and payment provider integration.

Table of Contents


1. Getting Started

1.1. Join the Shoper Partner Program

Before you can create applications for the Shoper App Store, you need to register as a partner:

  1. Go to Shoper Partner Program — App Creator
  2. Fill out the registration form (company data, NIP required)
  3. After approval, you will receive:
    • Access to the developer panel for managing your applications
    • A dedicated technical advisor for support
    • Access to the API documentation and sandbox environment for testing

1.2. Register Your Application

  1. Log in to the Shoper developer panel
  2. Create a new application — provide:
    • Application name and description
    • Application URL — the base URL where your app is hosted
    • Webhook URL — the endpoint that will receive lifecycle events (in this template: /api/v1/webhook/application)
    • Permissions/scopes — which shop resources your app needs access to (e.g. orders, payments, metafields)
  3. After creating the application, you will receive:
    • app_id — unique application identifier
    • app_secret (App Store secret) — used for HMAC signature verification and OAuth token exchange
    • webhook_secret — used specifically for webhook request signature verification
  4. Store these credentials securely in your .env.local file:
    APPLICATION_APP_STORE_SECRET=your_app_secret_here
    WEBHOOK_SECRET=your_webhook_secret_here

Configure the Application Iframe

Add a link in the administration panel to display your application's settings iframe. Set the Object to Payments, Action to Edit, and Place to Settings:

Configuring the display of the application's iframe

Add the Application to the Admin Menu

Add a link so the application appears in the shop admin under Order Processing → Payment Methods:

Adding an app to the app menu in Order Processing > Payment Methods

Configure API Permissions

Your application needs the following API permissions (privileges) to communicate with the shop:

Languagesread permission for retrieving language information:

Configuring the languages permissions

Currenciesread permission for retrieving currency codes:

Configuring the currencies permissions

Ordersread permission for downloading order data:

Configuring the order data permissions

Manage your own paymentsread + create + edit + delete permission for payment method management:

Configuring the payment methods manage permissions

1.3. Configure Webhook Endpoints

In the developer panel, configure the following webhook URLs pointing to your deployed application:

Event Webhook URL Description
Application lifecycle https://your-app.com/api/v1/webhook/application Install, upgrade, uninstall events
Transaction https://your-app.com/api/v1/transaction/webhook Payment transaction processing
Refund https://your-app.com/api/v1/refund/webhook Refund processing

Transaction webhook configuration — set the Event to "Order - transaction added":

Configuring the transaction webhook

Refund webhook configuration — set the Event to "Order - refund added":

Configuring the refund webhook

1.4. Install & Run Locally

# Clone the repository
git clone <repository-url>
cd appstore-sf-example-payment-app/app

# Install dependencies
composer install

# Configure environment variables
cp .env .env.local
# Edit .env.local with your Shoper App Store credentials (app_secret, webhook_secret)

# Start the development server
symfony server:start
# or with Docker
docker-compose up -d

1.5. Test on Development Environment

  • Shoper provides a sandbox/development environment for testing your application before publishing
  • Use tools like ngrok to expose your local development server so Shoper can reach your webhook endpoints
  • Verify that webhook signatures are validated correctly by checking the HashValidator logic
  • Test the full install → transaction → refund flow before submitting for review

2. Architecture Overview

src/
├── Controller/              # HTTP entry points (webhook endpoints, iframe API)
│   ├── ApplicationWebhookController   # App lifecycle: install, upgrade, uninstall
│   ├── TransactionWebhookController   # Payment transaction processing
│   ├── RefundWebhookController        # Refund processing
│   └── FrontController                # Iframe view & checkout transaction URL retrieval
├── Enum/
│   └── ApplicationWebhookType         # Lifecycle event types (install/upgrade/uninstall)
├── Security/                # Request authentication layer
│   ├── HashValidator                  # HMAC signature verification algorithms
│   ├── WebhookAuthenticator           # Symfony authenticator for webhook requests
│   └── IframeAuthenticator            # Symfony authenticator for iframe requests
└── Service/                 # Business logic (stub implementations)
    ├── ApplicationWebhookResolver     # App lifecycle handler (token exchange, metafield sync)
    ├── PaymentService                 # Payment method creation in the shop
    ├── TransactionService             # Transaction processing & URL management
    └── RefundService                  # Refund processing

Request flow: Incoming HTTP request → Symfony Security (WebhookAuthenticator / IframeAuthenticator) → HashValidator verifies HMAC signature → Controller → Service


3. Application Lifecycle Flow

When a shop installs your application from the Shoper App Store, the following flow occurs:

1. Shop installs app
   └─► Shoper sends POST /api/v1/webhook/application?action=install
       └─► ApplicationWebhookResolver::install()
           ├── Create shop entity in database
           ├── Exchange auth_code for OAuth token (POST /webapi/rest/oauth/token)
           └── Synchronize metafields (GET /webapi/rest/metafields/system, /metafield-values)

2. Payment method creation (developer-implemented, not a webhook)
   └─► Your application logic calls PaymentService::createPayment()
       ├── Register payment in shop (POST /webapi/rest/payments)
       │   Important: The `notify` field in the request body MUST contain the
       │   `{payment_form}` tag. This tag is rendered by Shoper on the order
       │   summary page after checkout, producing a <script id="paymentForm">
       │   JSON block with transaction details (see section 4.4).
       │   Your JavaScript (embedded via snippets or modules — see section 12)
       │   reads this JSON to obtain the transaction context and redirect the
       │   customer to the payment provider.
       ├── Update metafield values (PUT /webapi/rest/metafield-values/{id})
       └── If the payment supports multiple channels, register them as well.
       Note: This step is NOT triggered by a webhook. It must be implemented
       as part of your application's own flow (e.g. after install, via CLI command,
       or through an admin panel action). PaymentService is only an example
       showing which API calls are needed.

3. Customer places an order — two complementary transaction creation scenarios:

   Scenario A — Webhook-driven (order-transaction.create):
   └─► Shoper sends the order-transaction.create webhook to the app's configured URL
       └─► Partner app receives the webhook and creates a transaction in the payment provider
           └── Transaction URL is stored, ready to be fetched by the shop's JS

   Scenario B — On-demand (JavaScript-initiated):
   └─► After checkout, the shop's order summary page renders {payment_form} JSON
       └─► Your JavaScript (snippet or module) reads the JSON and calls your app
           └─► Partner app creates a transaction on the fly and returns the payment URL

   Both scenarios complement each other as a fallback strategy:
   • If the JS request arrives first and creates the transaction, the subsequent
     webhook should only log the event (no duplicate transaction).
   • If the webhook arrives first, the JS endpoint should detect the existing
     transaction and return its URL without re-creating it.

4. Customer is redirected to payment
   └─► JavaScript on the order summary page obtains the payment URL from your app
       └── Redirects the customer to the payment provider's paywall
       └── After payment, the provider redirects back to paymentSuccessShopLink
           or paymentFailShopLink

5. Shop admin requests refund
   └─► Shoper sends POST /api/v1/refund/webhook
       └─► RefundService::createRefund()
           └── Process refund in payment provider

6. Shop upgrades app version
   └─► Shoper sends POST /api/v1/webhook/application?action=upgrade
       └─► ApplicationWebhookResolver::upgrade()
           ├── Update version in database
           └── Re-synchronize metafields

7. Shop uninstalls app
   └─► Shoper sends POST /api/v1/webhook/application?action=uninstall
       └─► ApplicationWebhookResolver::uninstall()
           └── Mark shop as uninstalled (soft delete)

4. Webhook Endpoints

Endpoints Summary

Endpoint Method Auth Description
/api/v1/webhook/application POST, GET Webhook HMAC Application lifecycle (install/upgrade/uninstall)
/api/v1/transaction/webhook POST Webhook HMAC Payment transaction processing
/api/v1/refund/webhook POST Webhook HMAC Refund processing
/api/v1/front/view GET Iframe HMAC Renders the application's admin panel iframe
/api/v1/front/transaction-url POST Webhook HMAC Returns the payment redirect URL (called by checkout JS)

4.1. POST /api/v1/webhook/application — Application Lifecycle

Handles install, upgrade, and uninstall events. The action field determines the event type.

Headers:

Header Description
x-webhook-sha1 HMAC signature for verification
x-webhook-id Unique webhook request ID
x-shop-license Shop identifier (license ID)

Install request payload:

{
    "action": "install",
    "auth_code": "one-time-authorization-code",
    "shop": "shop-license-id",
    "shop_url": "https://my-shop.shoper.pl",
    "application_version": 1
}

Upgrade request payload:

{
    "action": "upgrade",
    "shop": "shop-license-id",
    "application_version": 2
}

Uninstall request payload:

{
    "action": "uninstall",
    "shop": "shop-license-id"
}

Response:

Status Description
201 Created Install processed successfully
200 OK Upgrade/uninstall processed successfully
409 Conflict Processing error (returns error message)
405 Method Not Allowed Unknown action value

4.2. POST /api/v1/transaction/webhook — Transaction

Sent by the Shoper shop when a customer initiates a payment.

Headers:

Header Description
x-shop-license Shop identifier (license ID)
x-webhook-sha1 HMAC signature for verification
x-webhook-id Unique webhook request ID

Request payload:

{
    "currency_id": "1",
    "currency_value": "99.99",
    "order_id": "12345",
    "payment_data": "",
    "payment_fail_shop_link": "https://my-shop.shoper.pl/payment/fail",
    "payment_id": "1",
    "payment_success_shop_link": "https://my-shop.shoper.pl/payment/success",
    "transaction_id": "67890"
}

Response:

Status Description
201 Created Transaction processed successfully
500 Internal Server Error Processing error

4.3. POST /api/v1/refund/webhook — Refund

Sent by the Shoper shop when a shop admin initiates a refund.

Headers: Same as transaction webhook (x-shop-license, x-webhook-sha1, x-webhook-id).

Request payload:

{
    "refund_id": 1,
    "transaction_id": 67890,
    "status": "new",
    "currency_id": 1,
    "currency_value": "99.99",
    "comment": "Customer requested refund",
    "date": "2026-03-16 14:00:00"
}

Response:

Status Description
201 Created Refund processed successfully
500 Internal Server Error Processing error

4.4. POST /api/v1/front/transaction-url — Transaction URL Retrieval

Called by a JavaScript script running on the shop's order summary page (not the admin iframe). After a customer places an order and reaches the order summary page, the shop renders a {payment_form} block containing transaction context. Your JavaScript reads this data and sends a request to this endpoint to obtain the payment redirect URL. The returned URL is then used to redirect the customer to the payment provider's paywall.

There are two complementary scenarios for creating the transaction in the partner's system (see section 3 — Application Lifecycle Flow for details):

  1. Webhook-driven — the partner app receives an order-transaction.create webhook and pre-creates the transaction. When this endpoint is called, it returns the already-stored URL.
  2. On-demand — the partner app creates the transaction when this endpoint is called for the first time, then returns the newly generated URL.

Both approaches can coexist as a fallback strategy, ensuring the customer always gets a payment link regardless of webhook delivery timing.

Request payload:

{
    "shop": "shop-license-id",
    "transactionId": "67890"
}

Response:

{
    "transaction_url": "https://payment-provider.com/pay/abc123"
}

The {payment_form} Tag and paymentForm JSON

When creating a payment via POST /webapi/rest/payments (documentation), the notify field must contain the {payment_form} tag. Shoper replaces this tag on the order summary page with a <script> block containing transaction context:

<script id="paymentForm" type="application/json">
{
    "channelId": "",
    "transactionId": "11",
    "paymentData": "",
    "paymentSuccessShopLink": "https://my-shop.shoper.pl/basket/finished/status/ok/paymentid/22/orderid/12/",
    "paymentFailShopLink": "https://my-shop.shoper.pl/basket/finished/status/fail/paymentid/22/orderid/12/",
    "applicationId": "de1955d73fe80892f0d3384982923373",
    "shop": "302a2332f924dc6a30fde449db0668f71580bbca",
    "time": "1773396430",
    "hash": "b2f521cc91ae2046cb612db909163339..."
}
</script>
Field Description
channelId ID of the payment channel selected by the customer at checkout (if channels were registered via API)
transactionId Shop's internal transaction identifier
paymentData Custom data passed via setPaymentData() on earlier checkout steps
paymentSuccessShopLink URL where the payment provider should redirect on successful payment
paymentFailShopLink URL where the payment provider should redirect on failed payment
applicationId Your application's ID in the Shoper App Store (developer panel)
shop Unique shop identifier — sent during application installation
time Unix timestamp of when the data was generated
hash HMAC-SHA512 signature for verifying the request authenticity (see below)

Hash Verification

The hash field allows your application to verify that the request originates from a legitimate Shoper shop. It is computed using HMAC-SHA512 with your application secret as the key.

Hash calculation algorithm:

1. Construct the data array with these keys (in this exact order):
   shop, time, transactionId

2. Join as query string:
   "shop={value}&time={value}&transactionId={value}"

3. Compute HMAC-SHA512:
   hash = HMAC-SHA512(data: query_string, secret: application_secret)

PHP reference implementation:

$hashData = [
    "shop" => $view->shop,
    "time" => $view->time,
    "transactionId" => $view->transactionId,
];

public function signData(array $params): string
{
    $parameters = [];
    foreach ($params as $k => $v) {
        $parameters[] = $k . "=" . $v;
    }

    $p = join("&", $parameters);

    return hash_hmac('sha512', $p, 'your_application_secret');
}

When your application receives a request from the shop's JavaScript, verify the hash by recomputing it with the provided shop, time, and transactionId values and comparing the result to the received hash.

4.5. GET /api/v1/front/view — Application Iframe

Renders the application's admin panel view inside the Shoper admin iframe.

Query parameters (added by Shoper, used for authentication):

Parameter Description
admin-id ID of the logged-in admin user
admin-name Name of the logged-in admin user
place Location in the admin panel where the iframe is displayed
shop Shop identifier
timestamp Request timestamp
admin-hash HMAC-SHA512 signature of the above parameters

Response: HTML page rendered from templates/front/index.html.twig.


5. Webhook Authentication

All webhook and iframe requests from Shoper are signed with HMAC signatures to prevent tampering. The application verifies these signatures before processing any request. This logic is implemented in src/Security/HashValidator.php.

5.1. Webhook HMAC (SHA1 over SHA512)

Used for: application lifecycle, transaction, refund webhooks, and checkout transaction URL requests.

Verification algorithm:

1. Extract headers: x-webhook-sha1, x-webhook-id, x-shop-license
2. Compute HMAC-SHA512 key:
   key = HMAC-SHA512(
       data: "{x-shop-license}:{webhook_secret}",
       secret: app_secret
   )
3. Compute expected hash:
   hash = SHA1("{x-webhook-id}:{key}:{request_body}")
4. Compare hash with x-webhook-sha1 header
5. If they match → request is authentic

Relevant classes:

  • HashValidator::verifyWebhookRequest() — implements the algorithm above
  • WebhookAuthenticator — Symfony security authenticator that calls HashValidator

5.2. Iframe HMAC (SHA512)

Used for: iframe view endpoint only.

Verification algorithm:

1. Extract query parameters: admin-id, admin-name, place, shop, timestamp
2. Sort parameters alphabetically by key
3. Join as: "admin-id={value}&admin-name={value}&place={value}&shop={value}&timestamp={value}"
4. Compute expected hash:
   hash = HMAC-SHA512(
       data: joined_parameters,
       secret: app_secret
   )
5. Compare hash with admin-hash query parameter
6. If they match → request is authentic

Relevant classes:

  • HashValidator::verifyApplicationIframeRequest() — implements the algorithm above
  • IframeAuthenticator — Symfony security authenticator that calls HashValidator

6. Shoper REST API Endpoints

These are the Shoper shop API endpoints that your application needs to call (using the OAuth token obtained during install):

Endpoint Method Purpose
/webapi/rest/oauth/token POST Exchange authorization code for OAuth access token
/webapi/rest/metafields/system GET Fetch system metafield definitions
/webapi/rest/metafield-values GET Fetch metafield values
/webapi/rest/metafield-values/{id} PUT Update metafield values
/webapi/rest/payments POST Create a payment method in the shop
/webapi/rest/payments-channels POST Create payment channels for a payment method
/webapi/rest/orders/{id} GET Fetch order details (optional)

All API calls to a shop require the OAuth access_token in the Authorization: Bearer {token} header.


7. Payment Channels

After creating a payment method in the shop (see step 2 in section 3), you can optionally register payment channels — individual payment options such as specific banks, credit/debit cards, BLIK, or Google Pay. When channels are added, they are displayed to the customer during checkout, allowing them to select a specific payment option.

Payment channels displayed in the checkout basket

Creating Payment Channels

Use the Shoper REST API endpoint to add channels to an existing payment method:

POST {shopUrl}/webapi/rest/payments-channels

Each channel should include a name, optional icon, and any configuration needed for your payment provider to identify which payment option the customer selected. The selected channel ID is then available in the channelId field of the paymentForm JSON rendered on the order summary page (see section 4.4).

For the full API reference, see the Shoper Payments Channels documentation.


8. Configuration

The application requires the following environment variables:

Variable Description
APPLICATION_APP_STORE_SECRET Application secret from the Shoper App Store developer panel — used for HMAC verification and OAuth
WEBHOOK_SECRET Webhook secret configured for this application — used for webhook signature verification

These are injected into HashValidator via Symfony's service container.

Requirements

  • PHP >= 8.5
  • Symfony 7.4
  • Composer

9. What You Need to Implement

This template provides the webhook infrastructure and request authentication. You need to implement:

  1. Database layer — Entity classes and Doctrine mappings for shops, payments, transactions
  2. OAuth token management — Token exchange and refresh logic in ApplicationWebhookResolver
  3. Metafield synchronization — Fetching and storing metafield data from the Shoper API
  4. Payment provider integration — Connecting to your payment provider (e.g. Stripe, PayU, Przelewy24) in TransactionService and RefundService
  5. Payment method creation — Registering your payment method in the shop via PaymentService

10. Publishing & Versioning

Submitting Your Application

  1. Once your application is ready, submit it through the developer panel for Shoper's review
  2. Provide a clear description, screenshots, and documentation for shop administrators
  3. Each new submission must pass Shoper's review and approval before it becomes available to shops

Version Management

Each application version has a version number (passed as application_version in webhook payloads):

Action How
Set initial version During application registration in the developer panel
Release an update Increment the version number and submit the new version for review
Handle upgrades When a shop upgrades, Shoper sends an upgrade webhook with the new application_version — your app handles it in ApplicationWebhookResolver::upgrade()
Changelog Provide a description of changes with each version update in the developer panel

App Lifecycle in the Shop

Once published, the application lifecycle is managed automatically by Shoper:

Shop Action Webhook Sent Your App Should
Installs your app action=install with auth_code, shop, shop_url, application_version Create shop record, exchange auth code for OAuth token, sync metafields
Upgrades to new version action=upgrade with shop, application_version Update version, re-sync metafields
Uninstalls your app action=uninstall with shop Mark shop as uninstalled (soft delete)

11. Snippets & Modules — Embedding JavaScript in the Shop Frontend

After the customer places an order, the order summary page renders the {payment_form} JSON block (see section 4.4). Your application needs JavaScript running on this page to:

  1. Read the paymentForm JSON data
  2. Send a request to your application's backend to obtain the payment redirect URL
  3. Redirect the customer to the payment provider's paywall

The method for embedding this JavaScript depends on which shop template engine is in use: snippets for RWD templates, or custom modules for Storefront templates.

Important: Your JavaScript should include a path guard to ensure it only runs on the relevant pages (order summary and payment panel):

const path = window.location.pathname;
if (!/\/basket\/done\/+/.test(path) && !/\/panel\/payment\/+/.test(path)) {
    return;
}

11.1. Snippets (RWD Templates)

For shops using RWD templates, embed your JavaScript using snippets.

  1. In your application configuration in the developer panel, add a snippet
  2. Select the "Stopka strony" (Page Footer) section — this ensures the script loads on every page
  3. Your snippet code should include the path guard above, then read and process the paymentForm data

Adding the application snippets

For more information on snippets, see the Shoper Snippets documentation.

11.2. Custom Modules (Storefront Templates)

For shops using Storefront templates, embed your JavaScript using custom modules ("own modules").

Manual setup in the shop admin:

  1. Navigate to Look and contentStore appearanceCurrent themeOwn modules
  2. Add a new custom module containing your JavaScript code

Automatic setup via application configuration:

To have the module installed automatically when a shop installs your application, add the module definition in your application configuration in the developer panel. This way, every shop that installs your app will have the module created automatically.

Adding the application module

Your module code should include the same path guard and payment flow logic as the snippet version.

11.3. JavaScript Implementation Outline

Below is a high-level outline of the JavaScript that should run on the order summary page:

(function () {
    const path = window.location.pathname;
    if (!/\/basket\/done\/+/.test(path) && !/\/panel\/payment\/+/.test(path)) {
        return;
    }

    const paymentFormEl = document.getElementById('paymentForm');
    if (!paymentFormEl) {
        return;
    }

    const paymentData = JSON.parse(paymentFormEl.textContent);

    // Send a request to your application's backend to get the payment URL.
    // Include paymentData.shop, paymentData.transactionId, paymentData.hash,
    // and paymentData.time for authentication.
    //
    // Your backend should:
    //   1. Verify the hash (see section 4.4 — Hash Verification)
    //   2. Find or create the transaction in the payment provider
    //   3. Return the payment redirect URL
    //
    // On success, redirect the customer:
    //   window.location.href = response.transaction_url;
})();

12. Links & Documentation

Resource URL
Shoper Partner Program — App Creator https://www.shoper.pl/program-partnerski/tworca-aplikacji
Shoper App Store — Application Registration https://developers.shoper.pl/developers/appstore/registration/application
Shoper API — Getting Started https://developers.shoper.pl/developers/api/getting-started
Shoper API — Resources https://developers.shoper.pl/developers/api/resources
Shoper API — Payments (insert) https://developers.shoper.pl/developers/api/resources/payments/insert
Shoper API — Payments Channels https://developers.shoper.pl/developers/api/resources/payments-channels
Shoper Webhooks — Introduction https://developers.shoper.pl/developers/webhooks/introduction
Shoper Webhooks — order-transaction.create https://developers.shoper.pl/developers/webhooks/methods/order-transaction-create
Shoper Snippets — Introduction https://developers.shoper.pl/developers/snippets/introduction

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages