PHP (Guzzle) PHP (cURL) cURL JavaScript Python JSON

Introduction

This is the KWIGA API documentation page. You can use this API to perform actions on your behalf inside your KWIGA cabinets β€” a cabinet is the workspace (your account / tenant) where your courses, contacts and other data live.

In order to enable the API you should click on the corresponding checkbox in the settings section of your account. After that a token will be generated and account hash will be created, that will be used for subsequent API calls.

If a token is compromised, it can be re-issued, that will make the old token inactive and generate a new one.

API is only available for those users who have API enabled in their account.

Rate limiting

The API has a rate limit of 200 requests per minute. The limit is counted per API token.

Every response includes the following rate-limit headers:

Header Description
X-RateLimit-Limit maximum number of requests allowed per minute.
X-RateLimit-Remaining number of requests still allowed in the current minute.
Retry-After seconds to wait before retrying. Returned only when the limit is exceeded (HTTP 429).
X-RateLimit-Reset UNIX timestamp indicating when the current window resets. Returned only when the limit is exceeded (HTTP 429).

Idempotency

Mutating endpoints in the public API (currently POST /contacts/:contact/rewards; more will be marked individually) accept an optional Idempotency-Key request header. Pass a unique value once and reuse it on every retry of the same logical operation β€” the server will collapse retries to a single side-effect and return the same response on every replay.

Endpoints that support idempotency are marked with this badge in the reference: Idempotent

Contract:

Header Behavior
Idempotency-Key (request) Client-generated unique string (1–255 chars). UUIDv4 or a stable business key like reward-for-quiz-attempt-4821 both work. Empty/missing header disables idempotency for that call. Clients that cannot set custom headers (form-based integrations) may pass the same value as a body/query field named idempotency_key β€” header takes precedence if both are present.
Idempotent-Replayed: true (response) Returned on the second and subsequent calls with the same key. When you see this header, the server did NOT execute the operation again β€” the body contains the same object that was created on the first call (the entity is re-read and serialized again, so it reflects its current state). If that entity has since been permanently deleted, the body is a minimal acknowledgement with its type and id.

Scope of a key is (cabinet, endpoint, key): the same business key on a different endpoint or in another cabinet is treated as a fresh operation. We recommend UUIDv4 anyway β€” collisions become statistically impossible.

Basic work with API

<?php
// Get contact by ID

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/contacts/:contact', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get contact by ID

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/contacts/:contact' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get contact by ID
const url = 'https://api.kwiga.com/contacts/:contact';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get contact by ID
import requests

url = 'https://api.kwiga.com/contacts/:contact'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/contacts/:contact",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

All requests must be directed to the domain:

https://api.kwiga.com

For authorization the API token should be send in one of the next variants:

To identify the account in which the actions take place, the hash of the account must be sent along in one of the next variants:

PUT and DELETE requests can be sent as POST requests by specifying an additional _method parameter with the required method (PUT or DELETE).

You can set the localization of directories and validation messages with the header:

X-Language: {locale}

Locale Description
en English (default)
cs Czech
de German
el Greek
es Spanish
fr Franch
hu Hungarian
it Italian
ka Georgian
lv Polish
pl Polish
pt Portuguese
ro Romanian
ru Russian
uk Ukrainian
zh Chinese (Simplified)

Errors

Error Code Meaning
400 Bad Request - Your request is invalid.
401 Unauthorized - Your API key is wrong.
403 Forbidden - The requested resource is hidden for administrators only.
404 Not Found - Server cannot find the requested resource.
405 Method Not Allowed -- Server knows the request method, but the target resource doesn't support this method.
422 Unprocessable entity.
429 Too Many Requests - You're sending too many requests! Slow down!
500 Internal Server Error - We had a problem with our server. Try again later.
503 Service Unavailable - We're temporarily offline for maintenance. Please try again later.

CRM. Contacts

Contact records

List of contacts

Request example:

<?php
// Get contacts list

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/contacts', $options);

$result = json_decode($response->getBody());
?>


# ---
# Paginated example
# ---

<?php
// Get contacts list with pagination

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/contacts?page=1&per_page=15', $options);

$result = json_decode($response->getBody());
?>


# ---
# Filtered example
# ---

<?php
// Get contacts list with filters

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/contacts?page=1&per_page=15&filters[date_from]=2022-04-27&filters[search]=example.com&with_offers=1', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get contacts list

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Paginated example
# ---

<?php
// Get contacts list with pagination

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts?page=1&per_page=15');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Filtered example
# ---

<?php
// Get contacts list with filters

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts?page=1&per_page=15&filters[date_from]=2022-04-27&filters[search]=example.com&with_offers=1');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/contacts' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# Paginated example
# ---

curl --location --request GET 'https://api.kwiga.com/contacts?page=1&per_page=15' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# Filtered example
# ---

curl --location --request GET 'https://api.kwiga.com/contacts?page=1&per_page=15&filters[date_from]=2022-04-27&filters[search]=example.com&with_offers=1' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get contacts list
const url = 'https://api.kwiga.com/contacts';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Paginated example
# ---

// Get contacts list with pagination
const url = 'https://api.kwiga.com/contacts?page=1&per_page=15';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Filtered example
# ---

// Get contacts list with filters
const url = 'https://api.kwiga.com/contacts?page=1&per_page=15&filters[date_from]=2022-04-27&filters[search]=example.com&with_offers=1';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get contacts list
import requests

url = 'https://api.kwiga.com/contacts'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# Paginated example
# ---

# Get contacts list with pagination
import requests

url = 'https://api.kwiga.com/contacts?page=1&per_page=15'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# Filtered example
# ---

# Get contacts list with filters
import requests

url = 'https://api.kwiga.com/contacts?page=1&per_page=15&filters[date_from]=2022-04-27&filters[search]=example.com&with_offers=1'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/contacts",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# Paginated example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/contacts?page=1&per_page=15",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# Filtered example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/contacts?page=1&per_page=15&filters[date_from]=2022-04-27&filters[search]=example.com&with_offers=1",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": [
    {
      "id": 1,
      "created_at": "2022-02-04T12:17:32.000000Z",
      "email": "test@example.com",
      "first_name": "James",
      "last_name": "Bond",
      "phone": "+380931234567",
      "tags": [
          {
              "id": 93,
              "name": "test-tag"
          }
      ],
      "offers": [
          {
              "id": 8,
              "unique_offer_code": "ptJmPPXVYs0t",
              "title": "ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ #8",
              "limit_type": {
                  "id": 1,
                  "name": "НСограничСно"
              },
              "limit_of_sales": null
          }
      ]
    },
    "utm": {
        "utm_source": [
            "test-source"
        ],
        "utm_campaign": [],
        "utm_medium": [],
        "utm_term": [
            "test-term"
        ],
        "utm_content": []
    },
    {
      "id": 2,
      "created_at": "2022-02-04T12:17:32.000000Z",
      "email": "test2@example.com",
      "first_name": "Petr",
      "last_name": "Ivanov",
      "phone": "+380983234512",
      "tags": [],
      "offers": []
    }
  ],
  "links": {
        "first": "https://api.kwiga.com/contacts?page=1",
        "last": "https://api.kwiga.com/contacts?page=2",
        "prev": null,
        "next": "https://api.kwiga.com/contacts?page=2"
   },
   "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 2,
        "links": [
            {
                "url": null,
                "label": "&laquo; translation missing: en.pagination_prev",
                "active": false
            },
            {
                "url": "https://api.kwiga.com/contacts?page=1",
                "label": "1",
                "active": true
            },
            {
                "url": "https://api.kwiga.com/contacts?page=2",
                "label": "2",
                "active": false
            },
            {
                "url": "https://api.kwiga.com/contacts?page=2",
                "label": "translation missing: en.pagination_next &raquo;",
                "active": false
            }
        ],
        "path": "https://api.kwiga.com/contacts",
        "per_page": 15,
        "to": 2,
        "total": 3
    }
}

GET https://api.kwiga.com/contacts

URL Parameters

β–Έ filters object optional
Filter parameters
is_active integer optional
Filter by Active Contacts
date_from datetime optional
Filter by creation date. Parameter 'from'
date_to datetime optional
Filter by creation date. 'To' parameter
last_activity_from datetime optional
Filter by last activity date. Parameter 'from'
last_activity_to datetime optional
Filter by last activity date. 'To' parameter
search string optional
Filter by email, phone, name
utm_source string optional
utm_source
utm_campaign string optional
utm_campaign
utm_medium string optional
utm_medium
utm_term string optional
utm_term
utm_content string optional
utm_content
offers integer[] optional
offers ids
products integer[] optional
products ids
emails string/string[] optional
Filter by exact email address. Accepts a comma-separated string or a repeated array; each value must be a valid email.
contact_ids integer/integer[] optional
Filter by contact ids. Accepts a comma-separated string or a repeated array of integers.
user_ids integer/integer[] optional
Filter by user (student account) ids. Accepts a comma-separated string or a repeated array of integers.

with_orders boolean optional
Additionally get information on contact orders and offers
with_certificates boolean optional
Get additional information on contact certificates
page integer optional
Page Number
Default: 1
per_page integer optional
Number of fetch items
Default: 15
sort_by string optional
Possible values: asc, descDefault: desc

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).

POST alias

POST https://api.kwiga.com/contacts/query

A functional alias for the GET endpoint above. Accepts the same parameters in the request body instead of the query string β€” useful when the filter list is too large to fit a URL (or when JSON is simply easier to build on the client). Body has priority over query string, response shape is identical.

Get contact

Request example:

<?php
// Get contact by ID

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/contacts/:contact', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get contact by ID

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/contacts/:contact' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get contact by ID
const url = 'https://api.kwiga.com/contacts/:contact';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get contact by ID
import requests

url = 'https://api.kwiga.com/contacts/:contact'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/contacts/:contact",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
    "data": {
        "id": 132,
        "email": "bond@example.com",
        "first_name": "James",
        "last_name": "Bond",
        "phone_number": "931234567",
        "tags": [],
        "created_at": "2023-06-26T19:01:00.000000Z",
        "additional_fields": [
            {
                "id": 110,
                "field": {
                    "id": 17,
                    "title": "test global",
                    "is_local": false
                },
                "value": "this is test value"
            }
        ]
    }
}

GET https://api.kwiga.com/contacts/:contact

URL Parameters

with_certificates boolean optional
Get additional information on contact certificates

Response envelope

Create contact

Request example:

<?php
// Create new contact

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'first_name' => 'James',
        'last_name' => 'Bond',
        'email' => 'bond@example.com',
        'send_activation_email' => true,
        'phone' => '+380931234567',
        'manager_ids' => [264, 288],
        'additional_fields' => [{"field_id"=>17, "value"=>"this is test value"}],
    ],
];

$response = $client->request('POST', '/contacts', $options);

$result = json_decode($response->getBody());
?>
<?php
// Create new contact

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'first_name' => 'James',
    'last_name' => 'Bond',
    'email' => 'bond@example.com',
    'send_activation_email' => true,
    'phone' => '+380931234567',
    'manager_ids' => [264, 288],
    'additional_fields' => [{"field_id"=>17, "value"=>"this is test value"}],
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/contacts' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"first_name":"James","last_name":"Bond","email":"bond@example.com","send_activation_email":true,"phone":"+380931234567","manager_ids":[264,288],"additional_fields":[{"field_id":17,"value":"this is test value"}]}'
// Create new contact
const url = 'https://api.kwiga.com/contacts';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'first_name': 'James',
    'last_name': 'Bond',
    'email': 'bond@example.com',
    'send_activation_email': true,
    'phone': '+380931234567',
    'manager_ids': [264, 288],
    'additional_fields': [{"field_id"=>17, "value"=>"this is test value"}]
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Create new contact
import requests

url = 'https://api.kwiga.com/contacts'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'first_name': 'James',
    'last_name': 'Bond',
    'email': 'bond@example.com',
    'send_activation_email': true,
    'phone': '+380931234567',
    'manager_ids': [264, 288],
    'additional_fields': [{"field_id"=>17, "value"=>"this is test value"}],
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "first_name": "James",
    "last_name": "Bond",
    "email": "bond@example.com",
    "send_activation_email": true,
    "phone": "+380931234567",
    "manager_ids": [
      264,
      288
    ],
    "additional_fields": [
      {
        "field_id": 17,
        "value": "this is test value"
      }
    ]
  }
}

Example of response:

{
    "data": {
        "id": 132,
        "email": "bond@example.com",
        "first_name": "James",
        "last_name": "Bond",
        "phone_number": "931234567",
        "tags": [],
        "created_at": "2023-06-26T19:01:00.000000Z",
        "additional_fields": [
            {
                "id": 110,
                "field": {
                    "id": 17,
                    "title": "test global",
                    "is_local": false
                },
                "value": "this is test value"
            }
        ]
    }
}

POST https://api.kwiga.com/contacts

Request

email string required
Email - should be unique value within an account
phone string optional
Phone number β€” format: +{country code}{phone}
first_name string optional
Contact first name
middle_name string optional
Contact middle name (or patronymic)
last_name string optional
Contact last name
name string optional
Full name as a single string. Use this when you do not have first/middle/last split available; the server will keep it as the contact's name.
tags string[] optional
Tags
send_activation_email boolean optional
Send a welcome email to the contact.
Default: false
locale string optional
Contact's language in iso_2 format. See the supported locales list for all values.
Default: en
create_order boolean optional
Create an empty order
order_stage_id integer optional
Identifier of order stage in funnel (can be obtained in CRM → Orders → Settings → Status list)
β–Έ additional_fields object[] optional
Custom fields
field_id integer optional
Custom field id (can be obtained in CRM → Contacts → Settings → Add custom fields)
value string optional
Custom field value

Response envelope

Contact update

Request example:

<?php
// Update contact

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'first_name' => 'John',
        'last_name' => 'Black',
        'email' => 'bond123@example.com',
        'phone' => '+380931112233',
        'tags' => ["test-tag"],
    ],
];

$response = $client->request('PUT', '/contacts/:contact', $options);

$result = json_decode($response->getBody());
?>
<?php
// Update contact

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');

$data = [
    'first_name' => 'John',
    'last_name' => 'Black',
    'email' => 'bond123@example.com',
    'phone' => '+380931112233',
    'tags' => ["test-tag"],
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request PUT 'https://api.kwiga.com/contacts/:contact' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"first_name":"John","last_name":"Black","email":"bond123@example.com","phone":"+380931112233","tags":["test-tag"]}'
// Update contact
const url = 'https://api.kwiga.com/contacts/:contact';

const options = {
  method: 'PUT',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'first_name': 'John',
    'last_name': 'Black',
    'email': 'bond123@example.com',
    'phone': '+380931112233',
    'tags': ["test-tag"]
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Update contact
import requests

url = 'https://api.kwiga.com/contacts/:contact'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'first_name': 'John',
    'last_name': 'Black',
    'email': 'bond123@example.com',
    'phone': '+380931112233',
    'tags': ["test-tag"],
}

response = requests.put(url, headers=headers, json=data)

result = response.json()
{
  "method": "PUT",
  "url": "https://api.kwiga.com/contacts/:contact",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "first_name": "John",
    "last_name": "Black",
    "email": "bond123@example.com",
    "phone": "+380931112233",
    "tags": [
      "test-tag"
    ]
  }
}

Example of response:

{
    "data": {
        "id": 132,
        "email": "bond@example.com",
        "first_name": "James",
        "last_name": "Bond",
        "phone_number": "931234567",
        "tags": [],
        "created_at": "2023-06-26T19:01:00.000000Z",
        "additional_fields": [
            {
                "id": 110,
                "field": {
                    "id": 17,
                    "title": "test global",
                    "is_local": false
                },
                "value": "this is test value"
            }
        ]
    }
}

PUT https://api.kwiga.com/contacts/:contact

Request

email string optional
Email - should be unique value within an account
phone string optional
Phone number β€” format: +{country code}{phone}
first_name string optional
Contact first name
last_name string optional
Contact last name
tags string[] optional
Tags. They work in sync mode. That is, the contact will only have those tags that will be transferred
β–Έ additional_fields object[] optional
Custom fields
field_id integer optional
Custom field id (can be obtained in CRM → Contacts → Settings → Add custom fields)
value string optional
Custom field value

Response envelope

Add purchase

This method creates or finds contact, as well as adds purchase of the specified offer

Request example:

<?php
// Add purchase to contact

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'first_name' => 'James',
        'last_name' => 'Bond',
        'email' => 'bond@example.com',
        'send_activation_email' => true,
        'phone' => '+380931234567',
        'offer_id' => 18,
        'additional_fields' => [{"field_id"=>17, "value"=>"this is test value"}],
    ],
];

$response = $client->request('POST', '/contacts/purchases', $options);

$result = json_decode($response->getBody());
?>
<?php
// Add purchase to contact

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/purchases');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'first_name' => 'James',
    'last_name' => 'Bond',
    'email' => 'bond@example.com',
    'send_activation_email' => true,
    'phone' => '+380931234567',
    'offer_id' => 18,
    'additional_fields' => [{"field_id"=>17, "value"=>"this is test value"}],
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/contacts/purchases' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"first_name":"James","last_name":"Bond","email":"bond@example.com","send_activation_email":true,"phone":"+380931234567","offer_id":18,"additional_fields":[{"field_id":17,"value":"this is test value"}]}'
// Add purchase to contact
const url = 'https://api.kwiga.com/contacts/purchases';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'first_name': 'James',
    'last_name': 'Bond',
    'email': 'bond@example.com',
    'send_activation_email': true,
    'phone': '+380931234567',
    'offer_id': 18,
    'additional_fields': [{"field_id"=>17, "value"=>"this is test value"}]
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Add purchase to contact
import requests

url = 'https://api.kwiga.com/contacts/purchases'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'first_name': 'James',
    'last_name': 'Bond',
    'email': 'bond@example.com',
    'send_activation_email': true,
    'phone': '+380931234567',
    'offer_id': 18,
    'additional_fields': [{"field_id"=>17, "value"=>"this is test value"}],
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/purchases",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "first_name": "James",
    "last_name": "Bond",
    "email": "bond@example.com",
    "send_activation_email": true,
    "phone": "+380931234567",
    "offer_id": 18,
    "additional_fields": [
      {
        "field_id": 17,
        "value": "this is test value"
      }
    ]
  }
}

