developer

Workflow to create a checkout against API v3 with PHP and Guzzle

Preparation

Install Guzzle

First of all you need to install Guzzle. Please follow the steps here You can also use HTTP-Client or plain cURL if you want to reduce footprint of your implementation.

Create API-Token

We will use our all new RESTfull API. To authorize against this API, one have to create an API-Access-Token. You will need settings section in plenigo-Backend and at least the right to write Settings, to create a new Access-Token.

Examples for external User-Management

At the moment we only provide examples for external customer management.

Initialize Guzzle

$client = new GuzzleHttp\Client(['base_uri' => 'https://api.plenigo.com/api/v3.0/']);

POST the Customer

We tried to keep the examples as simple as possible. You can pimp the guzzle client as much as you want .

$client = new GuzzleHttp\Client(['base_uri' => 'https://api.plenigo.com/api/v3.0/']);
$user = [
    'customerId' => "4711", // User-ID in external system
    'email' => "{$emailAdress}", // E-Mail address of your customer
    'language' => 'de',
  ];

$response = $client->request('POST', 'customers', [
    // here you will need to work with the Api-Access token from plenigo backend
    'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
    // the payload, we'll send
    'json' => $user
]);

$customer = json_decode($response->getBody()->getContents());

This will return the created user or the already existing user. It will not recreate it.

Create Checkout-Code

After having a customer we need to create a checkout token:

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    "items" => [
        [
            "plenigoOfferId" => "O_6E487EBURRY35FOD0J",
            "quantity" => 1,
        ],
    ],
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

Create the Checkout

In your HTML-Code you will need to add the plenigo Javascript.

<!-- plenigo Javascript SDK will inject the Checkout into this div -->
<div id="plenigoCheckout"></div>

<script>
    // if your're working against the staging system, you have to extend the endpoint
    var plenigo = plenigo || {};
    plenigo.configuration = plenigo.configuration || {};
    plenigo.baseCheckoutURI = "https://checkout.plenigo-stage.com";
</script>

<script src="https://static.plenigo.com/static_resources/javascript/{YourCompanyId}/plenigo_sdk.min.js"
        type="text/javascript" data-disable-metered="true" data-oauth2-access-code="oauth2Test"></script>

<script>

  new plenigo.Checkout("<?php echo $response->purchaseId;  ?>", {elementId: "plenigoCheckout"}).start();

  // this will be triggered, if Checkout is finished successfully
  document.addEventListener("plenigo.PurchaseSuccess", function (e) {
    // debugging Code:
    console.info("Event is: ", e);
    console.info("Custom data is: ", e.detail);
    location.href = location.pathname + "?" + e.orderId;
    // please take care by using orderId in your scripts. It can be -1, if customer tries to buy a product multiple times
  });

  // if you enabled WebAnalytics, then you will recieve some additional information during the checkout 
  document.addEventListener("plenigo.WebAnalyticsLoad", function (e) {
    // debugging Code:
    console.group("ANALYTICS");
    console.info("Event is: ", e);
    console.info("Custom data is: ", e.detail);
    console.groupEnd();
  });

</script>

Since plenigo checkout will prevent customers from buying same product multiple times, orderId will be -1, if product is already bought.

Allow multiple purchases

Normally plenigo will check, if a user has active access to the product he tries to buy. If this is the case, plenigo will prevent additional checkout by showing a notice. You can disable this behavior with the parameter boolean allowMultiplePurchases:

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    "allowMultiplePurchases" => true, // true: user can buy same product multiple times / false (default): user can buy a product only once, depending on its access rights
    "items" => [
        [
            "plenigoOfferId" => "O_6E487EBURRY35FOD0J",
            "quantity" => 1,
        ],
    ],
];

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

<!-- plenigo Javascript SDK will inject the Checkout into this div -->
<div id="plenigoCheckout"></div>

<!-- use this configuration for prod deployment -->
<script src="https://static.plenigo.com/static_resources/javascript/{YourCompanyId}/plenigo_sdk.min.js"
        type="text/javascript" data-disable-metered="true" data-oauth2-access-code="oauth2Test"></script>
<!-- end prod script -->


<!-- use this configuration for stage deployment -->
<script src="https://static.plenigo-stage.com/static_resources/javascript/{YourCompanyId}/plenigo_sdk.min.js"
        type="text/javascript" data-disable-metered="true" data-oauth2-access-code="oauth2Test"></script>
<!-- end stage script -->


<script>

  new plenigo.Checkout("<?php echo $response->purchaseId;  ?>", {elementId: "plenigoCheckout"}).start();

  // this will be triggered, if Checkout is finished successfully
  document.addEventListener("plenigo.PurchaseSuccess", function (e) {
    // debugging Code:
    console.info("Event is: ", e);
    console.info("Custom data is: ", e.detail);
    location.href = location.pathname + "?" + e.orderId;
    // please take care by using orderId in your scripts. It can be -1, if customer tries to buy a product multiple times
  });

  // if you enabled WebAnalytics, then you will recieve some additional information during the checkout 
  document.addEventListener("plenigo.WebAnalyticsLoad", function (e) {
    // debugging Code:
    console.group("ANALYTICS");
    console.info("Event is: ", e);
    console.info("Custom data is: ", e.detail);
    console.groupEnd();
  });

