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());

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 xternal 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.