Example of response:

{
    "data": {
        "id": 132,
        "email": "bond@example.com",
        "first_name": "James",
        "last_name": "Bond",
        "phone_number": "931234567",
        "tags": [],
        "created_at": "2023-06-26T19:01:00.000000Z",
        "additional_fields": [
            {
                "id": 110,
                "field": {
                    "id": 17,
                    "title": "test global",
                    "is_local": false
                },
                "value": "this is test value"
            }
        ],
        "orders": [
            {
                "id": 1060,
                "type_id": 1,
                "first_paid_at": "2023-10-13T18:04:02.000000Z",
                "paid_at": "2023-10-13T18:04:02.000000Z",
                "created_at": "2023-10-13T18:04:02.000000Z",
                "updated_at": "2023-10-13T18:04:02.000000Z",
                "products": [
                    {
                        "id": 25,
                        "productable_id": 10,
                        "productable_type": "course",
                        "title": "test course",
                        "url": "https://sample-school.kwiga.com/courses/test-course"
                    }
                ],
                "payments": [
                    {
                        "id": 1231,
                        "status": 2,
                        "status_title": "Paid",
                        "payment_type": null,
                        "payment_type_title": null,
                        "payment_form": null,
                        "payment_form_title": null,
                        "price_info": {
                            "amount": 0,
                            "currency": {
                                "id": 147,
                                "code": "UAH",
                                "html_code": "β‚΄",
                                "html_letter_code": "Π³Ρ€Π½"
                            }
                        },
                        "paid_at": "2023-10-13T18:04:02.000000Z",
                        "created_at": "2023-10-13T18:04:02.000000Z",
                        "updated_at": "2023-10-13T18:04:02.000000Z",
                        "transactions": [
                            {
                                "id": 28,
                                "merchant_id": 2,
                                "payment_id": 1231,
                                "order_id": 1060,
                                "payment_system_status": "Declined",
                                "failure_reason": "Cardholder session expired",
                                "price": "147",
                                "currency_code": "UAH",
                                "payer_account": null,
                                "card_mask": null,
                                "rrn": null,
                                "fee": null,
                                "created_at": "2023-11-03T08:45:10.000000Z"
                            },
                            {
                                "id": 29,
                                "merchant_id": 2,
                                "payment_id": 1231,
                                "order_id": 1060,
                                "payment_system_status": "Approved",
                                "failure_reason": null,
                                "price": "147",
                                "currency_code": "UAH",
                                "payer_account": "JKNNASWTZ99SJ",
                                "card_mask": "53****2144",
                                "rrn": 330710335365,
                                "fee": null,
                                "created_at": "2023-11-03T08:48:10.000000Z"
                            }
                        ]
                    }
                ],
                "paid_status": "paid",
                "paid_status_title": "Π‘ΠΏΠ»Π°Ρ‡Π΅Π½ΠΎ",
                "order_stage": {
                    "id": 43,
                    "title": "Π‘Ρ‚Π΅ΠΉΠ΄ΠΆ 1",
                    "order_group": {
                        "id": 22,
                        "slug": "voronka-1",
                        "title": null,
                        "order": 4,
                        "created_at": "2023-06-22T18:20:53.000000Z"
                    },
                    "order_funnel": {
                        "id": 1,
                        "title": "Π’ΠΎΡ€ΠΎΠ½ΠΊΠ° Π·Π° замовчуванням",
                        "created_at": "2023-05-26T09:47:16.000000Z"
                    },
                    "created_at": "2023-06-22T18:21:10.000000Z"
                },
                "cost_info": {
                    "amount": 0,
                    "currency": {
                        "id": 147,
                        "code": "UAH",
                        "html_code": "β‚΄",
                        "html_letter_code": "Π³Ρ€Π½"
                    }
                },
                "managers": [
                    {
                        "id": 264,
                        "name": "test",
                        "email": "testfsdf@fdafasd.fsd"
                    },
                    {
                        "id": 288,
                        "name": "fdsf",
                        "email": "sdfs@fdf.dfss"
                    }
                ]
            }
        ]
    }
}

POST https://api.kwiga.com/contacts/purchases

Request

email string required
Email - should be unique value within an account
phone string optional
Phone number β€” format: +{country code}{phone}
first_name string optional
Contact first name
middle_name string optional
Contact middle name (or patronymic)
last_name string optional
Contact last name
name string optional
Full name as a single string. Use this when you do not have first/middle/last split available; the server will keep it as the contact's name.
tags string[] optional
Tags
send_activation_email boolean optional
Send a welcome email to the contact.
Default: false
send_product_access_email boolean optional
Send an email telling the contact that product access has been granted.
Default: false
send_payment_success_email boolean optional
Send an email confirming a successful offer payment.
Default: false
locale string optional
Contact's language in iso_2 format. See the supported locales list for all values.
Default: en
offer_id integer optional
Offer identifier β€” taken from the end of the offer edit page URL (e.g. https://sample-school.kwiga.com/expert/payments/offers/edit/3858). If not provided, the request falls back to the product_ids field.
product_ids integer[] optional
Array of product IDs.
If offer_id and product_ids are missing, an empty order will be created
order_stage_id integer optional
Identifier of order stage in funnel (can be obtained in CRM → Orders → Settings → Status list)
is_paid boolean optional
Marks the created order as paid.
Default: true
manager_ids integer[] optional
Managers to order (can be obtained in *Settings->Administration access*)
comment string optional
Order comment. Max: 5000
Max length: 5000 characters
β–Έ additional_fields object[] optional
Custom fields
field_id integer optional
Custom field id (can be obtained in CRM → Contacts → Settings → Add custom fields)
value string optional
Custom field value

Response envelope

Tags

Contact tags adding

Request example:

<?php
// Add tags to contacts

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'contacts' => [23698],
        'tags' => ["test-tag"],
    ],
];

$response = $client->request('POST', '/contacts/tags', $options);

$result = json_decode($response->getBody());
?>
<?php
// Add tags to contacts

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/tags');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'contacts' => [23698],
    'tags' => ["test-tag"],
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/contacts/tags' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"contacts":[23698],"tags":["test-tag"]}'
// Add tags to contacts
const url = 'https://api.kwiga.com/contacts/tags';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'contacts': [23698],
    'tags': ["test-tag"]
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Add tags to contacts
import requests

url = 'https://api.kwiga.com/contacts/tags'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'contacts': [23698],
    'tags': ["test-tag"],
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/tags",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "contacts": [
      23698
    ],
    "tags": [
      "test-tag"
    ]
  }
}

Example of response:

{
    "success": true
}

POST https://api.kwiga.com/contacts/tags

Request

contact_ids integer/integer[] optional
Filter by contact ids. Accepts a comma-separated string or a repeated array of integers.
contacts integer[] optional
Legacy alias for contact_ids. Kept for backwards compatibility; new integrations should use contact_ids.
tags string[] required
Tags

Response envelope

Response body is returned without any wrapping.

Contact tags removing

Request example:

<?php
// Remove tags from contacts

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'contacts' => [23698],
        'tags' => ["test-tag"],
    ],
];

$response = $client->request('DELETE', '/contacts/tags', $options);

$result = json_decode($response->getBody());
?>
<?php
// Remove tags from contacts

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/tags');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');

$data = [
    'contacts' => [23698],
    'tags' => ["test-tag"],
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request DELETE 'https://api.kwiga.com/contacts/tags' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"contacts":[23698],"tags":["test-tag"]}'
// Remove tags from contacts
const url = 'https://api.kwiga.com/contacts/tags';

const options = {
  method: 'DELETE',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'contacts': [23698],
    'tags': ["test-tag"]
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Remove tags from contacts
import requests

url = 'https://api.kwiga.com/contacts/tags'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'contacts': [23698],
    'tags': ["test-tag"],
}

response = requests.delete(url, headers=headers, json=data)

result = response.json()
{
  "method": "DELETE",
  "url": "https://api.kwiga.com/contacts/tags",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "contacts": [
      23698
    ],
    "tags": [
      "test-tag"
    ]
  }
}

Example of response:

{
    "success": true
}

DELETE https://api.kwiga.com/contacts/tags

Request

contact_ids integer/integer[] optional
Filter by contact ids. Accepts a comma-separated string or a repeated array of integers.
contacts integer[] optional
Legacy alias for contact_ids. Kept for backwards compatibility; new integrations should use contact_ids.
tags string[] required
Tags

Response envelope

Response body is returned without any wrapping.

Products & subscriptions

Contact products list

Request example:

<?php
// Get contact products

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/contacts/:contact/products', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get contact products

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/products');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/contacts/:contact/products' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get contact products
const url = 'https://api.kwiga.com/contacts/:contact/products';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get contact products
import requests

url = 'https://api.kwiga.com/contacts/:contact/products'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/contacts/:contact/products",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": [
    {
      "id": 299,
      "productable_id": 142,
      "productable_type": "course",
      "title": "Copy Test backup",
      "image_url": "",
      "is_published": false,
      "aggregated_subscription": {
        "is_active": true,
        "is_paid": true,
        "start_at": "2025-05-05T15:09:04.000000Z",
        "end_at": null,
        "offer_end_at": null,
        "order_end_at": null,
        "count_available_days": 39,
        "count_left_days": null,
        "state": {
          "id": 2,
          "name": "Open",
          "title": "Open"
        }
      },
      "subscriptions": [
        {
          "id": 4775,
          "creator_id": 24457,
          "user_id": 24457,
          "product_id": 299,
          "order_id": 2710,
          "offer_id": 757,
          "is_active": true,
          "start_at": "2025-05-05T15:09:04.000000Z",
          "order_end_at": null,
          "end_at": null,
          "paid_at": "2025-05-05T15:09:04.000000Z",
          "created_at": "2025-05-05T15:09:04.000000Z",
          "updated_at": "2025-05-05T15:09:04.000000Z"
        }
      ]
    }
  ]
}

GET https://api.kwiga.com/contacts/:contact/products

Returns a list of contact products with subscription and access period information. The aggregated_subscription parameter contains summary information about the terms and activity of access based on all user subscriptions for the product. The subscriptions parameter contains an array of all user subscriptions for the product.

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).

Delete product from contact

Request example:

<?php
// Delete contact product

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('DELETE', '/contacts/:contact/products/:product', $options);

$result = json_decode($response->getBody());
?>
<?php
// Delete contact product

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/products/:product');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request DELETE 'https://api.kwiga.com/contacts/:contact/products/:product' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Delete contact product
const url = 'https://api.kwiga.com/contacts/:contact/products/:product';

const options = {
  method: 'DELETE',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Delete contact product
import requests

url = 'https://api.kwiga.com/contacts/:contact/products/:product'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.delete(url, headers=headers)

result = response.json()
{
  "method": "DELETE",
  "url": "https://api.kwiga.com/contacts/:contact/products/:product",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "success": true
}

DELETE https://api.kwiga.com/contacts/:contact/products/:product

Deletes contact subscriptions for the specified product. Orders become custom without access to deleted products.

Response envelope

Response body is returned without any wrapping.

Delete products from contact (bulk)

Request example:

<?php
// Delete contact products in bulk

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'email' => 'user@example.com',
        'product_id' => 299,
        'offers' => [421],
    ],
];

$response = $client->request('DELETE', '/contacts/products', $options);

$result = json_decode($response->getBody());
?>
<?php
// Delete contact products in bulk

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/products');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');

$data = [
    'email' => 'user@example.com',
    'product_id' => 299,
    'offers' => [421],
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request DELETE 'https://api.kwiga.com/contacts/products' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"email":"user@example.com","product_id":299,"offers":[421]}'
// Delete contact products in bulk
const url = 'https://api.kwiga.com/contacts/products';

const options = {
  method: 'DELETE',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'email': 'user@example.com',
    'product_id': 299,
    'offers': [421]
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Delete contact products in bulk
import requests

url = 'https://api.kwiga.com/contacts/products'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'email': 'user@example.com',
    'product_id': 299,
    'offers': [421],
}

response = requests.delete(url, headers=headers, json=data)

result = response.json()
{
  "method": "DELETE",
  "url": "https://api.kwiga.com/contacts/products",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "email": "user@example.com",
    "product_id": 299,
    "offers": [
      421
    ]
  }
}

Example of response:

{
  "data": {
    "affected_subscriptions": [
      2309,
      2661
    ],
    "affected_orders": [
      1578,
      1784
    ],
    "affected_by_offers": {
      "421": {
        "affected_subscriptions": [
          2309,
          2661
        ],
        "affected_orders": [
          1578,
          1784
        ]
      }
    },
    "affected_by_products": []
  },
  "contact_id": 125
}

DELETE https://api.kwiga.com/contacts/products

Deletes subscriptions by specified conditions, and orders become custom without access to deleted products. You can combine product_id and offers parameters to delete part of products by specific offer.

Parameters

email string optional
Contact email. Required if contact_id is not provided
contact_id integer optional
Contact id. Required if email is not provided
product_id integer optional
Product id. Required if offers is not provided. Can be obtained from the Contact Products List endpoint or from the Courses List (it will be the product_id field in the course)
offers integer[] optional
Array of offer ids by which subscriptions are deleted. Required if product_id is not provided. Can be obtained from the offer edit page URL or from the API in the contact offers list.

Response envelope

Payload of the response.
contact_id integer
ID of the contact whose subscriptions were affected

Freeze contact subscription

Request example:

<?php
// Freeze the subscription for 30 days

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'count_frozen_days' => 30,
    ],
];

$response = $client->request('POST', '/contacts/:contact/products/:product/freeze', $options);

$result = json_decode($response->getBody());
?>
<?php
// Freeze the subscription for 30 days

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/products/:product/freeze');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'count_frozen_days' => 30,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/contacts/:contact/products/:product/freeze' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"count_frozen_days":30}'
// Freeze the subscription for 30 days
const url = 'https://api.kwiga.com/contacts/:contact/products/:product/freeze';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'count_frozen_days': 30
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Freeze the subscription for 30 days
import requests

url = 'https://api.kwiga.com/contacts/:contact/products/:product/freeze'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'count_frozen_days': 30,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/:contact/products/:product/freeze",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "count_frozen_days": 30
  }
}

Example of response:

{
  "data": {
    "id": 299,
    "productable_id": 142,
    "productable_type": "course",
    "title": "JS Foundations",
    "image_url": "https://cdn.kwiga.com/preview.jpg",
    "url": "https://kwiga.com/courses/js-foundations",
    "is_published": true,
    "aggregated_subscription": {
      "is_active": false,
      "is_paid": true,
      "start_at": "2026-06-05T00:00:00.000000Z",
      "end_at": "2026-08-04T00:00:00.000000Z",
      "offer_end_at": null,
      "order_end_at": "2026-08-04T00:00:00.000000Z",
      "frozen_at": "2026-05-20T10:00:00.000000Z",
      "extended_at": null,
      "count_available_days": 60,
      "count_left_days": 60,
      "state": {
        "id": 4,
        "name": "Frozen",
        "title": "Frozen"
      }
    },
    "subscriptions": [
      {
        "id": 4775,
        "creator_id": 24457,
        "user_id": 24457,
        "product_id": 299,
        "order_id": 2710,
        "offer_id": 757,
        "is_active": false,
        "start_at": "2026-06-05T00:00:00.000000Z",
        "order_end_at": "2026-08-04T00:00:00.000000Z",
        "end_at": "2026-08-04T00:00:00.000000Z",
        "frozen_at": "2026-05-20T10:00:00.000000Z",
        "extended_at": null,
        "paid_at": "2026-05-05T15:09:04.000000Z",
        "created_at": "2026-05-05T15:09:04.000000Z",
        "updated_at": "2026-05-06T12:42:11.000000Z"
      }
    ]
  }
}

POST https://api.kwiga.com/contacts/:contact/products/:product/freeze

Freezes all of the contact's active subscriptions for the given product for count_frozen_days calendar days. The freeze shifts start_at / end_at / order_end_at forward, marks the subscription inactive, and notifies the student. On unfreeze (manual or automatic when the freeze period elapses) start_at is restored from the contact's order data, not just moved back by the same delta.

URL Parameters

count_frozen_days integer required
Number of days to freeze the subscription for (1–400).
Range: 1 – 400

Response envelope

Payload of the response.

Unfreeze contact subscription

Request example:

<?php
// Unfreeze the subscription (no body)

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('POST', '/contacts/:contact/products/:product/unfreeze', $options);

$result = json_decode($response->getBody());
?>
<?php
// Unfreeze the subscription (no body)

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/products/:product/unfreeze');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/contacts/:contact/products/:product/unfreeze' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Unfreeze the subscription (no body)
const url = 'https://api.kwiga.com/contacts/:contact/products/:product/unfreeze';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Unfreeze the subscription (no body)
import requests

url = 'https://api.kwiga.com/contacts/:contact/products/:product/unfreeze'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.post(url, headers=headers)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/:contact/products/:product/unfreeze",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": {
    "id": 299,
    "productable_id": 142,
    "productable_type": "course",
    "title": "JS Foundations",
    "image_url": "https://cdn.kwiga.com/preview.jpg",
    "url": "https://kwiga.com/courses/js-foundations",
    "is_published": true,
    "aggregated_subscription": {
      "is_active": false,
      "is_paid": true,
      "start_at": "2026-06-05T00:00:00.000000Z",
      "end_at": "2026-08-04T00:00:00.000000Z",
      "offer_end_at": null,
      "order_end_at": "2026-08-04T00:00:00.000000Z",
      "frozen_at": "2026-05-20T10:00:00.000000Z",
      "extended_at": null,
      "count_available_days": 60,
      "count_left_days": 60,
      "state": {
        "id": 4,
        "name": "Frozen",
        "title": "Frozen"
      }
    },
    "subscriptions": [
      {
        "id": 4775,
        "creator_id": 24457,
        "user_id": 24457,
        "product_id": 299,
        "order_id": 2710,
        "offer_id": 757,
        "is_active": false,
        "start_at": "2026-06-05T00:00:00.000000Z",
        "order_end_at": "2026-08-04T00:00:00.000000Z",
        "end_at": "2026-08-04T00:00:00.000000Z",
        "frozen_at": "2026-05-20T10:00:00.000000Z",
        "extended_at": null,
        "paid_at": "2026-05-05T15:09:04.000000Z",
        "created_at": "2026-05-05T15:09:04.000000Z",
        "updated_at": "2026-05-06T12:42:11.000000Z"
      }
    ]
  }
}