</script>

Since plenigo checkout will prevent customers from buying same product multiple times, orderId will be -1, if product is already bought.

Purchase a single product and overwrite it with custom data

Some may have hundreds of identical products. Lets say, you are publishing one ePaper a day. And you want to be able to sell each of them. Then you should not create a new product every day - just reuse an old one. Create one product in the plenigo backend for all of your pdf-files, and sell it this way:

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    "items" => [
        [
            "plenigoOfferId" => "O_6E487EBURRY35FOD0J", // offerId of your all pdf product
            "quantity" => 1,
            "title" => "my daily ePaper issue: 2020-04-04",
            "products" => [
               [
                   "plenigoProductId" => "P_GZUZGJHGJHGGJ", // productID of the underlying product
                   "productId" => "ePaper-2020-04-04", // unique ID to make you able to differentiate between singular issues
                   "price" => 5, // you can overwrite price (only if you want to)
                   "title" => "my daily ePaper issue: 2020-04-04", // we will provide multi product offers
                   "accessRightUniqueId" => "ePaper-2020-04-04", // to be able to give access to one specific issue, you need to set specific access right
               ]
           ]

        ],
    ],
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

Add additional data to order data (upscore integration)

If you are using integrations like upscore, you want to add specific data to each order to have it in analytics systems. For upscore integration please also read the upscore integration guide.

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    "items" => [
        [
            "plenigoOfferId" => "O_6E487EBURRY35FOD0J", // offerId of your all pdf product
            "quantity" => 1,
        ],
    ],
    "additionalData" => [
        "data" => [
            "upscore_object_id" => $_GET["upscore_object_id"], //  Example:"object_123453"
            "upscore_offer_id"  => $_GET["upscore_offer_id"],  // Example "offer4711", not mandatory if upscore paywal is not in use
            "upscore_click_id"  => $_GET["upscore_click_id"], // Example "12345", not mandatory if upscore paywal is not in use
            "upscore_domain"    => "example.com", // this is optional and depends on your integration
            "custom_parameter"  => "4711",
        ]
    ],
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

Adding campaign parameters

If you are using campaign tracking, you can use the marketing data section to persist those tracking data:

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    "items" => [
        [
            "plenigoOfferId" => "O_6E487EBURRY35FOD0J", // offerId of your all pdf product
            "quantity" => 1,
        ],
    ],
   "additionalData" => [
        "sourceId" => "example.com/4711",
        "affiliateId" => "webshop",
        "data" => [
            "custom_parameter" => "4711",
        ],
        "utm" => [
            "source" => "example.com",
            "medium" => "https%3A%2F%2Fexample.com%2Fspor t%2Fwater%2Farticle.html",
            "campaign" => "171517130", // all values MUST be of type string!
            "term" => "4711",
            "content" => "4711",
        ],
    ],
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

Purchase a shopping cart

You can sell multiple offers in one checkout by passing multiple items in the purchase payload. A different approach would be, configure shopping carts to sell multiple offers.

You have to configure those shopping carts in the plenigo merchant backend. The code to sell them would be the following:

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    // there should not be any items attribute here
    "basketId" => "SC_UKKJH8KJHKJ",
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

Purchase an offer including a bonus

You can either bundle an offer with one single bonus product in plenigo backend. Selling it works like our normal checkout process. If you want your customer be able to select a bonus befor starting checkout process you can put offer together with bonus(es) into the preparePurchase method:

// @see https://api.plenigo-stage.com/#operation/preparePurchase
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    // there should not be any items attribute here
    "items" => [
        [
            "plenigoOfferId" => "O_6E487EBURRY35FOD0J", // offerId of your all pdf product
            "quantity" => 1,
            "plenigoBonusIds" => ["BO_ZWOSLHAS3CVOG73G2"], // a list of bonus products you want to sell together with offer
        ]
    ]
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

To retreive a list of avaiable bonuses you can use this endpoint: get bonuses

Show net prices

The default checkout will always display gross prices. If you want to display the net value of each orice you can enable it with the parameter boolean showNetPrices:

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    "showNetPrices" => true, // true: net price is shown / false (default): only gross price is shown
    "items" => [
        [
            "plenigoOfferId" => "O_6E487EBURRY35FOD0J",
            "quantity" => 1,
        ],
    ],
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

Check Access

If you are selling products, you want to give your customers access to your digital goods. With plenigo you simply use the method hasAccess

try {

    $response = json_decode($client->request('GET', "accessRights/{$customer->customerId}/hasAccess?accessRightUniqueIds=ePaper-2020-04-04", [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw']
    ])->getBody()->getContents()));

   $access = $response->accessGranted; // here we get the access status

} catch(\GuzzleHttp\Exception\ClientException $e) {
    // with external user managment we can't guarantee for an existing plenigo user. in these case you have to catch a GuzzleHttp\Exception\ClientException - this user should not get any access
    $access = false;
}

Guzzle loves to throw Exceptions. In case of a missing user plenigo will return a 404 StatusCode - guzzle will turn this into a GuzzleHttp\Exception\ClientException.

Work with Sessions, use plenigo SSO

If plenigo is used as a SSO you have to deal with sessions. You can use the session, to start a Checkout, to check users access rights or start selfservice process.

