Subscriptions

Both the Zlick SDK and widget allow you to accept recurring-payments for the subscriptions you offer.

When creating an account with Zlick, you have to define the subscription plans you offer in advance. These plans are stored in the Zlick database and to activate this subscription for a certain user, you just have to refer to it by its unique name.

A typical subscription plan has the following properties:

When the onboarding process is complete, you can then refer to this subscription and start activating it for users on your site through the subscription's unique name.

Subscription Flow

The following chart might help you understand the flow of subscriptions on the Zlick platform.

Zlick Subscriptions Flow

Webhook Callback

The webhook callback is defined during the onboarding process and is used by zlick to notify you for any updates in the transaction or subscription. Zlick will send a POST request to your defined endpoint with content-type application/json.

Retry Mechanism

Zlick will make 3 retries to your endpoint for any failed callback that returns a response. Retry cycle will be performed on the following response status codes (100 - 199), (429 - 429), (500 - 599) and will retry twice on errors that don’t return a response (ENOTFOUND, ETIMEDOUT, etc)

Webhook Body

Webhook's body is different depending on whether it is a subscription callback or a transaction callback and whether or not it was a success or a failure.

Transaction

Success
{
    "eventId": "642ac976-352a-45ed-b263-8da194a53db7",
    "event": "transaction.completed",
    "apiVersion": "2020-10-22",
    "data": {
        "productId": "zlick-product-123",
        "amount": 299, # price of product in change
        "clientTransactionId": "tms-1234", # client transaction id (not applicable for Apollo)
        "zlickTransactionId": "b84bf4c3-091e-4621-bfd0-54c0a48cb405", # unique transaction ID from Zlick
        "subscriptionId": "a74bf4c3-094e-5621-bfd0-54c0a48cb307", # unique subscription ID from Zlick
        "subscriptionProductName" : "PRODUCT_NAME",
        "isSubscription": true,
        "clientUserId": "u123" # user id specified by client SDK
    },
    "livemode": false,
    "timestamp": "2020-08-17T11:42:43.007Z"
}

Error
{
    "eventId": "642ac976-352a-45ed-b263-8da194a53db7",
    "event": "transaction.failed",
    "apiVersion": "2020-10-22",
    "data": {
        "desc": "", # error description (if available)
        "zlickErrorCode": "ZLICK05", # one of Zlick's internal Error codes. See below.
        "productId": "zlick-product-123",
        "amount": 299,
        "clientTransactionId": "tms-1234",
        "zlickTransactionId": "b84bf4c3-091e-4621-bfd0-54c0a48cb405",
        "subscriptionId": "a74bf4c3-094e-5621-bfd0-54c0a48cb307", # unique subscription ID from Zlick
        "subscriptionProductName" : "PRODUCT_NAME",
        "isSubscription": true,
        "clientUserId": "u123"
    },
    "livemode": false,
    "timestamp": "2020-08-17T11:42:43.007Z"
}

Subscription

Status

The following webhook is sent whenever subscription status is changed. Use data.state to set subscription status in your own system.

Success
{
    "eventId": "642ac976-352a-45ed-b263-8da194a53db7", # Always unique for each webhook request
    "event": "subscription.status",
    "apiVersion": "2020-10-22",
    "data": {
        "clientUserId": "accc61ba",
        "expiresAt": "2020-02-17T11:26:49.631Z",
        "productName": "zlick-plan-1", # subscription name
        # state values can be "canceled", "ended", "inactive", "active"
        # ended    - limited subscription has reached its end
        # inactive - subscription ended manually
        # canceled - no further payments are generated. generally due to payment failure
        "state": "active",
        "subscriptionId": "6f87fe56-50b0-4e64-93c3-86f930fc3bd5"
    },
    "livemode": false,
    # when this event was generated
    "timestamp": "2020-08-17T11:42:43.007Z"
}

Webhook signature

Verify the events that Zlick sends to your webhook endpoints.

Zlick signs the webhook events it sends to your endpoints by including a signature in each event’s signature header. This allows you to verify that the events were sent by Zlick, not by a third party.

You can verify the signature with the api client secret key provided by Zlick during the onboarding process.

Preventing replay attacks

A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. To mitigate such attacks, Zlick includes a timestamp in the signature header. Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker cannot change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can have your application reject the payload.

You can have a tolerance of few minutes between the timestamp and the current time. You can use this tolerance to either accept or reject the payload. Zlick uses Coordinated Universal Time (UTC) as a standard for timestamp value in webhooks.

Verifying signatures

The signature header included in each signed event contains a timestamp and one signature. The timestamp is prefixed by t=, and the signature is prefixed by v=.

signature:
t=1597676339727,
v=3af6267fbd9693391b96d3d1fb5266733991ce2bc09684f68c0af3bd89614b77

Note that newlines have been added for clarity, but a real signature header is on a single line.

Zlick generates signatures using a hash-based message authentication code (HMAC) with SHA-256.

You can follow these steps to verify webhook event signature.

Step 1: Extract the timestamp and signature from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp in milliseconds, and v corresponds to the signature.

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

Step 3: Determine the expected signature

Compute an HMAC with the SHA256 hash function. Use the api client secret as the key, and use the signed_payload string as the input string/message.

Step 4: Compare the signature

Compare the signature in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to the received signature.


Node JS Code Example
const crypto = require('crypto')
const moment = require('moment')

const validateSignature = ({ body, signature }) => {
    const payload = JSON.stringify(body)
    let pairs = signature.split(',')
    pairs = pairs.map(v=>{
        const tmp = v.split("=");
        return {[tmp[0]]:tmp[1]}
    })
    const request_timestamp = pairs[0].t
    const request_hmac = pairs[1].v

    let signed_payload = `${pairs[0].t}.${payload}`
    signed_payload = crypto.createHmac('sha256', api_secret)
        .update(signed_payload)
        .digest('hex')

    if (signed_payload === request_hmac){
        // hmac matched
        const tolerance = moment.utc().valueOf() - request_timestamp
        // tolerance in milliseconds
        if (tolerance > 10*60*1000){ // tolerance of 10 minutes
            return false
        }
        return true
    } else {
        // miss match 
        return false
    }
}

// Client secret by Zlick
const api_secret = 'a9f8880a41cca0524a0815df'

// Request 
const request = {
    headers: {
        signature: 't=1597676339727,v=3af6267fbd9693391b96d3d1fb5266733991ce2bc09684f68c0af3bd89614b77'
    },
    data: {
        eventId: "642ac986-382a-45ed-b363-8da124a53db7",
        event: "subscription.status",
        apiVersion: "2020-10-22",
        data: {
            clientUserId: "accc61ba",
            expiresAt: "2020-02-17T11:26:49.631Z",
            productName: "zlick-plan-1",
            state: "active",
            subscriptionId: "8f87fe55-50b0-4e64-93c3-86f930fc3bd5"
        },
        livemode: false,
        timestamp: "2020-08-17T12:39:46.553Z"
    }    
}

if (validateSignature({
        body: request["data"], 
        signature: request["headers"]["signature"]
    })) {

    // Use the response for your needs
    const response = request["data"]
    console.log("accepted")
    // ...

} else {
    // Validation failed
    // Reject Payload
    // ...
    console.log("rejected")
}

Zlick Error Codes