POST https://api.kwiga.com/contacts/:contact/products/:product/unfreeze

Unfreezes all of the contact's frozen subscriptions on the given product. The unfreeze restores start_at, recomputes end_at / order_end_at (subtracting any unused part of the freeze period), reactivates access where applicable, and notifies the student. Body is empty.

Response envelope

Payload of the response.

Extend contact subscription

Request example:

<?php
// Extend access by 14 days

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'count_extend_days' => 14,
    ],
];

$response = $client->request('POST', '/contacts/:contact/products/:product/extend', $options);

$result = json_decode($response->getBody());
?>
<?php
// Extend access by 14 days

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/products/:product/extend');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'count_extend_days' => 14,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/contacts/:contact/products/:product/extend' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"count_extend_days":14}'
// Extend access by 14 days
const url = 'https://api.kwiga.com/contacts/:contact/products/:product/extend';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'count_extend_days': 14
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Extend access by 14 days
import requests

url = 'https://api.kwiga.com/contacts/:contact/products/:product/extend'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'count_extend_days': 14,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/:contact/products/:product/extend",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "count_extend_days": 14
  }
}

Example of response:

{
  "data": {
    "id": 299,
    "productable_id": 142,
    "productable_type": "course",
    "title": "JS Foundations",
    "image_url": "https://cdn.kwiga.com/preview.jpg",
    "url": "https://kwiga.com/courses/js-foundations",
    "is_published": true,
    "aggregated_subscription": {
      "is_active": false,
      "is_paid": true,
      "start_at": "2026-06-05T00:00:00.000000Z",
      "end_at": "2026-08-04T00:00:00.000000Z",
      "offer_end_at": null,
      "order_end_at": "2026-08-04T00:00:00.000000Z",
      "frozen_at": "2026-05-20T10:00:00.000000Z",
      "extended_at": null,
      "count_available_days": 60,
      "count_left_days": 60,
      "state": {
        "id": 4,
        "name": "Frozen",
        "title": "Frozen"
      }
    },
    "subscriptions": [
      {
        "id": 4775,
        "creator_id": 24457,
        "user_id": 24457,
        "product_id": 299,
        "order_id": 2710,
        "offer_id": 757,
        "is_active": false,
        "start_at": "2026-06-05T00:00:00.000000Z",
        "order_end_at": "2026-08-04T00:00:00.000000Z",
        "end_at": "2026-08-04T00:00:00.000000Z",
        "frozen_at": "2026-05-20T10:00:00.000000Z",
        "extended_at": null,
        "paid_at": "2026-05-05T15:09:04.000000Z",
        "created_at": "2026-05-05T15:09:04.000000Z",
        "updated_at": "2026-05-06T12:42:11.000000Z"
      }
    ]
  }
}

POST https://api.kwiga.com/contacts/:contact/products/:product/extend

Adds count_extend_days calendar days to end_at / order_end_at of every subscription the contact has on this product. For automation-style use, the same endpoint powers re-activation of expired subscriptions in CRM.

URL Parameters

count_extend_days integer required
Number of days to extend the subscription by (1–400).
Range: 1 – 400

Response envelope

Payload of the response.

Change subscription end date

Request example:

<?php
// Set the subscription end to the given date in the chosen timezone

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'end_at' => '2027-01-01 23:59:59',
        'timezone_id' => 1,
    ],
];

$response = $client->request('PUT', '/contacts/:contact/products/:product/end-date', $options);

$result = json_decode($response->getBody());
?>
<?php
// Set the subscription end to the given date in the chosen timezone

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/products/:product/end-date');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');

$data = [
    'end_at' => '2027-01-01 23:59:59',
    'timezone_id' => 1,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request PUT 'https://api.kwiga.com/contacts/:contact/products/:product/end-date' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"end_at":"2027-01-01 23:59:59","timezone_id":1}'
// Set the subscription end to the given date in the chosen timezone
const url = 'https://api.kwiga.com/contacts/:contact/products/:product/end-date';

const options = {
  method: 'PUT',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'end_at': '2027-01-01 23:59:59',
    'timezone_id': 1
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Set the subscription end to the given date in the chosen timezone
import requests

url = 'https://api.kwiga.com/contacts/:contact/products/:product/end-date'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'end_at': '2027-01-01 23:59:59',
    'timezone_id': 1,
}

response = requests.put(url, headers=headers, json=data)

result = response.json()
{
  "method": "PUT",
  "url": "https://api.kwiga.com/contacts/:contact/products/:product/end-date",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "end_at": "2027-01-01 23:59:59",
    "timezone_id": 1
  }
}

Example of response:

{
  "data": {
    "id": 299,
    "productable_id": 142,
    "productable_type": "course",
    "title": "JS Foundations",
    "image_url": "https://cdn.kwiga.com/preview.jpg",
    "url": "https://kwiga.com/courses/js-foundations",
    "is_published": true,
    "aggregated_subscription": {
      "is_active": false,
      "is_paid": true,
      "start_at": "2026-06-05T00:00:00.000000Z",
      "end_at": "2026-08-04T00:00:00.000000Z",
      "offer_end_at": null,
      "order_end_at": "2026-08-04T00:00:00.000000Z",
      "frozen_at": "2026-05-20T10:00:00.000000Z",
      "extended_at": null,
      "count_available_days": 60,
      "count_left_days": 60,
      "state": {
        "id": 4,
        "name": "Frozen",
        "title": "Frozen"
      }
    },
    "subscriptions": [
      {
        "id": 4775,
        "creator_id": 24457,
        "user_id": 24457,
        "product_id": 299,
        "order_id": 2710,
        "offer_id": 757,
        "is_active": false,
        "start_at": "2026-06-05T00:00:00.000000Z",
        "order_end_at": "2026-08-04T00:00:00.000000Z",
        "end_at": "2026-08-04T00:00:00.000000Z",
        "frozen_at": "2026-05-20T10:00:00.000000Z",
        "extended_at": null,
        "paid_at": "2026-05-05T15:09:04.000000Z",
        "created_at": "2026-05-05T15:09:04.000000Z",
        "updated_at": "2026-05-06T12:42:11.000000Z"
      }
    ]
  }
}

PUT https://api.kwiga.com/contacts/:contact/products/:product/end-date

Replaces end_at (and order_end_at when present) with an explicit datetime. The provided value is interpreted in the cabinet timezone identified by timezone_id (see the Timezones list endpoint); when timezone_id is omitted, end_at is treated as UTC.

URL Parameters

end_at datetime required
New subscription end date. Interpreted in the cabinet timezone identified by timezone_id.
timezone_id integer optional
Timezone id from the Timezones list endpoint. Optional β€” when omitted, end_at is treated as UTC.

Response envelope

Payload of the response.

Rewards (points)

List contact rewards (points)

Request example:

<?php
// List rewards (default ordering)

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/contacts/:contact/rewards', $options);

$result = json_decode($response->getBody());
?>


# ---
# Filtered example
# ---

<?php
// Only accruals from quiz-passed and manual reasons, scoped to two products

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/contacts/:contact/rewards?filters[accrual_type]=accrued&filters[reason_types]=1,6&filters[product_ids]=10,11&per_page=50', $options);

$result = json_decode($response->getBody());
?>
<?php
// List rewards (default ordering)

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/rewards');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Filtered example
# ---

<?php
// Only accruals from quiz-passed and manual reasons, scoped to two products

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/rewards?filters[accrual_type]=accrued&filters[reason_types]=1,6&filters[product_ids]=10,11&per_page=50');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/contacts/:contact/rewards' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# Filtered example
# ---

curl --location --request GET 'https://api.kwiga.com/contacts/:contact/rewards?filters[accrual_type]=accrued&filters[reason_types]=1,6&filters[product_ids]=10,11&per_page=50' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// List rewards (default ordering)
const url = 'https://api.kwiga.com/contacts/:contact/rewards';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Filtered example
# ---

// Only accruals from quiz-passed and manual reasons, scoped to two products
const url = 'https://api.kwiga.com/contacts/:contact/rewards?filters[accrual_type]=accrued&filters[reason_types]=1,6&filters[product_ids]=10,11&per_page=50';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# List rewards (default ordering)
import requests

url = 'https://api.kwiga.com/contacts/:contact/rewards'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# Filtered example
# ---

# Only accruals from quiz-passed and manual reasons, scoped to two products
import requests

url = 'https://api.kwiga.com/contacts/:contact/rewards?filters[accrual_type]=accrued&filters[reason_types]=1,6&filters[product_ids]=10,11&per_page=50'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/contacts/:contact/rewards",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# Filtered example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/contacts/:contact/rewards?filters[accrual_type]=accrued&filters[reason_types]=1,6&filters[product_ids]=10,11&per_page=50",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": [
    {
      "id": 9821,
      "points": 50,
      "accrual_type": {
        "id": "accrued",
        "slug": "Plus",
        "title": "Accrued"
      },
      "reason_type": {
        "id": 6,
        "slug": "Manual",
        "title": "Manual"
      },
      "event": null,
      "event_name": null,
      "eventable_type": null,
      "eventable_id": null,
      "event_comment": null,
      "manual_comment": "Bonus for active chat participation",
      "is_visible_to_student": true,
      "message": "Manual points accrual: Bonus for active chat participation",
      "product": null,
      "creator": {
        "id": 17,
        "name": "Alice Curator",
        "email": "alice@kwiga.com"
      },
      "created_at": "2026-05-24T11:14:51.000000Z"
    },
    {
      "id": 9820,
      "points": 25,
      "accrual_type": {
        "id": "accrued",
        "slug": "Plus",
        "title": "Accrued"
      },
      "reason_type": {
        "id": 1,
        "slug": "QuizPassed",
        "title": "Quiz passed"
      },
      "event": "quiz.passed",
      "event_name": "Quiz passed",
      "eventable_type": "quiz_attempt",
      "eventable_id": 4821,
      "event_comment": null,
      "manual_comment": null,
      "is_visible_to_student": null,
      "message": "Quiz Module 3 final test in lesson Closures & scope -> Attempt",
      "product": {
        "id": 431,
        "productable_type": "course",
        "productable_id": 226,
        "name": "JS Foundations",
        "url": "https://lm4.kwiga.com/courses/js-foundations"
      },
      "creator": null,
      "created_at": "2026-05-23T11:14:51.000000Z"
    },
    {
      "id": 9810,
      "points": -30,
      "accrual_type": {
        "id": "deducted",
        "slug": "Minus",
        "title": "Deducted"
      },
      "reason_type": {
        "id": 6,
        "slug": "Manual",
        "title": "Manual"
      },
      "event": null,
      "event_name": null,
      "eventable_type": null,
      "eventable_id": null,
      "event_comment": null,
      "manual_comment": null,
      "is_visible_to_student": null,
      "message": "Manual points accrual",
      "product": null,
      "creator": {
        "id": 17,
        "name": "Alice Curator",
        "email": "alice@kwiga.com"
      },
      "created_at": "2026-05-22T09:00:00.000000Z"
    }
  ],
  "links": {
    "first": "https://api.kwiga.com/contacts/142/rewards?page=1",
    "last": "https://api.kwiga.com/contacts/142/rewards?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "path": "https://api.kwiga.com/contacts/142/rewards",
    "per_page": 15,
    "to": 3,
    "total": 3
  },
  "sum_points": 45,
  "sum_accrued_points": 75,
  "sum_deducted_points": -30
}

GET https://api.kwiga.com/contacts/:contact/rewards

Returns the contact's points journal β€” every accrual and deduction tied to quizzes, orders, gifts, automations or manual actions by curators. The response envelope adds sum_points, sum_accrued_points, sum_deducted_points for the filtered window.

URL Parameters

β–Έ filters object optional
translation missing: en.contacts.params.rewards_filters
product_ids integer/integer[] optional
Filter rewards by product ids the points were tied to.
accrual_type string optional
Filter by direction of the transfer: accrued (positive points) or deducted (negative points).
Example: accruedPossible values: accrued, deducted
reason_types integer/integer[] optional

Filter by reward reason id. Combine multiple values to widen the result.

  • 1 — Quiz passed
  • 2 — Quiz canceled
  • 3 — Quiz attempt reset previous scores
  • 4 — Quiz attempt set new scores
  • 5 — Pay by points
  • 6 — Manual
  • 7 — Automation
  • 8 — Quiz scores changing
  • 9 — Pay by points for gift
Possible values: 1, 2, 3, 4, 5, 6, 7, 8, 9

per_page integer optional
Items per page.
Maximum: 500Default: 15
page integer optional
Page number.
Default: 1

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).
sum_points number
Net points for the filtered list (accruals minus deductions).
Sum of positive entries (accruals only) for the filtered list.
Sum of negative entries (deductions only) for the filtered list.

Add a manual reward (points) to a contact Idempotent

Request example:

<?php
// Accrue 50 points for finishing a milestone, unbound from any product

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'points' => 50,
    ],
];

$response = $client->request('POST', '/contacts/:contact/rewards', $options);

$result = json_decode($response->getBody());
?>


# ---
# Accrue_with_product example
# ---

<?php
// Accrue 100 points scoped to product id 10

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'points' => 100,
        'product_id' => 10,
    ],
];

$response = $client->request('POST', '/contacts/:contact/rewards', $options);

$result = json_decode($response->getBody());
?>


# ---
# With_comment example
# ---

<?php
// Accrue 50 points with a curator comment that the student also sees in their journal

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'points' => 50,
        'comment' => 'Bonus for active chat participation',
        'is_visible_to_student' => true,
    ],
];

$response = $client->request('POST', '/contacts/:contact/rewards', $options);

$result = json_decode($response->getBody());
?>


# ---
# Idempotent_accrual example
# ---

<?php
// Safely retry the same accrual β€” passing Idempotency-Key collapses replays to a single FlowReward

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
        'Idempotency-Key' => 'reward-for-quiz-attempt-4821',
    ],
    'json' => [
        'points' => 25,
    ],
];

$response = $client->request('POST', '/contacts/:contact/rewards', $options);

$result = json_decode($response->getBody());
?>


# ---
# Deduct example
# ---

<?php
// Deduct 30 points

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'points' => -30,
    ],
];

$response = $client->request('POST', '/contacts/:contact/rewards', $options);

$result = json_decode($response->getBody());
?>
<?php
// Accrue 50 points for finishing a milestone, unbound from any product

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/rewards');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'points' => 50,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Accrue_with_product example
# ---

<?php
// Accrue 100 points scoped to product id 10

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/rewards');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'points' => 100,
    'product_id' => 10,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# With_comment example
# ---

<?php
// Accrue 50 points with a curator comment that the student also sees in their journal

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/rewards');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'points' => 50,
    'comment' => 'Bonus for active chat participation',
    'is_visible_to_student' => true,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Idempotent_accrual example
# ---

<?php
// Safely retry the same accrual β€” passing Idempotency-Key collapses replays to a single FlowReward

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
    'Idempotency-Key: reward-for-quiz-attempt-4821',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/rewards');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'points' => 25,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Deduct example
# ---

<?php
// Deduct 30 points

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/contacts/:contact/rewards');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'points' => -30,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/contacts/:contact/rewards' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"points":50}'


# ---
# Accrue_with_product example
# ---

curl --location --request POST 'https://api.kwiga.com/contacts/:contact/rewards' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"points":100,"product_id":10}'


# ---
# With_comment example
# ---

curl --location --request POST 'https://api.kwiga.com/contacts/:contact/rewards' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"points":50,"comment":"Bonus for active chat participation","is_visible_to_student":true}'


# ---
# Idempotent_accrual example
# ---

curl --location --request POST 'https://api.kwiga.com/contacts/:contact/rewards' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--header 'Idempotency-Key: reward-for-quiz-attempt-4821' \
--data-raw '{"points":25}'


# ---
# Deduct example
# ---

curl --location --request POST 'https://api.kwiga.com/contacts/:contact/rewards' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"points":-30}'
// Accrue 50 points for finishing a milestone, unbound from any product
const url = 'https://api.kwiga.com/contacts/:contact/rewards';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'points': 50
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Accrue_with_product example
# ---

// Accrue 100 points scoped to product id 10
const url = 'https://api.kwiga.com/contacts/:contact/rewards';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'points': 100,
    'product_id': 10
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# With_comment example
# ---

// Accrue 50 points with a curator comment that the student also sees in their journal
const url = 'https://api.kwiga.com/contacts/:contact/rewards';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'points': 50,
    'comment': 'Bonus for active chat participation',
    'is_visible_to_student': true
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Idempotent_accrual example
# ---

// Safely retry the same accrual β€” passing Idempotency-Key collapses replays to a single FlowReward
const url = 'https://api.kwiga.com/contacts/:contact/rewards';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
    'Idempotency-Key': 'reward-for-quiz-attempt-4821',
  },
  body: JSON.stringify({
    'points': 25
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Deduct example
# ---

// Deduct 30 points
const url = 'https://api.kwiga.com/contacts/:contact/rewards';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'points': -30
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Accrue 50 points for finishing a milestone, unbound from any product
import requests

url = 'https://api.kwiga.com/contacts/:contact/rewards'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'points': 50,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()


# ---
# Accrue_with_product example
# ---

# Accrue 100 points scoped to product id 10
import requests

url = 'https://api.kwiga.com/contacts/:contact/rewards'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'points': 100,
    'product_id': 10,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()


# ---
# With_comment example
# ---

# Accrue 50 points with a curator comment that the student also sees in their journal
import requests

url = 'https://api.kwiga.com/contacts/:contact/rewards'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'points': 50,
    'comment': 'Bonus for active chat participation',
    'is_visible_to_student': true,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()


# ---
# Idempotent_accrual example
# ---

# Safely retry the same accrual β€” passing Idempotency-Key collapses replays to a single FlowReward
import requests

url = 'https://api.kwiga.com/contacts/:contact/rewards'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
    'Idempotency-Key': 'reward-for-quiz-attempt-4821',
}

data = {
    'points': 25,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()


# ---
# Deduct example
# ---

# Deduct 30 points
import requests

url = 'https://api.kwiga.com/contacts/:contact/rewards'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'points': -30,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/:contact/rewards",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "points": 50
  }
}