Create a transfer token

To open plenigo selfservice you need to have a transfer token:

    
    // @see https://api.plenigo-stage.com/#operation/createTransferToken
    // $session should be the plenigo session token
    $payload = ['customerSession' => $session];
    try {        
        $response = json_decode($client->request('POST', '/sessions/transferToken', [
          'headers' => [
            'X-plenigo-token' =>                        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

    $transferToken = $response->transferToken;
    
   } catch (\GuzzleHttp\Exception\ClientException $exception) {
        // handle errors
    }

use token to start selfservice

<div id="plenigoCheckout"></div>
    <script src="https://static.plenigo-stage.com/static_resources/javascript/<?php echo $companyId; ?>/plenigo_sdk.min.js"
            type="text/javascript" data-disable-metered="true" data-lang="de" data-disable-redirect="true"></script>

    <script>
        new plenigo.Snippets("<?php echo $transferToken; ?>", {elementId: "plenigoCheckout"}).start();
    </script>

Start a checkout with plenigo session

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
// $session is you plenigo session token
// @see https://api.plenigo-stage.com/#operation/preparePurchase
    "customerSession" => $session,
    "items" => [
        [
            "plenigoOfferId" => "O_6E487EBURRY35FOD0J",
            "quantity" => 1,
        ],
    ],
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

Check access with plenigo session

// https://customer-api.plenigo-test.com/#operation/checkAccessOfCustomer
$response = json_decode($client->request('GET', 'https://customer-api.plenigo-stage.com/api/v1.0/accessRights/hasAccess', [
      'headers' => ['X-plenigo-customer-session' => $session],
])->getBody()->getContents());

if ($response->accessGranted) {
// show content
}

plenigo voucher process

Vouchers enable you to sell products you don’t want to show on your website. Vouchers are always connected to a plenigo product. So voucher process is thought like a normal checkout: it generates an order.

Validate a voucher code

You can split voucher process into the validation and redemption part. Validating a voucher code will return status of the code and the plenigo offer it will be redeemed into:

// @see https://api.plenigo-stage.com/#operation/validateVoucherCode

$response = json_decode($client->request('GET', "/vouchers/{$voucherCode}/validate", [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => null
])->getBody()->getContents());

Redeem into a free offer

If plenigo offer is a free product, you can redeem a voucher simply by calling https://api.plenigo-stage.com/#operation/voucherPurchase.

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

// @see https://api.plenigo-stage.com/#operation/voucherPurchase

$payload = [
    'customerId' => '123',
    'customerIpAddress' => $ip,
    'voucherCode' => "1234-5678-1234"
];

$response = json_decode($client->request('POST', "/checkout/buyWithVoucher", [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

Redeem into a not free offer

If offer is not free or you are not sure, you can use the checkout process to redeem the voucher code. Checkout process will run with a free offer too.

// @see https://stackoverflow.com/a/55790/2336470
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

// @see https://api.plenigo-stage.com/#operation/preparePurchase

$payload = [
    "debugMode" => true, // enable debugging in checkout (please turn it off in prod environment)
    "customerIpAddress" => $ip,
    "customerId" => $customer->customerId,
    "voucherCode" => "1234-5678-1234"
]);

$response = json_decode($client->request('POST', '/checkout/preparePurchase', [
      'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
      'json' => $payload
])->getBody()->getContents());

This will generate a purchaseToken. In the next step you have to start the checkout.

Some tipps for working with our API

Here we want to get some hints to help you while implementing plenigo into your infrastructure.

Iterating

Some of you may wonder, how you can retrieve more than one page from plenigo API. First of all, you can use the size parameter to get more than 5 entities back. You can set it to a maximum of 100. You can use the following function:


/**
 * @param string $url Url of entity list we want to iterate.
 * @param string $entityId Full name of primary id of entity. For example `customerId` if you want to iterate customers
 * @param \DateTime|null $start Start date of time frame
 * @param \DateTime|null $end End date of time frame
 * @param string $itemKey Name of items attribute for given entity. Defaults to `items`
 * @return array
 */
function getList(string $url, string $entityId, ?\DateTime $start = null, ?\DateTime $end = null, string $itemKey = 'items'):array {
    /** @var array $allEntities List of entites of all requests */
    $allEntities = [];
    /** @var mixed $id Id of last entity, null, if empty */
    $id = null;

    // instantiate guzzle client (you really can use any other http client too!)
    /** @var GuzzleHttp\Client $client HTTP client we decided to use */
    $client = new Client(['base_uri' => 'https://api.plenigo-stage.com',
                                        // here you will need to work with the Api-Access token from plenigo backend
                                        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
                                        // the payload, we'll send
                                    ]);

    // add parameter size to url
    // we choose our maximum 100 to reduce http requests
    $url = "/api/v3.0{$url}?size=100";

    // add start of time frame if given
    if (!empty($start) && is_a($start, \DateTime::class)) {
        $url .= '&startTime=' . $start->format('Y-m-d\TH:i:s.v\Z');
    }

    // add end of time frame if given
    if (!empty($end) && is_a($end, \DateTime::class)) {
        $url .= '&endTime=' . $end->format('Y-m-d\TH:i:s.v\Z');
    }

    // start looping the api
    do {

        // put id of last item into url, if it already exists
        $myUrl = $id ? $url. "&startingAfter={$id}" : $url;

        // reset entities
        /** @var array $entities list of entities from current request */
        $entities = [];

        // use http client to request API
        try {
            $entities = json_decode($client->get($myUrl)->getBody()->getContents(), true)[$itemKey] ?: [];
        } catch (\Exception $exception) {
            // use logger to find out, why it broke
            echo $exception->getMessage();
        }

        // put id into key to find each item later on
        /** @var array $entity  */
        foreach ($entities as $entity) {
            /** @var mixed $id id of current entity */
            $id = $entity[$entityId];

            // append current entity to list of all entities
            $allEntities[$id] = $entity;
        }

    } while (!empty($entities));

    return $allEntities;
}

Simply call it like this:

        $customers = getList("/customers", "customerId");

Import subscriptions from external source to plenigo

If you want to import a subscription, you have to create a matching offer first. Lets assume, our offer is O_FAK3OFF3R. Then we have to create a customer, delivery address, an invoice address if needed, a payment method and at least the order. At the moment you only can create orders with subscripton offers - not with single product offers.

1. Create customer

    // @see https://api.plenigo.com/#tag/Customers/operation/createCustomer
    $customerData = [
        "invoiceEmail" => "office@company.com", // used only for invoice emails
        "username" => "kitty_207", // username is unique in plenigo
        "email" => "kitty.miller@company.com", // used for login, password reset and communication. email is unique
        "pseudoEmail" => false, // if set to true, plenigo will create a pseudo email address. Used to import customers without email address
        "salutation" => "MR", // one of NONE, MRS, MR, DIVERSE
        "firstName" => "kitty",
        "lastName" => "miller",
        "birthday" => "2000-07-20T00:00:00.000Z",
        "language" => "de", // two chars ISO Code of language
        "registrationSource" => "Import- " . (new DateTime())->format('Y-m-d'),
        "customerId" => "12345", // you can set customerId, if you use your own SSO system. This must be numeric. unique
        "externalSystemId" => "550e8400-e29b-41d4-a716-446655440000", // used for alphanumeric ids. unique
    ];

    $customer = json_decode($client->request('POST', '/customers', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $customerData
    ])->getBody()->getContents(), true);

2. Create delivery address

    // @see: https://api.plenigo.com/#tag/Addresses/operation/createAddress
    $addressData = [
        "type" => "DELIVERY", // one of DELIVERY ir INVOICE
        "preferred" => true, // preferred address is used in checkout automatically
        "salutation" => "MR", // one of NONE, MRS, MR, DIVERSE
        "firstName" => "kitty",
        "lastName" => "miller",
        "title" => "", // string, dr for example
        "country" => "DE", // for imports a country in delivery address is required
        "customerId" => $customer['customerId'],
        "businessAddress" => false, // if set to true address is handled as business address, we show company forms
    ];

    $deliveryAddress = json_decode($client->request('POST', '/addresses', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $addressData
    ])->getBody()->getContents(), true);

3. Create payment method (bank account)

    // @see https://api.plenigo.com/#tag/Payment-Methods/operation/createBankAccount
    $sepaData = [
        "customerId" => $customer['customerId'], // customerId of customer from 1st step
        "owner" => "{$customer['firstName']} {$customer['lastName']}", // account owner
        "iban" => "DE65500105173367173799", // IBAN
        "invalid" => false, // if set to true, account is not visible for customer and won't be used for payment
        "mandateId" => "DE0815", // mandate ref of existing sepa payment
        "mandateDate" => "2022-10-10T23:44:11.357Z", // date, whan mandate was created
    ];


    $paymentAccount = json_decode($client->request('POST', '/paymentMethods/bankAccounts', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $sepaData
    ])->getBody()->getContents(), true);

4. Create order

    // @see https://api.plenigo.com/#tag/Order-Imports/operation/orderImport
    $import = [
        "suppressMail" => true, // set to true if you do not want to send process emails
        "purchase" => false, // if set to true, this import is handled as a purchase
        'items' => [
            [
                "externalSystemId" => "Migration-4711", // unique identifier of this subscription within plenigo
                "plenigoOfferId" => "O_FAK3OFF3R",
                "invoiceCustomerId" => $customer['customerId'],
                "paymentMethod" => "BANK_ACCOUNT",
                "paymentMethodId" => $sepaData['bankAccountId'],
                "deliveryCustomerId" => $customer['customerId'],
                "deliveryAddressId" => $deliveryAddress['addressId'],
                "startDate" => "2020-10-10T10:10:10.100Z", // date, when subscription started
                "referenceStartDate" => "2023-10-10T00:00:00.000Z", // last invoice in source system (should be in past)
                "orderDate" => "2020-10-05T10:10:10.100Z", // date, when order was created in source system
                "endDate" => null, // date, when subscription should end in plenigo
                "nextBookingDate" => "2024-10-10T00:00:00.000Z", // end of performance period in source system. plenigo will create a new invoice at this date
                "quantity" => 1,
            ]
        ]
    ];

    $importStatus = json_decode($client->request('POST', '/imports/orders', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $import
    ])->getBody()->getContents(), true);

5. find your imported subscription

    $importedSubscription = json_decode($client->request('GET', '/subscriptions?externalSystemId=Migration-4711', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $import
    ])->getBody()->getContents(), true)['items'][0] ?? null;

Synchronize external managed subscriptions with plenigo

An external managed subscription is a subscription which is not invoices by plenigo. Since this subscription is completely managed in source system, customer can not cancel it in plenigo. You should use external managed subscripions, if you do not want to migrate an existing subscription to plenigo, but use plenigo as SSO and access management system. You either should use it, if you want to offer customers of a subscription in your source system a discount in plenigo with rules in plenigo. To import an external managed subscription, you have to create an external managed offer first.

Create external managed order

    // @see https://api.plenigo.com/#tag/Order-Imports/operation/orderImport
    $import = [
        "suppressMail" => true, // set to true if you do not want to send process emails
        "purchase" => false, // if set to true, this import is handled as a purchase
        'items' => [
            [
                "externalSystemId" => "Sync-4711", // unique identifier of this subscription within plenigo
                "plenigoOfferId" => "O_FAK3OFF3R",
                "invoiceCustomerId" => $customer['customerId'],
                "paymentMethod" => "BILLING",
                "deliveryCustomerId" => $customer['customerId'],
                "deliveryAddressId" => $deliveryAddress['addressId'],
                "startDate" => "2020-10-10T10:10:10.100Z", // date, when subscription started
                "quantity" => 1,
            ]
        ]
    ];

    $importStatus = json_decode($client->request('POST', '/imports/orders', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $import
    ])->getBody()->getContents(), true);

Now you have to take care of externalSystemId. You can use it, to find your imported subscription within plenigo and to update this subscription. If it ends in your source system, you simply set endDate in plenigo to the endDate in your source system:

Sync end date

    // @see https://api.plenigo.com/#tag/Order-Imports/operation/orderImport
    $import = [
        "suppressMail" => true, // set to true if you do not want to send process emails
        "purchase" => false, // if set to true, this import is handled as a purchase
        'items' => [
            [
                "externalSystemId" => "Sync-4711", // unique identifier of this subscription within plenigo and your ID in source system with prefix
                "plenigoOfferId" => "O_FAK3OFF3R",
                "invoiceCustomerId" => $customer['customerId'],
                "paymentMethod" => "BILLING",
                "deliveryCustomerId" => $customer['customerId'],
                "deliveryAddressId" => $deliveryAddress['addressId'],
                "startDate" => "2020-10-10T10:10:10.100Z", // date, when subscription started
                "endDate" => "2025-10-10T00:00:00.000Z", // date, when subscription should end in plenigo
                "quantity" => 1,
            ]
        ]
    ];

    $importStatus = json_decode($client->request('POST', '/imports/orders', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $import
    ])->getBody()->getContents(), true);

Restart already ended external subscription

If your source system can restart an already ended subscription (Wiedereinweisung), you can reset endDate. In plenigo it will remove endDate from subscription.

    // @see https://api.plenigo.com/#tag/Order-Imports/operation/orderImport
    $import = [
        "suppressMail" => true, // set to true if you do not want to send process emails
        "purchase" => false, // if set to true, this import is handled as a purchase
        'items' => [
            [
                "externalSystemId" => "Sync-4711", // unique identifier of this subscription within plenigo and your ID in source system with prefix
                "plenigoOfferId" => "O_FAK3OFF3R",
                "invoiceCustomerId" => $customer['customerId'],
                "paymentMethod" => "BILLING",
                "deliveryCustomerId" => $customer['customerId'],
                "deliveryAddressId" => $deliveryAddress['addressId'],
                "startDate" => "2020-10-10T10:10:10.100Z", // date, when subscription started
                "endDate" => null, // endDate now has to be empty
                "quantity" => 1,
            ]
        ]
    ];

    $importStatus = json_decode($client->request('POST', '/imports/orders', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $import
    ])->getBody()->getContents(), true);