# ---
# Accrue_with_product example
# ---

{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/:contact/rewards",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "points": 100,
    "product_id": 10
  }
}


# ---
# With_comment example
# ---

{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/:contact/rewards",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "points": 50,
    "comment": "Bonus for active chat participation",
    "is_visible_to_student": true
  }
}


# ---
# Idempotent_accrual example
# ---

{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/:contact/rewards",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Idempotency-Key": "reward-for-quiz-attempt-4821",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "points": 25
  }
}


# ---
# Deduct example
# ---

{
  "method": "POST",
  "url": "https://api.kwiga.com/contacts/:contact/rewards",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "points": -30
  }
}

Example of response:

{
  "data": {
    "id": 9822,
    "points": 50,
    "accrual_type": {
      "id": "accrued",
      "slug": "Plus",
      "title": "Accrued"
    },
    "reason_type": {
      "id": 6,
      "slug": "Manual",
      "title": "Manual"
    },
    "event": null,
    "event_name": null,
    "eventable_type": null,
    "eventable_id": null,
    "event_comment": null,
    "manual_comment": "Bonus for active chat participation",
    "is_visible_to_student": true,
    "message": "Manual points accrual: Bonus for active chat participation",
    "product": null,
    "creator": {
      "id": 17,
      "name": "Alice Curator",
      "email": "alice@kwiga.com"
    },
    "created_at": "2026-05-25T14:30:00.000000Z"
  }
}

POST https://api.kwiga.com/contacts/:contact/rewards

Adds a manual reward to the contact's points journal. Positive points = accrual, negative = deduction. Optional product_id ties the reward to a specific product (otherwise it is cabinet-wide). Reason type is always Manual; the API user is recorded as the curator who issued it.

URL Parameters

points number required
Points to award. Positive value — accrual, negative — deduction. Always recorded with reason_type=Manual.
Range: -1000 – 1000
product_id integer optional
Optional product id to tie the reward to. When omitted the reward is cabinet-wide and not associated with any product.
comment string optional
Optional curator comment (up to 255 chars). Stored alongside the reward and appended to the human-readable message in the response. The student sees the comment in their points journal only when is_visible_to_student is true.
Max length: 255 characters
is_visible_to_student boolean optional
Whether the student should see the comment in their own points journal. Curators (including this public API) always see the comment; this flag controls visibility on the student side only.
Default: false

Response envelope

Payload of the response.

Products

Cabinet products list

Request example:

<?php
// Get products list

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/products', $options);

$result = json_decode($response->getBody());
?>


# ---
# Paginated example
# ---

<?php
// Get products list with pagination and sorting

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/products?per_page=20&page=1&sort_by=asc', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get products list

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/products');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Paginated example
# ---

<?php
// Get products list with pagination and sorting

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/products?per_page=20&page=1&sort_by=asc');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/products' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# Paginated example
# ---

curl --location --request GET 'https://api.kwiga.com/products?per_page=20&page=1&sort_by=asc' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get products list
const url = 'https://api.kwiga.com/products';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Paginated example
# ---

// Get products list with pagination and sorting
const url = 'https://api.kwiga.com/products?per_page=20&page=1&sort_by=asc';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get products list
import requests

url = 'https://api.kwiga.com/products'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# Paginated example
# ---

# Get products list with pagination and sorting
import requests

url = 'https://api.kwiga.com/products?per_page=20&page=1&sort_by=asc'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/products",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# Paginated example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/products?per_page=20&page=1&sort_by=asc",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": [
    {
      "id": 299,
      "productable_id": 142,
      "productable_type": "course",
      "title": "Sample Course Title",
      "url": "http://test.local/courses/test-slug"
    }
  ],
  "links": {
    "first": "https://api.kwiga.com/products?page=1",
    "last": "https://api.kwiga.com/products?page=5",
    "prev": null,
    "next": "https://api.kwiga.com/products?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 5,
    "path": "https://api.kwiga.com/products",
    "per_page": 15,
    "to": 15,
    "total": 75
  }
}

GET https://api.kwiga.com/products

Cabinet products list. Pagination is available, default is 15.

URL Parameters

sort_by string optional
Possible values: asc, descDefault: desc
per_page integer optional
Number of items per page
Default: 15
page integer optional
Page number
Default: 1

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).

Courses

Cabinet courses list

Request example:

<?php
// Get courses list

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/courses', $options);

$result = json_decode($response->getBody());
?>


# ---
# With_params example
# ---

<?php
// Get courses list with pagination and includes

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/courses?with[]=offers&with[]=description&with[]=program&per_page=15&page=1', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get courses list

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/courses');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# With_params example
# ---

<?php
// Get courses list with pagination and includes

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/courses?with[]=offers&with[]=description&with[]=program&per_page=15&page=1');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/courses' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# With_params example
# ---

curl --location --request GET 'https://api.kwiga.com/courses?with[]=offers&with[]=description&with[]=program&per_page=15&page=1' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get courses list
const url = 'https://api.kwiga.com/courses';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# With_params example
# ---

// Get courses list with pagination and includes
const url = 'https://api.kwiga.com/courses?with[]=offers&with[]=description&with[]=program&per_page=15&page=1';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get courses list
import requests

url = 'https://api.kwiga.com/courses'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# With_params example
# ---

# Get courses list with pagination and includes
import requests

url = 'https://api.kwiga.com/courses?with[]=offers&with[]=description&with[]=program&per_page=15&page=1'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/courses",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# With_params example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/courses?with[]=offers&with[]=description&with[]=program&per_page=15&page=1",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": [
    {
      "id": 226,
      "product_id": 431,
      "type": {
        "id": 1,
        "name": "Course",
        "type": "course"
      },
      "title": "Test Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ ΠΊΠ²ΠΈΠ·ΠΎΠ²",
      "slug": "test-chernovik-kvizov",
      "preview": {
        "id": 3140,
        "uuid": "bdfe1347-574f-4829-a745-09ba2176cc2a",
        "name": "image_13.jpeg",
        "original_name": "image_13.jpeg",
        "url": "https://cdn57098163.ahacdn.me/local/cabinet-1/rcIJuNkWmpCQ/image_13.jpeg",
        "thumbnails": {
          "xs": "https://cdn57098163.ahacdn.me/local/cabinet-1/rcIJuNkWmpCQ/image_13_thumb_150.jpeg",
          "small": "https://cdn57098163.ahacdn.me/local/cabinet-1/rcIJuNkWmpCQ/image_13_thumb_500.jpeg",
          "medium": "https://cdn57098163.ahacdn.me/local/cabinet-1/rcIJuNkWmpCQ/image_13.jpeg",
          "large": "https://cdn57098163.ahacdn.me/local/cabinet-1/rcIJuNkWmpCQ/image_13.jpeg",
          "default": "https://cdn57098163.ahacdn.me/local/cabinet-1/rcIJuNkWmpCQ/image_13.jpeg"
        },
        "extension": "jpeg",
        "type_id": 2,
        "mime_type": "image/jpeg"
      },
      "url": "http://test.local/courses/test-chernovik-kvizov",
      "status": {
        "id": 3,
        "name": "Published"
      }
    }
  ],
  "links": {
    "first": "http://api.kwiga.local/courses?page=1",
    "last": "http://api.kwiga.local/courses?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "links": [
      {
        "url": null,
        "label": "&laquo; Previous",
        "active": false
      },
      {
        "url": "http://api.kwiga.local/courses?page=1",
        "label": "1",
        "active": true
      },
      {
        "url": null,
        "label": "Next &raquo;",
        "active": false
      }
    ],
    "path": "http://api.kwiga.local/courses",
    "per_page": 15,
    "to": 1,
    "total": 1
  }
}

GET https://api.kwiga.com/courses

Cabinet courses list. Pagination is available, default is 15 (max: 15).

URL Parameters

with string[] optional
Array of additional parameters to get along with the course. Possible options: offers (list of all offers with the course), description (info blocks with course description), program (course program tree)
Possible values: offers, description, program
sort_by string optional
Possible values: asc, descDefault: desc
per_page integer optional
Number of items per page
Maximum: 15Default: 15
page integer optional
Page number
Default: 1

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).

Course participants list

Request example:

<?php
// Get course users

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/courses/:course/users', $options);

$result = json_decode($response->getBody());
?>


# ---
# Filtered example
# ---

<?php
// Get course users with filters

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/courses/:course/users?progress_general_from=50&progress_general_to=100&per_page=20', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get course users

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/courses/:course/users');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Filtered example
# ---

<?php
// Get course users with filters

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/courses/:course/users?progress_general_from=50&progress_general_to=100&per_page=20');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/courses/:course/users' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# Filtered example
# ---

curl --location --request GET 'https://api.kwiga.com/courses/:course/users?progress_general_from=50&progress_general_to=100&per_page=20' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get course users
const url = 'https://api.kwiga.com/courses/:course/users';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Filtered example
# ---

// Get course users with filters
const url = 'https://api.kwiga.com/courses/:course/users?progress_general_from=50&progress_general_to=100&per_page=20';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get course users
import requests

url = 'https://api.kwiga.com/courses/:course/users'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# Filtered example
# ---

# Get course users with filters
import requests

url = 'https://api.kwiga.com/courses/:course/users?progress_general_from=50&progress_general_to=100&per_page=20'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/courses/:course/users",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# Filtered example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/courses/:course/users?progress_general_from=50&progress_general_to=100&per_page=20",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
    "data": [
        {
            "user": {
                "id": 3,
                "name": "Admin ΠšΠ°Π±ΠΈΠ½Π΅Ρ‚",
                "email": "test@test.com"
            },
            "contact": {
                "id": 1,
                "user_id": 3,
                "email": "test@test.com",
                "first_name": "test",
                "last_name": "admin cabinet",
                "phone": "979",
                "created_at": "2023-05-26T09:47:20.000000Z",
                "last_activity_at": null
            },
            "course_points": 0,
            "course_progress": {
                "course_id": 20,
                "course_url": "http://test.local/courses/Pm8CEofJ",
                "title": "Dripping",
                "lessons_count": 9,
                "lessons_count_viewed": 0,
                "lessons_viewed_percentage": 0,
                "lessons_count_completed": 0,
                "lessons_completion_percentage": 0,
                "quizzes_count": 0,
                "quizzes_count_completed": 0,
                "quizzes_completion_percentage": 0,
                "scores_max": 0,
                "quizzes_scores": 0,
                "product_scores": 0,
                "scores": 0,
                "is_completed": false,
                "completed_at": null,
                "current_lesson": {
                    "id": 48,
                    "course_id": 20,
                    "type_id": 1,
                    "status_id": 4,
                    "number": 6,
                    "title": "2",
                    "slug": "2",
                    "url": "http://test.local/courses/Pm8CEofJ/2",
                    "quizzes": [],
                    "module": {
                        "id": 7,
                        "course_id": 20,
                        "number": 1,
                        "title": "Module title"
                    }
                },
                "next_lesson": {
                    "id": 46,
                    "course_id": 20,
                    "type_id": 1,
                    "status_id": 4,
                    "number": 1,
                    "title": "3",
                    "slug": "3",
                    "url": "http://test.local/courses/Pm8CEofJ/3",
                    "module": {
                        "id": 7,
                        "course_id": 20,
                        "number": 1,
                        "title": "Module title"
                    }
                },
                "last_activity_at": null,
                "is_checkpoints_skipped": false,
                "checkpoints": [],
                "is_current_dripping_date_skipped": false,
                "is_next_dripping_date_skipped": false,
                "current_dripping_date": null,
                "next_dripping_date": null
            },
            "lessons_available_count": 9,
            "is_full_access": true,
            "subscription": {
                "is_active": true,
                "is_paid": true,
                "start_at": "2024-04-23T13:45:02.000000Z",
                "end_at": null,
                "offer_end_at": null,
                "order_end_at": null,
                "count_available_days": 661,
                "count_left_days": null,
                "state": {
                    "id": 2,
                    "name": "Open",
                    "title": "Open"
                }
            }
        }
    ],
    "links": {
        "first": "http://api.kwiga.local/courses/20/users?page=1",
        "last": "http://api.kwiga.local/courses/20/users?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "links": [
            {
                "url": null,
                "label": "&laquo; Previous",
                "active": false
            },
            {
                "url": "http://api.kwiga.local/courses/20/users?page=1",
                "label": "1",
                "active": true
            },
            {
                "url": null,
                "label": "Next &raquo;",
                "active": false
            }
        ],
        "path": "http://api.kwiga.local/courses/20/users",
        "per_page": 15,
        "to": 1,
        "total": 1
    }
}

GET https://api.kwiga.com/courses/:course/users

Returns a list of course participants and their progress. Pagination is available, default is 15 (max: 250). Replace :course with the course id that can be obtained from the course editing/management URL or from the course list endpoint.

URL Parameters

search string optional
Fuzzy search by email or name
contact_id integer optional
By contact id
contact_ids integer/integer[] optional
Filter by contact ids. Accepts a comma-separated string or a repeated array of integers.
user_id integer optional
By user id
user_ids integer/integer[] optional
Filter by user (student account) ids. Accepts a comma-separated string or a repeated array of integers.
emails string/string[] optional
Filter by exact email address. Accepts a comma-separated string or a repeated array; each value must be a valid email.
progress_general_from integer optional
General progress from (0 to 99)
progress_general_to integer optional
General progress to (0 to 100)
lessons_viewed_from integer optional
Lesson viewing percentage from (0 to 99)
lessons_viewed_to integer optional
Lesson viewing percentage to (0 to 100)
quizzes_progress_from integer optional
Quizzes progress percentage from (0 to 99)
quizzes_progress_to integer optional
Quizzes progress percentage to (0 to 100)
last_activity_from datetime optional
Last activity on course from (UTC date)
last_activity_to datetime optional
Last activity on course to (UTC date)
per_page integer optional
Number of items per page
Maximum: 250Default: 15
page integer optional
Page number
Default: 1
include string/string[] optional

Optional comma-separated list of response extensions. Each token enables one section of the response β€” clients only pay for what they ask for.

  • course_program β€” adds the course program tree at the top level of the response as course_program (array of CourseProgram nodes; lesson nodes contain CourseProgramSection children with attached quizzes). Returned once next to data/links/meta because it is the same for every user on the page.
  • lesson_progress β€” adds lesson_progress inside every data[] row: array of LessonProgress objects (one per lesson the user has progress on).
  • module_progress β€” adds module_progress inside every data[] row: array of ModuleProgress objects.
  • quiz_progress β€” adds quiz_progress inside every data[] row: array of QuizProgress objects, one per (course_lesson_id, quiz_id) pair summarised from the user's latest non-cancelled attempt.

Note: the same quiz can be attached to several lessons of the same course, so quiz_progress is keyed by the lesson/quiz pair, not by quiz_id alone. The client stitches per-user progress to the program tree by course_nodeble_id of lesson/module nodes and by the (lesson.course_nodeble_id, quizzes[i].id) pair for section nodes.

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).
Conditional: Returned when include contains course_program.
Course program tree returned at the top level when `include` contains `course_program`. Same shape as the existing `CourseProgram` schema, with lesson nodes containing `CourseProgramSection` children. Identical for all users on the page, so the API returns it once instead of repeating it per row.

POST alias

POST https://api.kwiga.com/courses/:course/users/query

A functional alias for the GET endpoint above. Accepts the same parameters in the request body instead of the query string β€” useful when the filter list is too large to fit a URL (or when JSON is simply easier to build on the client). Body has priority over query string, response shape is identical.

Receiving lesson-level analytics

In addition to the per-user course summary the endpoint can return a detailed lesson-level progress breakdown. Opt in with the include query parameter (see its description in the URL parameters above for the full list of tokens and the response shape each one enables). The course program is returned once at the top level under course_program β€” it is the same for every user on the page, so this keeps the response size proportional to the number of users multiplied by the number of lessons/quizzes, not by the size of the whole tree.

Response example with ?include=course_program,lesson_progress,module_progress,quiz_progress