Dealing with issue based subscriptions - create an order

Issue based subscriptions have some differences to time based subscriptions. They are not invoiced in a periodic way. They are invoiced after a specific amount of issues was used or sent.

    // @see https://api.plenigo.com/#tag/Order-Imports/operation/orderImport
    $import = [
        "suppressMail" => true, // set to true if you do not want to send process emails
        "purchase" => false, // if set to true, this import is handled as a purchase
        'items' => [
            [
                "externalSystemId" => "Sync-4711", // unique identifier of this subscription within plenigo and your ID in source system with prefix
                "plenigoOfferId" => "O_FAK3OFF3R",
                "invoiceCustomerId" => $customer['customerId'],
                "issueBased" => true, // this enables issue based mode
                "issueSteps" => [
                    [
                        "startDate": (new DateTime('tomorrow'))->format(DATE_RFC3339_EXTENDED)
                    ]
                ],
                "paymentMethod" => "BILLING",
                "deliveryCustomerId" => $customer['customerId'],
                "deliveryAddressId" => $deliveryAddress['addressId'],
                "quantity" => 1,
            ]
        ]
    ];

    $importStatus = json_decode($client->request('POST', '/imports/orders', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $import
    ])->getBody()->getContents(), true);

Dealing with issue based subscriptions - migrate a subscription