{
    "data": [
        {
            "user": {
                "id": 3,
                "name": "Admin ΠšΠ°Π±ΠΈΠ½Π΅Ρ‚",
                "email": "test@test.com"
            },
            "contact": {
                "id": 1,
                "user_id": 3,
                "email": "test@test.com",
                "first_name": "test",
                "last_name": "admin cabinet",
                "phone": "979",
                "created_at": "2023-05-26T09:47:20.000000Z",
                "last_activity_at": null
            },
            "course_points": 0,
            "course_progress": {
                "course_id": 20,
                "title": "Dripping",
                "lessons_count": 9,
                "lessons_count_viewed": 2,
                "lessons_viewed_percentage": 22,
                "lessons_count_completed": 1,
                "lessons_completion_percentage": 11,
                "quizzes_count": 2,
                "quizzes_count_completed": 1,
                "quizzes_completion_percentage": 50,
                "scores_max": 200,
                "quizzes_scores": 80,
                "is_completed": false,
                "completed_at": null
            },
            "lessons_available_count": 9,
            "is_full_access": true,
            "lesson_progress": [
                {
                    "lesson_id": 48,
                    "is_watched": true,
                    "is_completed": true,
                    "quizzing_status": {
                        "id": 4,
                        "slug": "Completed",
                        "title": "Completed"
                    },
                    "watching_start_at": "2026-05-20T11:20:00.000000Z",
                    "watching_end_at": "2026-05-20T11:30:00.000000Z",
                    "quizzing_start_at": "2026-05-20T11:30:00.000000Z",
                    "quizzing_end_at": "2026-05-20T11:34:00.000000Z",
                    "completed_at": "2026-05-20T11:35:00.000000Z",
                    "created_at": "2026-05-20T11:20:00.000000Z",
                    "updated_at": "2026-05-20T11:35:00.000000Z"
                },
                {
                    "lesson_id": 46,
                    "is_watched": true,
                    "is_completed": false,
                    "quizzing_status": {
                        "id": 3,
                        "slug": "InProgress",
                        "title": "In progress"
                    },
                    "watching_start_at": "2026-05-21T09:00:00.000000Z",
                    "watching_end_at": "2026-05-21T09:10:00.000000Z",
                    "quizzing_start_at": null,
                    "quizzing_end_at": null,
                    "completed_at": null,
                    "created_at": "2026-05-21T09:00:00.000000Z",
                    "updated_at": "2026-05-21T09:10:00.000000Z"
                }
            ],
            "module_progress": [
                {
                    "module_id": 7,
                    "is_watched": true,
                    "is_completed": false,
                    "quizzing_status": {
                        "id": 3,
                        "slug": "InProgress",
                        "title": "In progress"
                    },
                    "watching_start_at": "2026-05-20T11:20:00.000000Z",
                    "watching_end_at": "2026-05-21T09:10:00.000000Z",
                    "quizzing_start_at": null,
                    "quizzing_end_at": null,
                    "completed_at": null,
                    "created_at": "2026-05-20T11:20:00.000000Z",
                    "updated_at": "2026-05-21T09:10:00.000000Z"
                }
            ],
            "quiz_progress": [
                {
                    "course_lesson_id": 48,
                    "quiz_id": 101,
                    "status": {
                        "id": 1,
                        "slug": "Passed",
                        "title": "Passed"
                    },
                    "scores": 80,
                    "scores_max": 100,
                    "started_at": "2026-05-20T11:30:00.000000Z",
                    "finished_at": "2026-05-20T11:34:00.000000Z",
                    "checked_at": null,
                    "last_activity_at": "2026-05-20T11:34:00.000000Z",
                    "attempt_number": 2
                }
            ]
        }
    ],
    "links": {
        "first": "https://api.example.com/courses/20/users?page=1",
        "last": "https://api.example.com/courses/20/users?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "path": "https://api.example.com/courses/20/users",
        "per_page": 15,
        "to": 1,
        "total": 1
    },
    "course_structure": [
        {
            "id": 12,
            "course_nodeble_type": "course_module",
            "course_nodeble_id": 7,
            "parent_id": null,
            "order": 1,
            "course_nodeble": {
                "id": 7,
                "title": "Module title"
            },
            "children": [
                {
                    "id": 34,
                    "course_nodeble_type": "course_lesson",
                    "course_nodeble_id": 48,
                    "parent_id": 12,
                    "order": 1,
                    "course_nodeble": {
                        "id": 48,
                        "title": "Lesson 1"
                    },
                    "children": [
                        {
                            "id": 201,
                            "course_nodeble_type": "info_section",
                            "course_nodeble_id": 201,
                            "parent_id": 34,
                            "order": 1,
                            "course_nodeble": {
                                "id": 201,
                                "name": "List of practice",
                                "slug": "list-of-practice"
                            },
                            "quizzes": [
                                {"id": 101, "name": "Module wrap-up quiz"}
                            ],
                            "children": []
                        },
                        {
                            "id": 202,
                            "course_nodeble_type": "info_section",
                            "course_nodeble_id": 202,
                            "parent_id": 34,
                            "order": 2,
                            "course_nodeble": {
                                "id": 202,
                                "name": "Theory",
                                "slug": "theory"
                            },
                            "quizzes": [],
                            "children": []
                        }
                    ]
                },
                {
                    "id": 35,
                    "course_nodeble_type": "course_lesson",
                    "course_nodeble_id": 46,
                    "parent_id": 12,
                    "order": 2,
                    "course_nodeble": {
                        "id": 46,
                        "title": "Lesson 2"
                    },
                    "children": [
                        {
                            "id": 203,
                            "course_nodeble_type": "info_section",
                            "course_nodeble_id": 203,
                            "parent_id": 35,
                            "order": 1,
                            "course_nodeble": {
                                "id": 203,
                                "name": "Intro",
                                "slug": "intro"
                            },
                            "quizzes": [],
                            "children": []
                        }
                    ]
                }
            ]
        }
    ]
}

Stitching progress to the tree

The endpoint returns the course program once at the top level and the per-user progress arrays inside each data[] row. To render a user's progress on top of the program tree, do the following.

1. Index this user's progress arrays for fast lookup.

A note on names: progress entries use lesson_id/module_id/course_lesson_id, while program nodes use course_nodeble_id. These are the same value from different angles β€” see step 2.

2. Walk course_program and pick the right entry per node.

Every node carries course_nodeble_type + course_nodeble_id. Use course_nodeble_id as the lookup key:

If the lookup returns nothing, the user has no progress on that node yet β€” render the empty state. The course program is the same for every user on the page, so it's safe to build it once and reuse across rows.

Quiz attempts

Cabinet quiz attempts list

Request example:

<?php
// List quiz attempts (defaults β€” newest first by status_updated_at)

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/quiz-attempts', $options);

$result = json_decode($response->getBody());
?>


# ---
# Filtered example
# ---

<?php
// Filtered list β€” pending check & need rework, scoped to two products

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/quiz-attempts?filters[practice_statuses]=3,6&filters[product_ids]=10,11&sort_by=last_activity_at&sort_dir=desc&per_page=50', $options);

$result = json_decode($response->getBody());
?>
<?php
// List quiz attempts (defaults β€” newest first by status_updated_at)

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/quiz-attempts');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Filtered example
# ---

<?php
// Filtered list β€” pending check & need rework, scoped to two products

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/quiz-attempts?filters[practice_statuses]=3,6&filters[product_ids]=10,11&sort_by=last_activity_at&sort_dir=desc&per_page=50');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/quiz-attempts' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# Filtered example
# ---

curl --location --request GET 'https://api.kwiga.com/quiz-attempts?filters[practice_statuses]=3,6&filters[product_ids]=10,11&sort_by=last_activity_at&sort_dir=desc&per_page=50' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// List quiz attempts (defaults β€” newest first by status_updated_at)
const url = 'https://api.kwiga.com/quiz-attempts';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Filtered example
# ---

// Filtered list β€” pending check & need rework, scoped to two products
const url = 'https://api.kwiga.com/quiz-attempts?filters[practice_statuses]=3,6&filters[product_ids]=10,11&sort_by=last_activity_at&sort_dir=desc&per_page=50';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# List quiz attempts (defaults β€” newest first by status_updated_at)
import requests

url = 'https://api.kwiga.com/quiz-attempts'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# Filtered example
# ---

# Filtered list β€” pending check & need rework, scoped to two products
import requests

url = 'https://api.kwiga.com/quiz-attempts?filters[practice_statuses]=3,6&filters[product_ids]=10,11&sort_by=last_activity_at&sort_dir=desc&per_page=50'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/quiz-attempts",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# Filtered example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/quiz-attempts?filters[practice_statuses]=3,6&filters[product_ids]=10,11&sort_by=last_activity_at&sort_dir=desc&per_page=50",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": [
    {
      "id": 4821,
      "root_id": 4810,
      "previous_id": 4815,
      "number_version": 3,
      "user_id": 712,
      "product_id": 431,
      "quiz_id": 88,
      "course_id": 226,
      "course_lesson_id": 504,
      "lesson_section": {
        "id": 88,
        "name": "Knowledge check",
        "order": 3,
        "url": "https://lm4.kwiga.com/courses/js-foundations/3/lessons/504",
        "crm_url": "https://lm4.kwiga.com/expert/courses/js-foundations/3/lessons/504/sections/3"
      },
      "status": {
        "id": 6,
        "slug": "PendingCheck",
        "title": "Pending review"
      },
      "scores": 12.5,
      "scores_max": 15,
      "count_questions": 10,
      "count_questions_correct": 8,
      "count_questions_incorrect": 2,
      "is_force_approved": false,
      "is_read": false,
      "started_at": "2026-05-23T11:02:14.000000Z",
      "last_activity_at": "2026-05-23T11:14:51.000000Z",
      "finished_at": "2026-05-23T11:14:51.000000Z",
      "deadline_at": null,
      "status_updated_at": "2026-05-23T11:14:51.000000Z",
      "checked_at": null,
      "commented_at": null,
      "canceled_at": null,
      "passed_time_in_seconds": 757,
      "crm_url": "https://lm4.kwiga.com/expert/practices?attempt_id=4821",
      "user": {
        "id": 712,
        "name": "Alice Student",
        "email": "alice@example.com"
      },
      "quiz": {
        "id": 88,
        "name": "Module 3 final test"
      },
      "course": {
        "id": 226,
        "product_id": 431,
        "type": {
          "id": 1,
          "name": "Course",
          "type": "course"
        },
        "title": "JS Foundations",
        "slug": "js-foundations",
        "url": "http://test.local/courses/js-foundations",
        "status": {
          "id": 3,
          "name": "Published"
        }
      },
      "lesson": {
        "id": 504,
        "title": "Closures & scope",
        "course": {
          "id": 226,
          "product_id": 431,
          "type_id": 1,
          "title": "JS Foundations",
          "url": "http://test.local/courses/js-foundations"
        },
        "module": {
          "id": 30,
          "course_id": 226,
          "number": 3,
          "title": "Functions in depth"
        }
      },
      "product": {
        "id": 431,
        "productable_type": "course",
        "productable_id": 226,
        "name": "JS Foundations",
        "url": "http://test.local/courses/js-foundations"
      }
    }
  ],
  "links": {
    "first": "https://api.kwiga.com/quiz-attempts?page=1",
    "last": "https://api.kwiga.com/quiz-attempts?page=4",
    "prev": null,
    "next": "https://api.kwiga.com/quiz-attempts?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 4,
    "path": "https://api.kwiga.com/quiz-attempts",
    "per_page": 15,
    "to": 15,
    "total": 58,
    "links": [
      { "url": null, "label": "&laquo; Previous", "active": false },
      { "url": "https://api.kwiga.com/quiz-attempts?page=1", "label": "1", "active": true },
      { "url": "https://api.kwiga.com/quiz-attempts?page=2", "label": "2", "active": false },
      { "url": "https://api.kwiga.com/quiz-attempts?page=2", "label": "Next &raquo;", "active": false }
    ]
  },
  "attempts_unread_count": 12
}

GET https://api.kwiga.com/quiz-attempts

Returns a paginated list of student quiz attempts visible to the authenticated curator across the whole cabinet (no product is scoped from the URL β€” pass products if you need to narrow the result). Pagination defaults to 15 items per page, max 250. attempts_unread_count at the top level reflects the unread count for the same filter set, suitable for a "new attempts" badge.

URL Parameters

β–Έ filters object optional
Container for all data filters. Sorting, pagination and result include flags live at the root of the request instead.
search string optional
Fuzzy search by student email or name, quiz title or lesson title.
practice_statuses integer/integer[] optional

Filter by attempt status id. Combine multiple values to widen the result.

  • 1 — Completed
  • 2 — Not completed
  • 3 — Revision required
  • 4 — The latest version changed the status
  • 5 — In progress
  • 6 — Pending review
  • 7 — Not started
  • -1 — Unchecked answers for LMS assignments (filter-only sentinel; never appears in the response)
Possible values: -1, 1, 2, 3, 4, 5, 6, 7
product_ids integer/integer[] optional
Filter by product ids (course products). Limits to attempts on the given products.
quiz_ids integer/integer[] optional
Filter by quiz ids.
lesson_ids integer/integer[] optional
Filter by course lesson ids.
module_ids integer/integer[] optional
Filter by course module ids.
offer_ids integer/integer[] optional
Filter by offer ids the student bought the product through.
curator_ids integer/integer[] optional
Filter by responsible curator user ids.
group_ids integer/integer[] optional
Filter by product access group ids.
emails string/string[] optional
Filter by the attempt students' contact emails. Combined with contact_ids / user_ids into a single audience filter (union).
contact_ids integer/integer[] optional
Filter by the attempt students' cabinet contact ids. Combined with emails / user_ids (union).
user_ids integer/integer[] optional
Filter by the attempt students' user (account) ids. Combined with emails / contact_ids (union).
tag_ids integer/integer[] optional
Filter by contact tag ids of the attempt's student. Mutually exclusive with excluded_tag_ids β€” if both are present, excluded_tag_ids wins.
excluded_tag_ids integer/integer[] optional
Exclude attempts whose student carries any of the listed contact tag ids. Mutually exclusive with tag_ids.
last_activity_from datetime optional
Lower bound (inclusive) for the attempt's last activity timestamp (UTC).
last_activity_to datetime optional
Upper bound (inclusive) for the attempt's last activity timestamp (UTC).
status_updated_from datetime optional
Lower bound (inclusive) for the attempt status transition timestamp (UTC).
status_updated_to datetime optional
Upper bound (inclusive) for the attempt status transition timestamp (UTC).
finished_from datetime optional
Lower bound (inclusive) for the attempt finish timestamp (UTC).
finished_to datetime optional
Upper bound (inclusive) for the attempt finish timestamp (UTC).
curator_last_activity_from datetime optional
Lower bound (inclusive) for the curator's last activity on the attempt (UTC).
curator_last_activity_to datetime optional
Upper bound (inclusive) for the curator's last activity on the attempt (UTC).
only_last_attempt boolean optional
When true, returns only each student's latest non-canceled attempt instead of the full history.
curator_conditions string/string[] optional

Extra constraints on the selected curator_ids (ignored without them). Several values combine per curator_logic.

  • attached_to_quiz — the curator is assigned to the attempt's product
  • changed_status — the curator changed the attempt status
  • commented_on_status — the curator left a comment while changing the status
  • commented_on_assignments — the curator commented on the attempt's assignments
Possible values: attached_to_quiz, changed_status, commented_on_status, commented_on_assignments
curator_logic string optional
How curator_conditions combine β€” any (OR, default) or all (AND). Works only together with curator_ids and curator_conditions.
Example: anyPossible values: any, allDefault: any

per_page integer optional
Items per page.
Maximum: 250Default: 15
page integer optional
Page number.
Default: 1
sort_by string optional
Sort field for the result.
Example: last_activity_atPossible values: last_activity_at, status_updated_at, finished_at, statusDefault: last_activity_at
sort_dir string optional
Sort direction.
Example: descPossible values: asc, descDefault: desc

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).
Number of attempts in the unfiltered cabinet scope that are still unread by curators. Useful for a global "new attempts" badge.

POST alias

POST https://api.kwiga.com/quiz-attempts/query

A functional alias for the GET endpoint above. Accepts the same parameters in the request body instead of the query string β€” useful when the filter list is too large to fit a URL (or when JSON is simply easier to build on the client). Body has priority over query string, response shape is identical.

Offers

Get offers

Request example:

<?php
// Get offers list filtered by product

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/offers?product_id=130', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get offers list filtered by product

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/offers?product_id=130');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/offers?product_id=130' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get offers list filtered by product
const url = 'https://api.kwiga.com/offers?product_id=130';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get offers list filtered by product
import requests

url = 'https://api.kwiga.com/offers?product_id=130'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/offers?product_id=130",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": [
    {
      "id": 273,
      "unique_offer_code": "nELQzbLRPmzo",
      "url": "https://kwiga.com/o/nELQzbLRPmzo",
      "title": "This is paid offer for my course",
      "description": "<p style=\"text-align: left\">Description with html.</p>",
      "short_description": "<p style=\"text-align: left\">Shot description with html.</p>",
      "price_type": {
        "id": 2,
        "name": "ΠŸΠ»Π°Ρ‚Π½ΠΎ"
      },
      "price": {
        "amount": "10.00",
        "amount_rounded": "10.00",
        "amount_formatted": "10.00 usd",
        "amount_formatted_code": "$10.00",
        "amount_formatted_code_short": "$10.00",
        "currency": {
          "id": 5,
          "code": "USD",
          "html_code": "$",
          "html_letter_code": "usd",
          "is_volatile": false
        }
      },
      "price_discounted": {
        "amount": 5,
        "amount_rounded": "5.00",
        "amount_formatted": "5.00 usd",
        "amount_formatted_code": "$5.00",
        "amount_formatted_code_short": "$5.00",
        "currency": {
          "id": 5,
          "code": "USD",
          "html_code": "$",
          "html_letter_code": "usd",
          "is_volatile": false
        }
      },
      "discount": {
        "id": 3,
        "price_discounted": 5,
        "limit_type": {
          "id": 1,
          "name": "БСзстроково"
        },
        "start_at": "2023-12-28T14:41:00.000000Z",
        "start_at_utc": "2023-12-28T12:41:00.000000Z",
        "start_timezone_id": 371,
        "end_at": "2024-01-03T14:41:00.000000Z",
        "end_type": 3,
        "end_at_utc": "2024-01-03T12:41:00.000000Z",
        "end_timezone_id": 371,
        "sales_limit": null,
        "sales": null,
        "is_active": true,
        "is_available": true,
        "created_at": "2023-12-28T12:41:58.000000Z",
        "updated_at": "2023-12-28T12:41:58.000000Z",
        "start_timezone": {
          "id": 371,
          "name": "Europe/Kyiv",
          "name_full": "Europe/Kyiv (UTC+03:00)",
          "value": "UTC+03:00"
        },
        "end_timezone": {
          "id": 371,
          "name": "Europe/Kyiv",
          "name_full": "Europe/Kyiv (UTC+03:00)",
          "value": "UTC+03:00"
        }
      },
      "has_subscription": false,
      "limit_type": {
        "id": 1,
        "name": "НСобмСТСно"
      },
      "limit_of_sales": null,
      "is_active": true,
      "is_draft": false,
      "validity_start": {
        "type": "validity_start",
        "duration_type_id": 1,
        "date_at": "2023-12-28T14:40:00.000000Z",
        "date_at_utc": "2023-12-28T12:40:00.000000Z",
        "timezone_id": 371,
        "timezone": {
          "id": 371,
          "name": "Europe/Kyiv",
          "name_full": "Europe/Kyiv (UTC+03:00)",
          "value": "UTC+03:00"
        },
        "after_months": 0,
        "after_days": 0,
        "specific_time": null
      },
      "validity_end": {
        "type": "validity_end",
        "duration_type_id": 3,
        "date_at": "2023-12-28T14:40:00.000000Z",
        "date_at_utc": "2023-12-28T12:40:00.000000Z",
        "timezone_id": 371,
        "timezone": {
          "id": 371,
          "name": "Europe/Kyiv",
          "name_full": "Europe/Kyiv (UTC+03:00)",
          "value": "UTC+03:00"
        },
        "after_months": 0,
        "after_days": 0,
        "specific_time": null
      },
      "products": [
        {
          "id": 130,
          "productable_id": 38,
          "productable_type": "course",
          "title": "This is my course",
          "url": "https://kwiga.com/courses/test-fail"
        }
      ]
    },
    {
      "id": 272,
      "unique_offer_code": "2obtMsUEujcy",
      "url": "https://kwiga.com/o/2obtMsUEujcy",
      "title": "This is free offer for my course",
      "description": null,
      "short_description": null,
      "price_type": {
        "id": 1,
        "name": "Π‘Π΅Π·ΠΊΠΎΡˆΡ‚ΠΎΠ²Π½ΠΎ"
      },
      "price": {
        "amount": "0.00",
        "amount_rounded": "0.00",
        "amount_formatted": "0.00 usd",
        "amount_formatted_code": "$0.00",
        "amount_formatted_code_short": "$0.00",
        "currency": {
          "id": 5,
          "code": "USD",
          "html_code": "$",
          "html_letter_code": "usd",
          "is_volatile": false
        }
      },
      "price_discounted": {
        "amount": 0,
        "amount_rounded": "0.00",
        "amount_formatted": "0.00 usd",
        "amount_formatted_code": "$0.00",
        "amount_formatted_code_short": "$0.00",
        "currency": {
          "id": 5,
          "code": "USD",
          "html_code": "$",
          "html_letter_code": "usd",
          "is_volatile": false
        }
      },
      "discount": null,
      "has_subscription": false,
      "limit_type": {
        "id": 1,
        "name": "НСобмСТСно"
      },
      "limit_of_sales": null,
      "is_active": true,
      "is_draft": false,
      "validity_start": {
        "type": "validity_start",
        "duration_type_id": 1,
        "date_at": "2023-12-27T12:45:00.000000Z",
        "date_at_utc": "2023-12-27T10:45:00.000000Z",
        "timezone_id": 371,
        "timezone": {
          "id": 371,
          "name": "Europe/Kyiv",
          "name_full": "Europe/Kyiv (UTC+03:00)",
          "value": "UTC+03:00"
        },
        "after_months": 0,
        "after_days": 0,
        "specific_time": null
      },
      "validity_end": {
        "type": "validity_end",
        "duration_type_id": 3,
        "date_at": "2023-12-27T12:45:00.000000Z",
        "date_at_utc": "2023-12-27T10:45:00.000000Z",
        "timezone_id": 371,
        "timezone": {
          "id": 371,
          "name": "Europe/Kyiv",
          "name_full": "Europe/Kyiv (UTC+03:00)",
          "value": "UTC+03:00"
        },
        "after_months": 0,
        "after_days": 0,
        "specific_time": null
      },
      "products": [
        {
          "id": 130,
          "productable_id": 38,
          "productable_type": "course",
          "title": "This is my course",
          "url": "https://kwiga.com/courses/test-fail"
        }
      ]
    }
  ],
  "links": {
    "first": "http://api.kwiga.local/offers?page=1",
    "last": "http://api.kwiga.local/offers?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "links": [
      {
        "url": null,
        "label": "&laquo; Previous",
        "active": false
      },
      {
        "url": "http://api.kwiga.local/offers?page=1",
        "label": "1",
        "active": true
      },
      {
        "url": null,
        "label": "Next &raquo;",
        "active": false
      }
    ],
    "path": "http://api.kwiga.local/offers",
    "per_page": 15,
    "to": 1,
    "total": 2
  }
}

GET https://api.kwiga.com/offers

URL Parameters

page integer optional
Page Number
per_page integer optional
Number of fetch items
product_id integer optional
Filter by product
β–Έ filters object optional
Filter parameters
search string optional
Search

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).

Get offer

Request example:

<?php
// Get offer by ID

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/offers/:offer', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get offer by ID

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/offers/:offer');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/offers/:offer' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get offer by ID
const url = 'https://api.kwiga.com/offers/:offer';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get offer by ID
import requests

url = 'https://api.kwiga.com/offers/:offer'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/offers/:offer",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": {
    "id": 1,
    "unique_offer_code": "6rT1wj99lbZV",
    "title": "Offer #1",
    "limit_type": {
      "id": 2,
      "name": "Certain amount"
    },
    "limit_of_sales": 20,
    "purchases_count": 1,
    "sales_left": 19
  }
}

GET https://api.kwiga.com/offers/:offer

Response envelope

Marketing. Mailing

Contact List List

Request example:

<?php
// Get contact lists

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/mailing/contact-lists', $options);

$result = json_decode($response->getBody());
?>


# ---
# Paginated example
# ---

<?php
// Get contact lists with pagination

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/mailing/contact-lists?page=1&per_page=15', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get contact lists

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/mailing/contact-lists');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Paginated example
# ---

<?php
// Get contact lists with pagination

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/mailing/contact-lists?page=1&per_page=15');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/mailing/contact-lists' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# Paginated example
# ---

curl --location --request GET 'https://api.kwiga.com/mailing/contact-lists?page=1&per_page=15' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get contact lists
const url = 'https://api.kwiga.com/mailing/contact-lists';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Paginated example
# ---

// Get contact lists with pagination
const url = 'https://api.kwiga.com/mailing/contact-lists?page=1&per_page=15';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get contact lists
import requests

url = 'https://api.kwiga.com/mailing/contact-lists'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# Paginated example
# ---

# Get contact lists with pagination
import requests

url = 'https://api.kwiga.com/mailing/contact-lists?page=1&per_page=15'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/mailing/contact-lists",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# Paginated example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/mailing/contact-lists?page=1&per_page=15",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": [
    {
        "id": 2,
        "title": "ΠŸΡ€ΠΈΠΌΠ΅Ρ€ 2",
        "description": "ОписаниС",
        "is_default": false,
        "created_at": "2022-04-28T13:22:44.000000Z",
        "updated_at": "2022-04-28T13:22:44.000000Z"
    },
    {
        "id": 1,
        "title": "Мой ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ список",
        "description": "Π”Π°Π½Π½Ρ‹ΠΉ список создаётся автоматичСски с вашим ΠΊΠΎΠ½Ρ‚Π°ΠΊΡ‚ΠΎΠΌ Π²Π½ΡƒΡ‚Ρ€ΠΈ. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π΅Π³ΠΎ ΠΎΡ‚Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠΎΠ΄ свои потрСбности.",
        "is_default": true,
        "created_at": "2022-04-28T12:47:08.000000Z",
        "updated_at": "2022-04-28T12:47:08.000000Z"
    }
  ],
  "links": {
        "first": "https://api.kwiga.com/mailing/contact-lists?page=1",
        "last": "https://api.kwiga.com/mailing/contact-lists?page=2",
        "prev": null,
        "next": "https://api.kwiga.com/mailing/contact-lists?page=2"
   },
   "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 2,
        "links": [
            {
                "url": null,
                "label": "&laquo; translation missing: en.pagination_prev",
                "active": false
            },
            {
                "url": "https://api.kwiga.com/mailing/contact-lists?page=1",
                "label": "1",
                "active": true
            },
            {
                "url": "https://api.kwiga.com/mailing/contact-lists?page=2",
                "label": "2",
                "active": false
            },
            {
                "url": "https://api.kwiga.com/mailing/contact-lists?page=2",
                "label": "translation missing: en.pagination_next &raquo;",
                "active": false
            }
        ],
        "path": "https://api.kwiga.com/mailing/contact-lists",
        "per_page": 2,
        "to": 2,
        "total": 3
    }
}

GET https://api.kwiga.com/mailing/contact-lists

URL Parameters

page integer optional
Page Number
limit integer optional
Number of fetch items

Response envelope

Items on the current page.

Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).

Getting a contact list

Request example:

<?php
// Get contact list by ID

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/mailing/contact-lists/:list', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get contact list by ID

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/mailing/contact-lists/:list');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/mailing/contact-lists/:list' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get contact list by ID
const url = 'https://api.kwiga.com/mailing/contact-lists/:list';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get contact list by ID
import requests

url = 'https://api.kwiga.com/mailing/contact-lists/:list'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/mailing/contact-lists/:list",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
  "data": {
    "id": 1,
    "title": "Мой ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ список",
    "description": "Π”Π°Π½Π½Ρ‹ΠΉ список создаётся автоматичСски с вашим ΠΊΠΎΠ½Ρ‚Π°ΠΊΡ‚ΠΎΠΌ Π²Π½ΡƒΡ‚Ρ€ΠΈ. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π΅Π³ΠΎ ΠΎΡ‚Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠΎΠ΄ свои потрСбности.",
    "is_default": true,
    "created_at": "2022-04-28T12:47:08.000000Z",
    "updated_at": "2022-04-28T12:47:08.000000Z",
    "contacts": [
        {
            "id": 1,
            "first_name": "Alex",
            "middle_name": null,
            "last_name": "Π˜Π²Π°Π½ΠΎΠ²ΠΈΡ‡ Burt",
            "name": "Π˜Π²Π°Π½ΠΎΠ²ΠΈΡ‡ Burt Alex",
            "sex": null,
            "age": null,
            "email": "admin@grandstep.com.ua",
            "phone_country": "UA",
            "phone_number": "983721222",
            "city": null,
            "phone": "+380983721222",
            "created_at": "2022-04-28T12:47:07.000000Z",
            "updated_at": "2022-04-28T12:47:08.000000Z"
        }
    ],
    "statistic": {
        "contact_list_id": 1,
        "count_contacts": 1
    }
}
}

GET https://api.kwiga.com/mailing/contact-lists/:list

Response envelope

Creating a contact list

Request example:

<?php
// Create contact list

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'title' => 'НазваниС Π³Ρ€ΡƒΠΏΠΏΡ‹',
        'description' => 'описаниС',
    ],
];

$response = $client->request('POST', '/mailing/contact-lists/', $options);

$result = json_decode($response->getBody());
?>
<?php
// Create contact list

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/mailing/contact-lists/');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'title' => 'НазваниС Π³Ρ€ΡƒΠΏΠΏΡ‹',
    'description' => 'описаниС',
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/mailing/contact-lists/' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"title":"НазваниС Π³Ρ€ΡƒΠΏΠΏΡ‹","description":"описаниС"}'
// Create contact list
const url = 'https://api.kwiga.com/mailing/contact-lists/';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'title': 'НазваниС Π³Ρ€ΡƒΠΏΠΏΡ‹',
    'description': 'описаниС'
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Create contact list
import requests

url = 'https://api.kwiga.com/mailing/contact-lists/'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'title': 'НазваниС Π³Ρ€ΡƒΠΏΠΏΡ‹',
    'description': 'описаниС',
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/mailing/contact-lists/",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "title": "НазваниС Π³Ρ€ΡƒΠΏΠΏΡ‹",
    "description": "описаниС"
  }
}

Example of response:

{
    "data": {
        "id": 3,
        "title": "fsdaf asfsf dsafsda fsda",
        "description": "1dsad sadsa ds dsa",
        "is_default": false,
        "created_at": "2022-05-04T09:57:37.000000Z",
        "updated_at": "2022-05-04T09:57:37.000000Z"
    }
}

POST https://api.kwiga.com/mailing/contact-lists

Request

title string required
Title
description string optional
Description

Response envelope

Adding Contacts to a List

Request example:

<?php
// Bulk add contacts to lists

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'contacts[]' => 1,
        'contacts[]' => 2,
        'contact_lists[]' => 1,
    ],
];

$response = $client->request('POST', '/mailing/contact-lists/bulk-contacts', $options);

$result = json_decode($response->getBody());
?>
<?php
// Bulk add contacts to lists

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/mailing/contact-lists/bulk-contacts');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'contacts[]' => 1,
    'contacts[]' => 2,
    'contact_lists[]' => 1,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/mailing/contact-lists/bulk-contacts' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"contacts[]":2,"contact_lists[]":1}'
// Bulk add contacts to lists
const url = 'https://api.kwiga.com/mailing/contact-lists/bulk-contacts';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'contacts[]': 1,
    'contacts[]': 2,
    'contact_lists[]': 1
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Bulk add contacts to lists
import requests

url = 'https://api.kwiga.com/mailing/contact-lists/bulk-contacts'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'contacts[]': 1,
    'contacts[]': 2,
    'contact_lists[]': 1,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/mailing/contact-lists/bulk-contacts",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "contacts[]": 2,
    "contact_lists[]": 1
  }
}

Example of response:

{
    "success": true
}

POST https://api.kwiga.com/mailing/contact-lists/bulk-contacts

URL Parameters

contacts integer[] required
Contacts (ID)
contact_lists integer[] required
Contact Lists (ID)

Response envelope

Response body is returned without any wrapping.

Payments. Coupons

Coupon list

Request example:

<?php
// Get coupons list

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/coupons', $options);

$result = json_decode($response->getBody());
?>


# ---
# Filtered example
# ---

<?php
// Get coupons list with date filters

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/coupons?filters[date_from]=2022-04-15&filters[date_to]=2022-04-27', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get coupons list

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/coupons');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>


# ---
# Filtered example
# ---

<?php
// Get coupons list with date filters

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/coupons?filters[date_from]=2022-04-15&filters[date_to]=2022-04-27');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/coupons' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'


# ---
# Filtered example
# ---

curl --location --request GET 'https://api.kwiga.com/coupons?filters[date_from]=2022-04-15&filters[date_to]=2022-04-27' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get coupons list
const url = 'https://api.kwiga.com/coupons';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });


# ---
# Filtered example
# ---

// Get coupons list with date filters
const url = 'https://api.kwiga.com/coupons?filters[date_from]=2022-04-15&filters[date_to]=2022-04-27';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get coupons list
import requests

url = 'https://api.kwiga.com/coupons'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()


# ---
# Filtered example
# ---

# Get coupons list with date filters
import requests

url = 'https://api.kwiga.com/coupons?filters[date_from]=2022-04-15&filters[date_to]=2022-04-27'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/coupons",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}


# ---
# Filtered example
# ---

{
  "method": "GET",
  "url": "https://api.kwiga.com/coupons?filters[date_from]=2022-04-15&filters[date_to]=2022-04-27",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
    "data": [
        {
            "id": 3,
            "code": "KwigaSMVT-MD",
            "reward": 30,
            "is_fixed": true,
            "is_disposable": false,
            "used_amount": 0,
            "total_uses": 10,
            "total_uses_per_user": 1,
            "user": {
                "id": 3,
                "avatar_url": "https://someurl.com",
                "hash": "EL9p2ycy4Ypga715",
                "name": "Test User",
                "email": "test@example.com",
                "tag_name": "TestUser"
            },
            "created_at": "2022-04-28T09:46:11.000000Z",
            "updated_at": "2022-04-28T09:46:11.000000Z",
            "deleted_at": null
        }
    ]
}

GET https://api.kwiga.com/coupons

URL Parameters

β–Έ filters object optional
Filter parameters
date_from datetime optional
Filter by creation date. Parameter 'from'
date_to datetime optional
Filter by creation date. 'To' parameter

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).

Getting a coupon

Request example:

<?php
// Get coupon by ID

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/coupons/:coupon', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get coupon by ID

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/coupons/:coupon');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/coupons/:coupon' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get coupon by ID
const url = 'https://api.kwiga.com/coupons/:coupon';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get coupon by ID
import requests

url = 'https://api.kwiga.com/coupons/:coupon'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/coupons/:coupon",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
    "data": {
        "id": 1,
        "code": "KwigaSMVT-MD",
        "reward": 30,
        "is_fixed": true,
        "is_disposable": false,
        "used_amount": 0,
        "total_uses": 10,
        "total_uses_per_user": 1,
        "created_at": "2022-04-28T09:46:11.000000Z",
        "updated_at": "2022-04-28T09:46:11.000000Z",
        "deleted_at": null
    }
}

GET https://api.kwiga.com/coupons/:coupon

Response envelope

Create coupon

Request example:

<?php
// Create coupon

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'code' => 'MY-COUPON',
        'reward' => 0,
        'type_discount[id]' => 'discount_with_percent',
        'expires_at' => ,
        'timezone[id]' => 375,
        'total_uses' => 0,
        'total_uses_per_user' => 0,
    ],
];

$response = $client->request('POST', '/coupons', $options);

$result = json_decode($response->getBody());
?>
<?php
// Create coupon

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/coupons');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'code' => 'MY-COUPON',
    'reward' => 0,
    'type_discount[id]' => 'discount_with_percent',
    'expires_at' => ,
    'timezone[id]' => 375,
    'total_uses' => 0,
    'total_uses_per_user' => 0,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/coupons' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"code":"MY-COUPON","reward":0,"type_discount[id]":"discount_with_percent","expires_at":null,"timezone[id]":375,"total_uses":0,"total_uses_per_user":0}'
// Create coupon
const url = 'https://api.kwiga.com/coupons';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'code': 'MY-COUPON',
    'reward': 0,
    'type_discount[id]': 'discount_with_percent',
    'expires_at': ,
    'timezone[id]': 375,
    'total_uses': 0,
    'total_uses_per_user': 0
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Create coupon
import requests

url = 'https://api.kwiga.com/coupons'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'code': 'MY-COUPON',
    'reward': 0,
    'type_discount[id]': 'discount_with_percent',
    'expires_at': ,
    'timezone[id]': 375,
    'total_uses': 0,
    'total_uses_per_user': 0,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/coupons",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "code": "MY-COUPON",
    "reward": 0,
    "type_discount[id]": "discount_with_percent",
    "expires_at": null,
    "timezone[id]": 375,
    "total_uses": 0,
    "total_uses_per_user": 0
  }
}

Example of response:

{
    "data": {
        "id": 8,
        "code": "MY-COUPON",
        "reward": 100,
        "type_discount": {
            "id": "discount_with_percent",
            "name": "In % of order/offer value"
        },
        "type_date_expired": {
            "id": "indefinite_action",
            "name": "Never expires"
        },
        "type_total_count_used": {
            "id": "certain_amount",
            "name": "A certain amount"
        },
        "type_used_contact_lists": {
            "id": "all_users",
            "name": "All users"
        },
        "is_fixed": false,
        "is_active": true,
        "used_amount": 0,
        "has_infinite_used": true,
        "has_access_contact_lists": false,
        "total_uses": 0,
        "total_uses_per_user": 1,
        "contact_lists": [],
        "type": {
            "id": 4,
            "title": "Offers"
        },
        "expires_at": null,
        "expires_at_utc": null,
        "timezone_id": null,
        "currency_id": null,
        "currency": null,
        "created_at": "2024-01-09T16:17:55.000000Z",
        "updated_at": "2024-01-09T16:17:55.000000Z",
        "deleted_at": null
    }
}