Because of not working with dates for invoicing you have to know the specific amount of issues, the customer can consume until next billing. If we have to deal with multi step subscriptions, we have to pass past steps too:

    // @see https://api.plenigo.com/#tag/Order-Imports/operation/orderImport
    $import = [
        "suppressMail" => true, // set to true if you do not want to send process emails
        "purchase" => false, // if set to true, this import is handled as a purchase
        'items' => [
            [
                "externalSystemId" => "Sync-4711", // unique identifier of this subscription within plenigo and your ID in source system with prefix
                "plenigoOfferId" => "O_FAK3OFF3R",
                "invoiceCustomerId" => $customer['customerId'],
                "issueBased" => true, // this enables issue based mode
                "issueSteps" => [
                    [
                        "startDate" => "2020-10-10T10:10:10.100Z", // date, when subscription started
                        "openDeliveries" => 0
                    ],

                    [
                        "startDate" => "2023-10-10T10:10:10.100Z", // date, when 2nd step was started
                        "openDeliveries" => 2, // customer will be invoiced after sending of two issues
                        // "cancellationDate" => (new DateTime('today'))->format(DATE_RFC3339_EXTENDED), // if you pass cancellationDate, subscription will end after sending 2nd issue
                    ]
                ],
                "paymentMethod" => "BILLING",
                "deliveryCustomerId" => $customer['customerId'],
                "deliveryAddressId" => $deliveryAddress['addressId'],
                "quantity" => 1,
            ]
        ]
    ];

    $importStatus = json_decode($client->request('POST', '/imports/orders', [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => $import
    ])->getBody()->getContents(), true);

Import issue based external subscriptions - our thoughts

From our perspective there is no need to import external managed issue based subscriptions. The external system knows, when a subscriptions starts, and when it ends (or ended) thats why you should simply use time based subscriptions in these cases. The will give customers access for the exact time, when subscription is active in external system.

Subscriptions: Cancelling and undo cancellation

If you want to offer some custom functionality on your website without using plenigo self service, you might want to use one of these APIs: https://customer-api.plenigo.com/ or https://api.plenigo.com/ Here we show you different options:

Cancel Subscription

A subscription has its specific renewal mechanism. If you cancel a subscription before the next renewal, you may trigger a refund. Lets start with cancelling a subscription at next renewal. Regardless of the type of subscription, cancellation works as follows at the next possible date.

    // @see https://customer-api.plenigo.com/#tag/Subscriptions/operation/cancelSubscription
    // @see https://api.plenigo.com/#tag/Subscriptions/operation/cancelSubscriptionRegular

    $cancelledSubscription = json_decode($client->request('PUT', "/subscriptions/{$subscriptionId}/cancel/regular", [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => []
    ])->getBody()->getContents(), true);

Cancel Subscription at a specific date

We have to differentiate 2 kinds of subscription:

  1. A subscription, which runs for about a year and is paid annual. Customers can cancel it only to the end of an annual period.
  2. A subscription, which runs for a month but is paid annual (fair consumer contracts). Customers can cancel it to the end of each month and get a refund of a part of the paid price.

You will always get the dates of renewals of the given subscription. That’s every possible date a subscription can be cancelled. For the 1st type you get the dates of the whole next year, it’s a subscription with monthly renewal, for 12 years if its an annual renewal. For 2nd type subscription you’ll get only the renewals up to the next payment. Lets assume, you’re in the 3rd month of an annual subscription, than you’ll get back 9 dates. The last date will be the date the subscription ends with a regular cancellation.

If your subscription has multiple steps, you’ll only get dates until the end of the subscription step defined by subscriptionId. If you want to get dates of the next step, you have to use its subscriptionId.

    // @see https://customer-api.plenigo.com/#tag/Subscriptions/operation/cancelSubscriptionDates
    // @see https://customer-api.plenigo.com/#tag/Subscriptions/operation/cancelSubscriptionAt

    // @see https://api.plenigo.com/#tag/Subscriptions/operation/cancelSubscriptionDates
    // @see https://api.plenigo.com/#tag/Subscriptions/operation/cancelSubscriptionAt

    $cancellelationDates = json_decode($client->request('GET', "/subscriptions/{$subscriptionId}/cancel/dates", [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
    ])->getBody()->getContents(), true)['items'] ?? [];

    /**
    $cancellationDates = [
        "2024-07-20T00:00:00Z",
        "2024-08-20T00:00:00Z",
        "2024-09-20T00:00:00Z",
        "2024-10-20T00:00:00Z",
        "2024-11-20T00:00:00Z",
        "2024-12-20T00:00:00Z",
        "2025-01-20T00:00:00Z",
        "2025-02-20T00:00:00Z",
        "2025-03-20T00:00:00Z",
        "2025-04-20T00:00:00Z",
        "2025-05-20T00:00:00Z",
        "2025-06-20T00:00:00Z"
    ];
    **/

    $cancelledSubscription = json_decode($client->request('PUT', "/subscriptions/{$subscriptionId}/cancel/at", [
        'headers' => ['X-plenigo-token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGVzZSI6ImFyZSIsInBsZW5pZ28iOiJ0ZXN0IiwiZGF0YSI6ImRvIiwibm90IjoiY29uc3VtZSJ9.xnFAQQbHEFLisgeU2YqWsIfpCgEbmh_Hy59Ja0Ztxyw'],
        'json' => [
            "cancellationDate" => "2025-02-20T00:00:00Z"
        ],
    ])->getBody()->getContents(), true);