POST https://api.kwiga.com/coupons

URL Parameters

code string optional
Coupon code. If you do not specify, the code is generated automatically
type_discount.id string required
Discount type: percentage of the amount - `discount_with_percent` or fixed discount - `discount_with_currency`
Example: discount_with_percent or discount_with_currency
expires_at datetime optional
Coupon validity period. If null or not specified, then indefinitely
Example: 2024-12-31 or 2024-12-31 23:59:59
timezone.id integer optional
Time zone of coupon expiration date
total_uses integer optional
Limit on the number of times a coupon can be used. 0 - unlimited
total_uses_per_user integer optional
Limit on the number of times a coupon can be used by one user

Response envelope

Check coupon

Request example:

<?php
// Check coupon validity

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
    'json' => [
        'code' => 'VAJ6-EA',
        'price' => 65,
    ],
];

$response = $client->request('POST', '/coupons/check', $options);

$result = json_decode($response->getBody());
?>
<?php
// Check coupon validity

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/coupons/check');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');

$data = [
    'code' => 'VAJ6-EA',
    'price' => 65,
];

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request POST 'https://api.kwiga.com/coupons/check' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>' \
--data-raw '{"code":"VAJ6-EA","price":65}'
// Check coupon validity
const url = 'https://api.kwiga.com/coupons/check';

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
  body: JSON.stringify({
    'code': 'VAJ6-EA',
    'price': 65
  })
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Check coupon validity
import requests

url = 'https://api.kwiga.com/coupons/check'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

data = {
    'code': 'VAJ6-EA',
    'price': 65,
}

response = requests.post(url, headers=headers, json=data)

result = response.json()
{
  "method": "POST",
  "url": "https://api.kwiga.com/coupons/check",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json",
    "Content-Type": "application/json"
  },
  "body": {
    "code": "VAJ6-EA",
    "price": 65
  }
}

Example of response:

{
  "data": {
    "can_use": true,
    "discount": 6.5
  }
}

POST https://api.kwiga.com/coupons/check

Checks coupon for existence and usability (limit not exhausted / not expired).
Note: when used on checkout page, the coupon may not be applied under the following conditions: if the use of coupons/this coupon is disabled on the offer; there are restrictions on the coupon by contact lists; the user has already used the coupon and there is a limit on the number of uses on the coupon.

Parameters

code string required
Coupon code to check
price number optional
Price for discount calculation. If specified, the discount value will be returned in the discount field in the response

Response envelope

Certificates

Receiving a certificate by number

Request example:

<?php
// Get certificate by number

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/certificates/by-number/:number', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get certificate by number

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/certificates/by-number/:number');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/certificates/by-number/:number' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get certificate by number
const url = 'https://api.kwiga.com/certificates/by-number/:number';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get certificate by number
import requests

url = 'https://api.kwiga.com/certificates/by-number/:number'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/certificates/by-number/:number",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
    "data": {
        "id": 55,
        "uuid": "262e7042-1933-42a9-ba01-e96d7d5110dc",
        "cabinet_id": 1,
        "user_id": 273,
        "issued_at": "2023-07-25T15:36:44.000000Z",
        "number": "1986-0001",
        "url": "http://cabinet-1.kwiga.local/certificates/262e7042-1933-42a9-ba01-e96d7d5110dc",
        "created_at": "2023-07-25T15:36:44.000000Z",
        "updated_at": "2023-07-25T15:36:44.000000Z",
        "finished_at": "2023-07-25T15:36:44.000000Z",
        "points": 120,
        "user": {
            "id": 273,
            "name": "test name",
            "email": "test-student@kwiga.com"
        },
        "certificateble_title": "Test course"
    }
}

GET https://api.kwiga.com/certificates/by-number/:number

Response envelope

Timezones

Getting a list of time zones

Request example:

<?php
// Get timezones list

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.kwiga.com']);

$options = [
    'headers' => [
        'Accept' => 'application/json',
        'Token' => '<Token>',
        'Cabinet-Hash' => '<Cabinet-Hash>',
    ],
];

$response = $client->request('GET', '/timezones', $options);

$result = json_decode($response->getBody());
?>
<?php
// Get timezones list

$headers = [
    'Accept: application/json',
    'Token: <Token>',
    'Cabinet-Hash: <Cabinet-Hash>',
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.kwiga.com/timezones');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response);
?>
curl --location --request GET 'https://api.kwiga.com/timezones' \
--header 'Content-Type: application/json' \
--header 'Token: <Token>' \
--header 'Cabinet-Hash: <Cabinet-Hash>'
// Get timezones list
const url = 'https://api.kwiga.com/timezones';

const options = {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
  },
};

fetch(url, options)
  .then(response => response.json())
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
# Get timezones list
import requests

url = 'https://api.kwiga.com/timezones'

headers = {
    'Accept': 'application/json',
    'Token': '<Token>',
    'Cabinet-Hash': '<Cabinet-Hash>',
}

response = requests.get(url, headers=headers)

result = response.json()
{
  "method": "GET",
  "url": "https://api.kwiga.com/timezones",
  "headers": {
    "Token": "<Token>",
    "Cabinet-Hash": "<Cabinet-Hash>",
    "Accept": "application/json"
  }
}

Example of response:

{
    "data": [
        {
            "id": 1,
            "name": "Asia/Kabul",
            "name_full": "Asia/Kabul (UTC+04:30)",
            "value": "UTC+04:30"
        },
        {
            "id": 2,
            "name": "Europe/Tirane",
            "name_full": "Europe/Tirane (UTC+02:00)",
            "value": "UTC+02:00"
        },
        ...
    ]
}

GET https://api.kwiga.com/timezones

Response envelope

Items on the current page.
Pagination links to other pages of the same listing.
Pagination state for the current request (page number, total count, etc.).

Schemas

Reusable response objects referenced across endpoints. Fields marked as conditional only appear in the response under the documented circumstance (a query flag, an optional include, or a specific endpoint).

Contacts

Contact

id integer
Unique contact identifier
user_id integer nullable
Underlying user identifier (null when the contact has no associated user account yet)
email string
Contact email address
first_name string nullable
middle_name string nullable
last_name string nullable
name string nullable
Display name assembled from first/middle/last names in the cabinet's preferred order.
phone string nullable
crm_url string nullable
Direct URL to the contact's profile page in your cabinet's CRM.
created_at datetime
Timestamp when the contact was created
tags FlowTag[] conditional
Conditional: Included when tags are loaded for the contact.
Tags attached to the contact
last_activity_at datetime conditional
Conditional: Included when the contact has activity history in the cabinet.
Timestamp of the contact's last activity within the cabinet
first_visit Visit conditional
Conditional: Included when the contact has at least one recorded visit.
First recorded visit of the contact
utm UtmList conditional
Conditional: Included when UTM data has been collected for the contact.
Aggregated UTM attribution across the contact's visits
utm_visits Visit[] conditional
Conditional: Returned only on `GET /contacts/{id}` β€” not present on the contacts list.
All UTM-tracked visits of the contact
offers Offer[] conditional
Conditional: Included when `with_orders=true` is passed.
Offers the contact has purchased
orders Order[] conditional
Conditional: Included when `with_orders=true` is passed.
Orders placed by the contact
Conditional: Included when the contact has custom field values configured.
Cabinet-specific custom fields filled in for this contact
Conditional: Included when `with_certificates=true` is passed.
Certificate issuance details per product the contact owns

ContactIdentifier

id integer
user_id integer nullable
first_name string nullable
last_name string nullable
name string nullable
Composed full name (first + last) when set
email string nullable
Contact email (may be masked for non-owner viewers)
phone string nullable
Contact phone (may be masked for non-owner viewers)

ContactAdditionalField

field ContactField conditional
Conditional: Included when the field definition is part of the response.
Definition of the cabinet-specific custom field
value string nullable
Raw value stored for this field on the contact
display_value string nullable
Human-readable rendering of the value (e.g. localized enum label)

ContactField

id integer
title string
Localized field title
is_local boolean
Whether the field is cabinet-local (not part of the global schema)
input_title string conditional
Conditional: Returned only for viewers with extended access to the field.
format string conditional
Conditional: Returned only for viewers with extended access to the field.
Field input format token
validation string conditional
Conditional: Returned only for viewers with extended access to the field.
Validation rule expression
default_value string conditional
Conditional: Returned only for viewers with extended access to the field.
placeholder string conditional
Conditional: Returned only for viewers with extended access to the field.
type string conditional
Conditional: Returned only for viewers with extended access to the field.
Field type identifier (text, number, date, enum, etc.)
is_default boolean conditional
Conditional: Returned only for viewers with extended access to the field.
is_required boolean conditional
Conditional: Returned only for viewers with extended access to the field.
is_at_import boolean conditional
Conditional: Returned only for viewers with extended access to the field.
visible_type Enum conditional
Conditional: Returned only for viewers with extended access to the field.
order integer conditional
Conditional: Returned only for viewers with extended access to the field.
Display order within the cabinet
values object[] conditional
Conditional: Returned for viewers with extended access when value options are configured.
Allowed values for enum-style fields
name_order_type object conditional
Conditional: Returned for viewers with extended access when the field is the default full-name field.
Cabinet-specific ordering for first/last name display

ContactList

id integer
cabinet_id integer conditional
Conditional: Included when the cabinet relation is part of the response.
Owning cabinet identifier
user_id integer conditional
Conditional: Included when the owning user is part of the response.
Owning user identifier
title string
description string nullable
is_default boolean
True for the cabinet's built-in default list
is_active boolean
is_segment boolean
True when the list is a dynamic segment driven by filters
is_new boolean
Marker for lists created after 2023-08-15 (used by the UI)
paused_at datetime nullable
created_at datetime
updated_at datetime
contacts_count integer conditional
Conditional: Included when contact counts are available for the list.
Conditional: Returned on the single-list endpoint; not present on the list response.
Conditional: Included when list statistics are part of the response.
Conditional: Included when the list's email campaigns are part of the response.
Email campaigns associated with the list
segment_settings object conditional
Conditional: Present for dynamic segments; empty object for regular lists.
Filter rules driving the segment membership

ContactListStatistic

contact_list_id integer
count_contacts integer
Number of contacts currently in the list

FlowTag

id integer
name string
contacts_count integer conditional
Conditional: Included when contact counts are available for the tag.
Number of contacts the tag is attached to

Courses

Course