How to determine type of subscription

timebased If on a timebased subscription term is equal or greater than accounting period the dates are the following:

// annual example
{
    "items": [
        "2024-07-20T00:00:00Z", // first possible cancellation date (regular cancellation)
        "2025-07-20T00:00:00Z", // further possible cancellation dates
        "2026-07-20T00:00:00Z",
        "2027-07-20T00:00:00Z",
        "2028-07-20T00:00:00Z",
        "2029-07-20T00:00:00Z",
        "2030-07-20T00:00:00Z",
        "2031-07-20T00:00:00Z",
        "2032-07-20T00:00:00Z",
        "2033-07-20T00:00:00Z",
        "2034-07-20T00:00:00Z",
        "2035-07-20T00:00:00Z",
        "2036-07-20T00:00:00Z"
    ]
}

If on a timebased subscription term is less than accounting period the dates are the following:

// annual example
{
    "items": [
        "2024-07-20T00:00:00Z", // first possible cancellation date (will refund)
        "2024-08-20T00:00:00Z",
        "2024-09-20T00:00:00Z",
        "2024-10-20T00:00:00Z",
        "2024-11-20T00:00:00Z",
        "2024-12-20T00:00:00Z",
        "2025-01-20T00:00:00Z",
        "2025-02-20T00:00:00Z",
        "2025-03-20T00:00:00Z",
        "2025-04-20T00:00:00Z",
        "2025-05-20T00:00:00Z",
        "2025-06-20T00:00:00Z" // date of a regular cancellation
    ]
}

issue based If on an issue based subscription the attribute "cancellationType": "ISSUE_BASED", than accounting period the dates are the following:

// weekly magazine, 12 issues
{
    "items": [
        "2024-07-01T00:00:00Z", // first possible cancellation (will refund)
        "2024-07-08T00:00:00Z",
        "2024-07-15T00:00:00Z",
        "2024-07-22T00:00:00Z",
        "2024-07-29T00:00:00Z",
        "2024-08-05T00:00:00Z",
        "2024-08-12T00:00:00Z",
        "2024-08-19T00:00:00Z",
        "2024-08-26T00:00:00Z",
        "2024-09-02T00:00:00Z",
        "2024-09-09T00:00:00Z",
        "2024-09-16T00:00:00Z" // regular cancellation
    ]
}

issue based If on an issue based subscription the attribute "cancellationType": "ISSUE_BASED_REGULAR", than accounting period the dates are the following:

// weekly magazine, 12 issues
{
    "items": [
        "2024-09-16T00:00:00Z" // regular cancellation
    ]
}

Create a middleware for in app purchases

The aim of this interface is to create an opportunity to sell the same offers that are sold on the website or in the shop as in-app purchases in your own apps. This means that users can use a purchase they have made in the app in other apps, on other platforms and of course also on the web platform as normal. All shown examples are only to show the flow. We removed all validations and exception handling.

Preparation

To purchase an in app offer you have to implement the normal platform functionality:

And setup in app functionality in plenigo: https://support.plenigo.com/help/de-de/187-in-app-kaufe

Process

After purchasing an in app offer in the platform related store, the app creates a purchase in penigo. In parallel callbacks of the stores begin to update this purchase regulary. process

Useful functions

It starts with the creation of the purchase.

Example Storekit v1:

    /**
     * @Route("/apple/receipts/add", name="AppleReceipt", methods={"POST"})
     *
     * @param Request $request
     * @return Response
     */
    public function addAppleReceipt(Request $request):Response {
        // extraxt data from payload
        $content = json_decode($request->getContent(), true);
        // extract name of the app, this wil be used in plenigo
        $appIdentifier = $request->headers->get('x-app-bundle');
        // take only receipt data
        $receipt = $content['receipt'];

        $result = $this->client->post("/appStores/appleAppStore", [
            "appIdentifier" => $appIdentifier ,
            "receiptData" => [$receipt]
        ]);

        return $result->getBody()->getContents();
    }

Example google:

     /**
     * @Route("/google/purchase/add", name="GooglePurchase", methods={"POST"})
     *
     * @param Request $request
     * @return Response
     */
    public function addGooglePurchase(Request $request):Response {
        // extraxt data from payload
        $content = json_decode($request->getContent(), true);
        // extract name of the app, this wil be used in plenigo
        $appIdentifier = $request->headers->get('x-app-bundle');

        $result = $this->client->post("/appStores/googlePlayStore", [
            "packageName" => $content['packageName'],
            "receiptData" => [
                [
                    "purchaseToken" => $content['purchaseToken'],
                    "productId" => $content['productId'],
                    "subscription" => $content['isSubscription'],
                ]
            ]
        ]);

        return $result->getBody()->getContents();
    }

Example Storekit v2:

 /**
     * @Route("/apple/transaction/add", name="AppleTransaction", methods={"POST"})
     *
     * @param Request $request
     * @return Response
     */
    public function addAppleTransaction(Request $request):Response {
        // extraxt data from payload
        $content = json_decode($request->getContent(), true);
        // extract name of the app, this wil be used in plenigo
        $appIdentifier = $request->headers->get('x-app-bundle');
        // take only transactionId
        $transactionId = $content['transactionId'];

        $result = $this->client->post("/appStores/appleAppStore", [
            "appIdentifier" => $appIdentifier ,
            "transactionIds" => [$transactionId]
        ]);

        return $result->getBody()->getContents();
    }

Since all these kinds of purchase won’t have a customer, they’re called anonymous. You can use plenigo`s access right mechanism and combine each app store offer with various access rights to control access of the apps content. Therefore all methods return a token, which should be stored in the app. With this token the app can ask for access rights:

Example:

    /**
     * @Route("/receipts/{provider}/{token}/verify", name="VerifyReceipt", methods={"GET"})
     *
     * @param string $provider
     * @param string $token
     * @param Request $request
     * @return Response
     */
    public function verifyReceipt(string $provider, string $token, Request $request):Response {

        $appIdentifier = $request->headers->get('x-app-bundle', $this->companies[$companyId]['appIdentifier']);

        if ("apple" == $provider) {
            $result = $this->client->get("/appStores/appleAppStore/{$token}/verify");
        } elseif ("google" == $provider) {
            $result = $this->client->get("/appStores/googlePlayStore/{$token}/verify");
        }

        return $result->getBody()->getContents();
    }

Create order and subscription

Creation of order (and subscription, if app store offer is a subscription) will make the in app purchase available on all platforms including your website. The customer will get access rights, which can be used as described before. All you need is, to provide a login process in you app, where customers can login. If they do so, you can use their customerId in your app to control access.

Example:

    /**
     * @Route("/receipts/{provider}/associate", name="Associate", methods={"POST"})
     *
     * @param string $provider
     * @param Request $request
     * @return Response
     */
    public function associate(string $companyId, string $provider, string $token, int $customerId, Request $request):Response {

        // extraxt data from payload
        $content = json_decode($request->getContent(), true);
        // token, the app received during add process
        $token = $content['token'];
        // customerId of the customer you retreived during login
        $customerId = $content['customerId'];

        if ("apple" == $provider) {
            $result = $this->client->post("/appStores/appleAppStore/{$token}/associate", [
                "customerId" => (string)$customerId
            ]);
        } elseif ("google" == $provider) {
            $result = $this->client->post("/appStores/googlePlayStore/{$token}/associate", [
                "customerId" => (string)$customerId
            ]);
        }

        return $result->getBody()->getContents();
    }

Create an id system with plenigo

We want to share with you some of our thoughts, how we would like to implement some plenigo functionality.

If it comes to sso or an entire ID-System we have to think about some special use cases:

  • user should be logged in on all our websites
  • our websites do not necessarily have the same domain
  • user should be logged out, if he logs out
  • we do not want to implement login or register functionality on all and every web project

Our architecture for these cases is, having an id system in the middle. Every customer logs in or registers here, and this system persists the session for every user: process If a website or shop needs a user, it redirects the user to id system (id.domain.com/login?returnTo=www.domain.com/slug-of-article.html). This either has a session of the user, or shows a login page, to log in and creates a session. In both cases user will be redirected to source system afterwards (www.domain.com/slug-of-article.html). You always should pass a source page. If the user was already logged in on our id system, he won’t mention the redirect.

To create a login or register page, you simply have to use plenigo js-sdk and follow these instructions: https://plenigo.github.io/sdks/javascript-v3#using-plenigo-sso. Both create a session, which should be stored in session of id-system. To securely transport the session, id system should create a transfer token pass the token as url parameter (www.domain.com/slug-of-article.html?transferToken={transferToken}), and target system gets its session out of it.

To complete integration we add plenigo self service into the id system. Since the id system already has a session of the current user, it can provide some functionality like personalizing or short routes to cancel some subscriptions by using plenigo customer api.

Create checkout workflow with plenigo

Requirement for this tool was, having a checkout journey, where the customer can buy all possible offers we provide in plenigo. The plenigo checkout is just an iframe in the middle of the page. Rest of page should show customized headers and footers and some additional stuff, like some more offer information.

The easiest way to solve this is having an url like www.domain.com/checkout?offer={plenigoOfferId} If you want to do some customizing depending on offer, you should get the offer: https://api.plenigo.com/#tag/Offers/operation/searchProductOffers. If you are using tags, you can change design and behavior depending on tags of this specific offer. You can pass custom css dynamically to the checkout to change its design by offer configuration.

Now its time to embed the plenigo checkout.

Checkout will end with an javascript event:

  document.addEventListener("plenigo.PurchaseSuccess", function (e) {
    location.href = location.pathname + "?orderId=" + e.orderId;
  });

And this can either do the redirect to our source page on our website or show the entire subscription configuration by getting the order: https://api.plenigo.com/#tag/Orders/operation/getOrder and the included subscription: https://api.plenigo.com/#tag/Subscriptions/operation/searchSubscriptions by searching for the subscriptionItemId which is included in the order-item.

process After the purchase you can either redirect back to source article page by adding a transferToken to the url, to be able to login the customer in one step, or let the website aks the id-system for the customer.