id integer
product_id integer
Linked product identifier (use for /contacts/{contact}/products lookup)
type string
Course type identifier (course / marathon / etc.)
title string
Course title (falls back to `Course
slug string nullable
URL-friendly slug
preview FileSimple conditional
Conditional: Included when the course has a preview image.
url string
Public link to the course landing page
status string
Course publish status identifier (`draft`, `published`, etc.)
offers Offer[] conditional
Conditional: Included when the course offers are part of the response.
info_units InfoUnit[] conditional
Conditional: Included when course info units are part of the response.
Conditional: Included when the course program is part of the response (rendered as a tree).

CourseIdentify

id integer
product_id integer
Linked product identifier
type_id integer
Course type identifier (course / marathon / etc.)
title string
Conditional: Included when the course's lesson identity payloads are loaded.
Compact identity entries for the course's lessons
url string
Public URL to the course landing page
preview_url string conditional
Conditional: Included when the course preview is loaded.
URL to the course preview image (or default placeholder when missing)
Conditional: Included when offer identity payloads tied to the course are loaded.
Compact identity entries for offers linked to the course

CourseLessonIdentify

id integer
title string
course CourseIdentify conditional
Conditional: Included when the parent course identity is part of the response.
Conditional: Included when the lesson's module branch is part of the response. Null for top-level lessons.

CourseLessonSimple

id integer
course_id integer
type_id integer
Lesson type identifier
status_id integer
Lesson publish status identifier
number string nullable
User-visible lesson number (chapter ordering token)
title string
slug string nullable
url string
Public URL to the lesson
Conditional: Included when lesson quizzes are part of the response.
Quizzes attached to the lesson
course CourseIdentify conditional
Conditional: Included when the parent course summary is part of the response.
Identity payload of the parent course
Parent module summary; null for top-level lessons

CourseModuleSimple

id integer
course_id integer
number string nullable
User-visible module number / ordering token
title string

CourseProgram

id integer
order integer
Position among siblings
Polymorphic type of the underlying entity (lesson, module, etc.)
course_id integer
title string
Title of the underlying entity
preview FileSimple conditional
Conditional: Included when the node has a preview image attached.
Nested child nodes (empty array for leaves)
root_id integer nullable
Identifier of the tree root
parent_id integer nullable
is_public boolean
url string nullable
Public link to the node (if applicable)

CourseProgress

course_id integer
course_url string
Public URL to the course landing page
title string
Course title
lessons_count integer
Total number of lessons currently active in the course
Number of lessons the user has opened at least once
Percentage of lessons the user has viewed (0 – 100)
Number of lessons the user has finished
Percentage of lessons the user has finished (0 – 100)
Display-friendly completion percentage (clamped/rounded for UI)
quizzes_count integer
Total number of quizzes in the course
Number of quizzes the user has completed
Percentage of quizzes the user has completed (0 – 100)
scores_max number
Maximum total score the user could earn in the course
Total score earned by the user across all quizzes
Total product-level score earned by the user (outside quizzes)
scores number
Combined score the user has earned across the course
is_completed boolean
True when the user has fully completed the course
completed_at datetime nullable
Timestamp of full course completion; null while in progress
Lesson the user is currently positioned on (null if none)
Next lesson available to the user (null when at the end)
last_activity_at datetime nullable
Timestamp of the user's most recent activity in the course
True when the user has skipped through gating checkpoints
Checkpoint quizzes the user has reached
True when the user bypassed the current drip-content gate
True when the user bypassed the upcoming drip-content gate
current_dripping_date datetime nullable
When the current drip-content gate unlocks
next_dripping_date datetime nullable
When the next drip-content gate unlocks

CourseUser

The student account participating in the course β€” identifier, name and email. Stable across requests.
Linked CRM contact record for this student.
course_points integer
Total points the user has earned in the course
Lesson/module completion summary
Number of lessons the user currently has access to
is_full_access boolean
True when the user has access to every active lesson in the course
Conditional: Included when the user has an aggregated subscription for the course.
Conditional: Included when include contains lesson_progress.
Array of `LessonProgress` objects β€” per-lesson watched/completed snapshots for this user. Returned when `include` contains `lesson_progress`.
Conditional: Included when include contains module_progress.
Array of `ModuleProgress` objects β€” per-module watched/completed snapshots for this user (aggregated server-side from the module's lessons). Returned when `include` contains `module_progress`.
Conditional: Included when include contains quiz_progress.
Array of `QuizProgress` objects β€” one entry per `(course_lesson_id, quiz_id)` pair summarised from the user's latest non-cancelled attempt. Returned when `include` contains `quiz_progress`.

InfoUnit

id integer
title string
description_formatted string nullable
Rich-text description with links processed for display
True if the description contains links pointing outside the cabinet
position integer
Position of the unit within its parent collection
type_id integer
Info-unit type identifier

QuizIdentifier

id integer
name string
Quiz name

QuizAttemptIdentifier

id integer
course_id integer nullable
quiz_id integer
user_id integer
status_id integer
Attempt status identifier (passed / failed / in-progress)
comment string nullable
quiz object conditional
Conditional: Included when the quiz relation is loaded.
Quiz metadata payload
course Course conditional
Conditional: Included when the parent course is loaded.
lesson object conditional
Conditional: Included when the parent lesson is loaded.
Lesson metadata payload

Coupons

ExpertCoupon

id integer
code string
Coupon code that customers enter at checkout
reward number nullable
Reward value (discount amount or percentage depending on `is_fixed`)
Discount type enum payload
Expiration-rule type enum payload
is_fixed boolean
True for fixed-currency discount, false for percentage
is_active boolean
used_amount integer
Number of times the coupon has been redeemed
Whether coupon use is restricted to specific contact lists
total_uses integer nullable
Maximum number of times the coupon can be used in total
total_uses_per_user integer nullable
Maximum number of uses per single contact
Conditional: Included when the coupon's contact-list restrictions are part of the response.
type CouponType conditional
Conditional: Included when the coupon-type metadata is part of the response.
Coupon type metadata
expert UserSimple conditional
Conditional: Included when the expert who owns the coupon is part of the response.
user UserSimple conditional
Conditional: Included when the issuing user is part of the response.
contact_id integer nullable
Conditional: Included when the linked contact is part of the response.
Compact identity payload of the linked contact
Conditional: Included when the offers limited by this coupon are part of the response.
Identity payloads of offers limited by this coupon
expires_at datetime nullable
expires_at_utc datetime nullable
timezone_id integer nullable
timezone Timezone conditional
Conditional: Included when the coupon has timezone settings configured.
Conditional: Included when the latest internal comment is part of the response.
Latest internal comment payload (admin-facing)
currency_id integer nullable
currency Currency nullable
created_at datetime
updated_at datetime
deleted_at datetime nullable

CouponType

id integer
title string
Localized coupon type title

CouponCheck

can_use boolean
Whether the coupon is valid and may be applied to an order
discount number nullable
Computed discount amount when `price` was sent in the request; otherwise null

Certificates

Certificate

id integer
uuid string
Universally unique identifier of the certificate
cabinet_id integer
user_id integer nullable
number string
Public certificate number used for lookup
url string nullable
Public verification link; `null` until the certificate is issued
issued_at datetime nullable
finished_at datetime conditional
Conditional: Included when both the user and the certificate target are available in the response.
When the user completed the certificateble entity (course, etc.)
points integer conditional
Conditional: Included when both the user and the certificate target are available in the response.
Points earned by the user toward the certificateble
user UserSimple conditional
Conditional: Included when the related user is part of the response.
creator UserSimple conditional
Conditional: Included when creator information is part of the response.
Conditional: Included when the certificate template is part of the response.
Title of the entity the certificate is issued for (course, product, etc.)

CertificateTemplate

id integer
title string
Internal template title
public_title string nullable
Public-facing template title shown on the issued certificate

StudentProductCertificateInfo

Certificate type (enum payload with id/title)
Conditional: Included when the certificate template is part of the response.
Polymorphic type of the certificateble entity (e.g. course, product)
certificateble_title string conditional
Conditional: Included when the certificate target is part of the response.
Title of the certificateble entity
message string nullable
system_comment string nullable
certificate Certificate conditional
Conditional: Returned only after the certificate has been issued.

Offers

Offer

id integer
unique_offer_code string nullable
Stable code used in payment URLs
url string
Public purchase URL for the offer
crm_url string
Deep link to the offer editor in the expert dashboard (CRM).
title string
description string nullable
short_description string nullable
price_type string
Offer pricing model identifier (e.g. `free`, `fixed`, `recurring`)
price_discounted Price conditional
Conditional: Included when the offer has an active discount.
Price after the active discount is applied
discount OfferDiscount conditional
Conditional: Included when the offer has an active, currently valid discount.
limit_type string nullable
Sales limit type identifier (e.g. `unlimited`, `limited`)
limit_of_sales integer nullable
Maximum number of purchases allowed (null when unlimited)
purchases_count integer conditional
Conditional: Included when purchase counts are available for the offer.
sales_left integer conditional
Conditional: Included when purchase counts are available. Null when the offer has no sales limit.
is_draft boolean
is_active boolean conditional
Conditional: Included when the offer has date settings configured.
Conditional: Returned together with `is_active` when the offer has date settings configured.
Conditional: Returned together with `is_active` when the offer has date settings configured.
products Product[] conditional
Conditional: Included when the offer's products are part of the response.
is_paid boolean conditional
Conditional: Included when payment status is determined for the current viewer.

OfferIdentifier

id integer
title string

OfferSimple

id integer
title string
description string nullable
url string
Public purchase URL for the offer
type_id integer
Offer type identifier
status string nullable
Offer status identifier
Whether the subscription is billed by the merchant
Conditional: Included when product identity entries are loaded for the offer.
curators object[] conditional
Conditional: Included when curator entries are loaded for the offer.
is_paid boolean conditional
Conditional: Included when payment status is determined for the current viewer.

OfferDiscount

id integer
Final price after the discount is applied
Discount limit type enum payload
start_at datetime nullable
start_at_utc datetime nullable
start_timezone_id integer nullable
end_at datetime nullable
end_type string nullable
How the discount ends (fixed date, after sales count, etc.)
end_at_utc datetime nullable
end_timezone_id integer nullable
sales_limit integer nullable
Maximum number of discounted sales allowed
sales integer
Number of discounted sales already made
is_active boolean
is_available boolean
Whether the discount is currently valid (not expired, not exhausted)
created_at datetime
updated_at datetime
offer OfferIdentifier conditional
Conditional: Included when the parent offer is part of the response.
Compact identity payload of the parent offer

Orders

Order

id integer
type_id integer
Order type identifier
crm_url string
Deep link to the order in the expert dashboard (CRM).
first_paid_at datetime nullable
paid_at datetime nullable
created_at datetime
updated_at datetime
products Product[] conditional
Conditional: Included when the order contains product information.
payments Payment[] conditional
Conditional: Included when the order has payment records.
paid_status integer nullable
Payment status identifier
paid_status_title string nullable
Localized title of the payment status
order_stage OrderStage conditional
Conditional: Included when the order has a stage assigned.
Total cost of the order with currency
managers UserSimple[] conditional
Conditional: Included when the order has assigned managers.
offers Offer[] conditional
Conditional: Included when the order contains offer information.
utm UtmList conditional
Conditional: Included when UTM data is available for the order.

OrderFunnel

id integer
title string
Localized funnel title
created_at datetime

OrderGroup

id integer
slug string
title string
Localized group title
order integer
Position of the group within its funnel
created_at datetime

OrderStage

id integer
title string
Localized stage title
order_group OrderGroup conditional
Conditional: Included when the parent group is part of the response.
Conditional: Included when the parent funnel is part of the response.
created_at datetime

Payments

Payment

id integer
status string
Payment status identifier
status_title string
Localized payment status title
payment_type string nullable
Payment type identifier (one-time, recurring, etc.)
payment_type_title string nullable
Localized payment type title
payment_form string nullable
Payment form identifier (online, manual, etc.)
payment_form_title string nullable
Localized payment form title
purchase_type string nullable
Purchase type identifier
purchase_type_title string nullable
Localized purchase type title
Price with currency information
paid_at datetime nullable
created_at datetime
updated_at datetime
Conditional: Included when payment transactions are part of the response.

Transaction

id integer
merchant_id integer nullable
payment_id integer
order_id integer nullable
payment_system_status string nullable
Raw status string returned by the payment system
failure_reason string nullable
Reason for the transaction failure when applicable
price number
currency_code string nullable
payer_account string nullable
Masked payer account identifier (last digits of card / wallet handle)
card_mask string nullable
rrn string nullable
Retrieval Reference Number from the bank
fee number nullable
Payment system processing fee
created_at datetime

Price

amount number
Raw amount value
Amount rounded according to cabinet rounding settings
Amount formatted with the currency symbol/code
Amount formatted with the ISO currency code

Currency

id integer
code string
ISO currency code
html_code string
Localized HTML representation of the currency symbol
Localized HTML representation of the letter currency code

Products

Product

id integer
productable_id integer
Identifier of the underlying entity (course, info-unit, etc.)
Polymorphic type of the underlying entity (e.g. `course`, `info_unit`)
title string
Localized title of the productable entity
url string conditional
Conditional: Included when the underlying entity is part of the response.
Public link to the productable entity

ProductIdentifier

id integer
Polymorphic type of the underlying entity (e.g. `course`, `info_unit`)
productable_id integer
name string
Localized name of the underlying entity
url string
Public link to the underlying entity
course_type_id Enum conditional
Conditional: Included when the underlying entity is a course; carries the course type enum.
Conditional: Included when course lesson identity payloads are loaded for the product.

ProductAggregatedSubscription

is_active boolean
Whether access to the product is currently active
is_paid boolean
Whether the subscription is paid (not pre-subscription/trial)
start_at datetime nullable
end_at datetime nullable
Effective end of access (computed across all active subscriptions)
offer_end_at datetime nullable
order_end_at datetime nullable
frozen_at datetime nullable
When the subscription was frozen; null when it is not frozen.
extended_at datetime nullable
When access was last extended; null when it was never extended.
count_available_days integer nullable
Total days the contact has access to the product
count_left_days integer nullable
Days of access remaining
Current state enum payload

ProductSubscription

id integer
creator_id integer nullable
user_id integer
product_id integer
order_id integer nullable
offer_id integer nullable
is_active boolean
start_at datetime nullable
order_end_at datetime nullable
end_at datetime nullable
Effective end of access for this subscription
frozen_at datetime nullable
When this subscription was frozen; null when it is not frozen.
extended_at datetime nullable
When this subscription was last extended; null when it was never extended.
paid_at datetime nullable
created_at datetime
updated_at datetime

UserProduct

id integer
productable_id integer
Polymorphic type of the underlying entity
title string
image_url string nullable
URL to the productable preview image
url string
Public link to the product page.
is_published boolean
Conditional: Included when subscription summary is part of the response.
Aggregated access info (overall is_active/start/end across all subscriptions)
Conditional: Included when individual subscription records are part of the response.
Individual subscription records (one per order/offer)

Email

EmailCampaign

id integer
title string
Campaign title; falls back to a localized "Transaction" label for system campaigns
state_id integer
Campaign state identifier
is_restricted boolean
True for built-in system campaigns that can't be edited
not_verified boolean
True when the owning account lacks a verified payment method / replenishment
is_system boolean
is_segment boolean
True when the campaign targets a dynamic segment
cabinet_id integer
user_id integer
subject string nullable
subsubject string nullable
Pre-header / preview text shown beneath the subject
start_type string nullable
Schedule type identifier (immediate, scheduled, recurring, etc.)
start_at_utc datetime nullable
start_at datetime nullable
timezone_id integer nullable
paused_at datetime nullable
rejected_at datetime nullable
finished_at datetime nullable
timezone Timezone nullable
Conditional: Included when the campaign's contact lists are loaded.
bounced_limit number
Threshold above which the bounce rate is considered problematic
Threshold above which the complaint rate is considered problematic
count_recipients integer conditional
Conditional: Included when the campaign statistic is loaded.
complaint_rate number conditional
Conditional: Included when the campaign statistic is loaded.
bounced_rate number conditional
Conditional: Included when the campaign statistic is loaded.
count_opened integer conditional
Conditional: Included when the campaign statistic is loaded.
count_clicks integer conditional
Conditional: Included when the campaign statistic is loaded.
percent_opened number conditional
Conditional: Included when the campaign statistic is loaded.
percent_clicks number conditional
Conditional: Included when the campaign statistic is loaded.
percent_completed number conditional
Conditional: Included when the campaign statistic is loaded.
distributor_id integer conditional
Conditional: Returned only for viewers with extended mailing access.
templatable_type string conditional
Conditional: Returned only for viewers with extended mailing access.
templatable_id integer conditional
Conditional: Returned only for viewers with extended mailing access.
template object conditional
Conditional: Returned only for viewers with extended mailing access when the templatable relation is loaded.
Email template payload (system or custom)
distributor object conditional
Conditional: Returned only for viewers with extended mailing access when the distributor relation is loaded.
statistic object conditional
Conditional: Returned only for viewers with extended mailing access when the statistic relation is loaded.
Full campaign statistic snapshot

EmailCampaignIdentifier

id integer
title string
Campaign title; falls back to a localized label for system campaigns

Visitors

Visit

id integer
ip string nullable
landing_url string nullable
Full URL where the visit landed (auth hash stripped)
landing_domain string nullable
landing_path string nullable
landing_params string nullable
Query string of the landing URL with auth hash stripped
referrer_url string nullable
referrer_domain string nullable
utm_source string nullable
utm_campaign string nullable
utm_medium string nullable
utm_term string nullable
utm_content string nullable
location string nullable
Resolved location label (city, country)
Detailed geolocation breakdown
device VisitUserAgent conditional
Conditional: Included when user agent / device data is available for the visit.
User agent / device information
created_at datetime

VisitLocation

Localized country payload
state_name string nullable
State / region name
city string nullable
full_location string
Comma-separated `city, state, country` path with empty parts skipped

VisitUserAgent

user_agent string nullable
Raw User-Agent header captured at the visit
browser string nullable
browser_version string nullable
platform string nullable
Operating system name
platform_version string nullable
Device type enum payload (desktop, mobile, tablet)
title string
Human-readable summary of the user agent

UtmList

utm_source string[]
Distinct utm_source values seen across the contact's visits
utm_campaign string[]
utm_medium string[]
utm_term string[]
utm_content string[]

Comments

CommentIdentifier

id integer
text string
Raw comment text
commentable_id integer
Identifier of the entity the comment is attached to
Polymorphic type of the target entity
commentable object conditional
Conditional: Included when the target entity is part of the response.
Compact payload of the target entity

Quiz attempts

QuizAttempt

id integer
root_id integer nullable
ID of the very first attempt in the retake chain. NULL on the root attempt itself.
previous_id integer nullable
ID of the previous attempt in the retake chain. NULL on the first attempt.
number_version integer
Sequential index of the attempt within the retake chain, starting at 1.
user_id integer
product_id integer
quiz_id integer
course_id integer nullable
course_lesson_id integer nullable
Compact metadata of the info section the quiz is attached to. Only present when the quiz lives inside a section of a lesson (rather than directly on the lesson). NULL otherwise.
status Enum nullable

Attempt status as an enum object — id (numeric), slug (stable case name) and title (label already translated for the cabinet locale).

  • 1 — Completed
  • 2 — Not completed
  • 3 — Revision required
  • 4 — The latest version changed the status
  • 5 — In progress
  • 6 — Pending review
  • 7 — Not started
scores number nullable
Total points scored by the student in this attempt.
scores_max number nullable
Maximum points achievable on this attempt. Falls back to the quiz's total when the attempt itself does not carry a stored max (e.g. virtual "not started" rows). NULL when the quiz has no scoring (e.g. an assignment).
count_questions integer
count_questions_correct integer nullable
count_questions_incorrect integer nullable
Whether a curator overrode the auto-grade for this attempt.
is_read boolean
Whether the curator has read the attempt. Open-text quizzes only - other quiz types are flagged read automatically.
started_at datetime nullable
last_activity_at datetime nullable
finished_at datetime nullable
deadline_at datetime nullable
status_updated_at datetime nullable
Timestamp of the last status transition.
checked_at datetime nullable
commented_at datetime nullable
canceled_at datetime nullable
Duration spent in the attempt, in seconds. Computed as finished_at minus started_at (or now minus started_at while still in progress); always at least 1.
crm_url string
Deep link to the curator's CRM view of this attempt on the cabinet subdomain. Lets staff jump straight to the attempt in the expert dashboard.
user UserSimple conditional
Conditional: Included when the user relation is loaded.
quiz QuizIdentifier conditional
Conditional: Included when the quiz relation is loaded.
course Course conditional
Conditional: Included when the parent course is loaded.
Conditional: Included when the parent lesson is loaded.
Conditional: Included when the product relation is loaded.

InfoSectionReport

id integer
name string
Display title of the section (falls back to a generated label when the section has no explicit title).
order integer
1-based position of the section within its parent lesson.
url string
Public link to the section as a participant sees it.
crm_url string
Deep link to the section constructor in the expert dashboard.

Rewards (gamification)

FlowRewardLog

id integer
points number
Number of points moved by this journal entry. Positive = accrual, negative = deduction.
Direction of the transfer derived from the sign of points β€” accrued or deducted. Mirrors the accrual_type filter on the list endpoint.

Why this points entry exists. Possible ids:

  • 1 — Quiz passed
  • 2 — Quiz canceled
  • 3 — Quiz attempt reset previous scores
  • 4 — Quiz attempt set new scores
  • 5 — Pay by points
  • 6 — Manual
  • 7 — Automation
  • 8 — Quiz scores changing
  • 9 — Pay by points for gift
event string nullable
Stable string key of the source flow event (only set for automation/event-driven rewards). NULL for manual entries.
event_name string nullable
Human-readable title of the source flow event (same source as event). NULL for manual entries.
eventable_type string nullable
Polymorphic type of the source entity (e.g. quiz_attempt, order, gift). NULL for manual entries.
eventable_id integer nullable
ID of the source entity inside eventable_type. NULL for manual entries.
event_comment string nullable
Free-form comment captured at the moment of the event (e.g. curator's reason when canceling a quiz). NULL when absent.
manual_comment string nullable
Curator comment attached to a manual accrual/deduction (reason_type=Manual). Up to 255 chars. NULL for all other reason types.
is_visible_to_student boolean nullable
Whether manual_comment is shown to the student in their own points journal. NULL for non-manual entries. Curators (this API included) always see the comment regardless of the flag.
message string
Pre-rendered plain-text summary of the entry suitable for showing in a UI. Same template the CRM uses, with HTML markup stripped. For manual entries, the curator comment is appended here.
Conditional: Included when the reward is tied to a product.
creator UserSimple conditional
Conditional: Included for rewards added by a curator/expert (manual or automation triggered by a user).
created_at datetime

Common

DateSetting

type string
Setting type β€” `fixed`, `relative`, `recurring`, etc.
duration_type_id integer nullable
Identifier of the duration unit (days/weeks/months)
date_at datetime nullable
Fixed date in the cabinet timezone
date_at_utc datetime nullable
Same date converted to UTC
timezone_id integer nullable
timezone Timezone nullable
after_months integer nullable
Relative offset in months (for relative types)
after_days integer nullable
specific_time string nullable
Specific time of day (HH:MM)
specific_day_of_week integer nullable
1 (Monday) – 7 (Sunday) when bound to a specific weekday
is_current_week boolean nullable
is_current_day boolean nullable

Timezone

id integer
name string
IANA timezone identifier
name_full string
Timezone name with UTC offset for display
value string
UTC offset string

Enum

id integer
Numeric enum value (also referred to as `value` in some places)
slug string
Enum case name (stable string identifier)
title string
Localized enum label rendered in the current `X-Language`

FileSimple

id integer
uuid string
Universally unique identifier of the file
name string
Internal stored name of the file
original_name string
Original filename uploaded by the user
url string nullable
Public URL to the file (null when storage is unavailable or the viewer lacks access)
thumbnails object nullable
Map of thumbnail sizes to public URLs
extension string nullable
File extension without the leading dot
type_id integer
File type identifier
mime_type string nullable

UserSimple

id integer
name string nullable
Composed full name (first + last) of the user
email string nullable
User email (may be masked for non-owner viewers)

CountrySimple

id integer
code string
ISO 3166-1 alpha-2 country code
name string
Localized country name in the current `X-Language`

Envelope & Pagination

Success

success boolean

AffectedSubscriptions

IDs of subscription records that were deleted
affected_orders integer[]
IDs of orders whose access was affected (became custom)
Per-offer breakdown keyed by offer_id; each value contains `affected_subscriptions` and `affected_orders` integer arrays. Empty object when no offers were passed in the request.
Per-product breakdown keyed by product_id; same shape as `affected_by_offers`. Empty object when no product_id was passed.

first string nullable
URL of the first page
last string nullable
URL of the last page
prev string nullable
URL of the previous page (null when on the first page)
next string nullable
URL of the next page (null when on the last page)

PaginationMeta

current_page integer
from integer nullable
Index of the first item on the current page (1-based)
last_page integer
Navigation items (prev / page-number buttons / next) for rendering a pager UI
path string
Base path used to build pagination URLs
per_page integer
to integer nullable
Index of the last item on the current page (1-based)
total integer
Total number of items across all pages

url string nullable
Link target URL (null for the active page)
label string
Display label β€” page number, "« Previous", "Next »", etc.
active boolean
True for the entry representing the current page