Документация по API

Интегрируйте свои приложения с торговой площадкой: читайте данные своего аккаунта и товаров, получайте данные о продажах для пользовательских панелей мониторинга и проверяйте коды покупок покупателей с вашего собственного лицензионного сервера.

Базовый URL: https://sellmycode.net/api Формат: JSON Ограничение частоты запросов: 60 req/min
Быстрая проверка
curl https://sellmycode.net/api

Аутентификация

Каждый запрос требует личного токена доступа. Создавайте именованные токены с ограниченными возможностями в своей рабочей области в разделе «Настройки» → «Ключ API» — каждое приложение получает свой токен только с необходимыми ему разрешениями, и вы можете отозвать любой из них в любое время.

account:read items:read sales:read purchases:validate purchases:read balance:read catalog:read
Отправляйте ключ в заголовке Authorization (рекомендуется). Параметр запроса api_key также принимается для обратной совместимости, но ключи в URL-адресах могут утечь в логи.
Рекомендуется — Authorization header
curl https://sellmycode.net/api/account/details \
  -H "Authorization: Bearer YOUR_API_KEY"
Альтернатива — X-Api-Key header
curl https://sellmycode.net/api/account/details \
  -H "X-Api-Key: YOUR_API_KEY"

Ошибки и ограничения

КодЗначение
200Успешно
400Ошибка валидации — отсутствует или некорректно сформирован обязательный параметр
401Недействительный или отсутствующий ключ API
404Ресурс не найден (также возвращается для недействительного кода покупки)
429Превышено ограничение частоты запросов — подождите и повторите попытку (60 запросов в минуту)
Формат ошибки
{
  "status": "error",
  "msg": "Invalid request"
}

GETДетали аккаунта

Возвращает профиль аккаунта, которому принадлежит ключ API.

GET/api/account/details
Request
curl https://sellmycode.net/api/account/details \
  -H "Authorization: Bearer YOUR_API_KEY"

GETВсе товары

Все ваши утвержденные товары, сначала новейшие. Только для авторов.

GET/api/items/all
Request
curl https://sellmycode.net/api/items/all \
  -H "Authorization: Bearer YOUR_API_KEY"

GETОтдельный товар

Один из ваших утвержденных товаров по его числовому ID.

GET/api/items/item?item_id={id}
ПараметрТипОписание
item_id ОбязательноintegerID товара (показан в списке товаров вашей рабочей области)
Request
curl "https://sellmycode.net/api/items/item?item_id=123" \
  -H "Authorization: Bearer YOUR_API_KEY"

GETПродажи

Ваши продажи, сначала новейшие — создавайте дашборды доходов или синхронизируйте заказы в своих собственных инструментах. С пагинацией.

GET/api/sales
ПараметрТипОписание
from необязательноdateY-m-d — только продажи на или после этой даты
to необязательноdateY-m-d — только продажи на или до этой даты
item_id необязательноintegerфильтр по одному из ваших товаров
status необязательноstringactive | refunded | cancelled | held
per_page необязательноintegerпо умолчанию 25, максимум 50
page необязательноintegerномер страницы
Request
curl "https://sellmycode.net/api/sales?from=2026-01-01&status=active&per_page=25" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response 200
{
  "status": "success",
  "pagination": { "page": 1, "per_page": 25, "total": 132, "last_page": 6 },
  "sales": [
    {
      "id": 981,
      "purchase_code": "8f14e45f-ce95-41d8-a2b6-72e5f13d81a7",
      "item": { "id": 123, "name": "My Theme" },
      "buyer": "johndoe",
      "license_type": "Regular",
      "price": 29.0,
      "fee": 5.8,
      "earning": 23.2,
      "currency": "USD",
      "sale_status": "active",
      "cleared": true,
      "date": "2026-07-01T09:30:00+00:00"
    }
  ]
}

GETМои покупки

Ваши собственные покупки в качестве покупателя, от новых к старым — проверяйте лицензии и окна загрузки в ваших инструментах. С разбивкой по страницам.

GET/api/purchases
ПараметрТипОписание
status необязательноstringactive | refunded | cancelled | held
per_page необязательноintegerпо умолчанию 25, максимум 50
page необязательноintegerномер страницы
Request
curl "https://sellmycode.net/api/purchases?status=active" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response 200
{
  "status": "success",
  "pagination": { "page": 1, "per_page": 25, "total": 3, "last_page": 1 },
  "purchases": [
    {
      "purchase_code": "8f14e45f-ce95-41d8-a2b6-72e5f13d81a7",
      "item": { "id": 123, "name": "My Theme", "url": "https://sellmycode.net/item/my-theme/123" },
      "license_type": "Regular",
      "price": 29.0,
      "currency": "USD",
      "purchase_status": "active",
      "download_expiry_at": "2027-01-01T00:00:00+00:00",
      "download_expired": false,
      "date": "2026-07-01T09:30:00+00:00"
    }
  ]
}

GETБаланс

Снимок ваших кошельков только для чтения: доступные для вывода средства, средства, находящиеся в процессе клиринга, и Store Credit. При необходимости включите историю выписок.

GET/api/balance
ПараметрТипОписание
statements необязательноboolean1, чтобы включить недавние выписки
wallet необязательноstringbalance | store_credit — фильтровать выписки по кошельку
per_page необязательноintegerпо умолчанию 25, максимум 50
Request
curl "https://sellmycode.net/api/balance?statements=1&wallet=balance" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response 200
{
  "status": "success",
  "currency": "USD",
  "wallets": {
    "balance": 1250.75,
    "pending_balance": 89.4,
    "store_credit": 12.5
  },
  "pagination": { "page": 1, "per_page": 25, "total": 57, "last_page": 3 },
  "statements": [
    {
      "id": 4021,
      "title": "[Sale] #981 (My Theme)",
      "wallet": "balance",
      "type": "credit",
      "amount": 29.0,
      "total": 23.2,
      "date": "2026-07-01T09:30:05+00:00"
    }
  ]
}

POSTПроверка покупки

Проверьте код покупки, который вам предоставил покупатель — основа любой лицензионной системы. Возвращает детали покупки, если код принадлежит одному из ВАШИХ товаров и все еще активен; в противном случае 404.

POST/api/purchases/validation
ПараметрТипОписание
purchase_code ОбязательноstringКод от покупателя (показан на странице «Покупки» и в лицензионном сертификате)
curl -X POST https://sellmycode.net/api/purchases/validation \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d "purchase_code=BUYER_PURCHASE_CODE"
// license check from your app / plugin
$ch = curl_init('https://sellmycode.net/api/purchases/validation');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS => http_build_query(['purchase_code' => $code]),
]);
$res = json_decode(curl_exec($ch), true);
$valid = ($res['status'] ?? '') === 'success';
// Node.js 18+ (built-in fetch)
const res = await fetch('https://sellmycode.net/api/purchases/validation', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ purchase_code: code }),
});
const data = await res.json();
const valid = res.ok && data.status === 'success';
# Python 3 + requests
import requests

res = requests.post(
    'https://sellmycode.net/api/purchases/validation',
    headers={'Authorization': 'Bearer YOUR_API_KEY'},
    data={'purchase_code': code},
    timeout=10,
)
valid = res.status_code == 200 and res.json().get('status') == 'success'
Response 200
{
  "status": "success",
  "buyer": "johndoe",
  "item": {
    "purchase_code": "8f14e45f-ce95-41d8-a2b6-72e5f13d81a7",
    "license_type": "Regular",
    "price": 29.0,
    "currency": "USD",
    "item": { "…item fields…" },
    "supported_until": "2026-12-01T00:00:00+00:00",
    "download_expiry": "2026-08-01T00:00:00+00:00",
    "downloaded": true,
    "date": "2026-07-01T09:30:00+00:00"
  }
}
Код 404 возвращается КАК для неизвестного кода, ТАК И для возвращенной/заблокированной покупки — считайте любой статус, отличный от 200, как «лицензия недействительна».

GETПубличный каталог

Просматривайте и ищите по всему утвержденному каталогу — открыто для ЛЮБОГО аккаунта, покупателя или автора. Создавайте трекеры цен, витрины портфолио или панели категорий. Возвращает только данные витрины: цены для покупателей (включая скидки), рейтинги и количество продаж — никогда файлы, ссылки для скачивания или идентификаторы покупателей.

GET/api/catalog/items
ПараметрТипОписание
search необязательноstringполнотекстовый поиск по названиям и тегам товаров
category необязательноstringслаг категории (см. конечную точку категорий)
sort необязательноstringlatest (по умолчанию) | popular | rating
per_page необязательноintegerпо умолчанию 25, максимум 50
page необязательноintegerномер страницы
Request
curl "https://sellmycode.net/api/catalog/items?search=woocommerce&sort=popular" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response 200
{
  "status": "success",
  "pagination": { "page": 1, "per_page": 25, "total": 64, "last_page": 3 },
  "items": [
    {
      "id": 123,
      "name": "My Theme",
      "url": "https://sellmycode.net/item/my-theme/123",
      "thumbnail": "https://sellmycode.net/files/thumbnails/…",
      "category": { "id": 4, "name": "WordPress", "slug": "wordpress" },
      "author": "janedoe",
      "price": { "regular": 29.0, "extended": 145.0 },
      "currency": "USD",
      "rating": { "average": 4.8, "count": 36 },
      "version": "2.1.0",
      "updated_at": "2026-07-10T08:00:00+00:00",
      "published_at": "2026-01-05T12:00:00+00:00"
    }
  ]
}
GET/api/catalog/items/{id}

Полная общедоступная информация об одном утвержденном товаре — поля списка плюс описание, предварительный просмотр медиа, список включенных файлов и фреймворк.

Request
curl https://sellmycode.net/api/catalog/items/123 \
  -H "Authorization: Bearer YOUR_API_KEY"
GET/api/catalog/categories

Все категории с количеством утвержденных товаров — используйте слаг в качестве фильтра категорий выше.

Response 200
{
  "status": "success",
  "categories": [
    { "id": 4, "name": "WordPress", "slug": "wordpress", "url": "https://sellmycode.net/categories/wordpress", "items_count": 128 }
  ]
}
Любая зарегистрированная учетная запись может создать токен с разрешением catalog:read — вам не нужно быть автором или совершать покупку.

Активация лицензии

Для программного обеспечения, обращающегося к серверу — скриптов PHP, настольных или мобильных приложений. Ваша установка отправляет код покупки и свой домен; мы подтверждаем лицензию и считаем, на скольких сайтах она работает, чтобы вам не пришлось размещать собственный сервер лицензирования.

These endpoints need NO API token — the purchase code is the credential, so the code you ship to buyers can call them directly.
Every answer is signed with Ed25519. Verify the signature and the nonce before trusting it. A module that only reads "status" can be fooled by a fake server or a hosts-file redirect that answers "success".
Право на ИСПОЛЬЗОВАНИЕ программного обеспечения никогда не истекает. Когда период поддержки обновлений заканчивается, проверка (verify) по-прежнему возвращает valid — только блок обновлений сообщает о необходимости продления. Истекший период обновлений ни в коем случае не должен нарушать работу действующего сайта.
POST/api/license/activate
POST/api/license/verify
POST/api/license/deactivate
GET/api/license/public-key
ПараметрТипОписание
purchase_code Обязательноstringкод, полученный покупателем
domain Обязательноstringсайт, на котором работает программное обеспечение; протокол, www и путь удаляются автоматически
nonce Обязательноstring16–128 random characters (A-Z a-z 0-9 _ -), new for EVERY call. It is echoed inside the signed answer, so an old answer cannot be replayed.
activation_tokenstringdeactivate only — the token you received when this site was activated

открытый ключ

Embed this key in your software. It is also served at GET /api/license/public-key; the kid in each answer tells you which key signed it, so a future key can be added without breaking installs.

Ed25519 · kid k1
5Uk3p1+J0PXzJkwS4wVFvTN9GcHpJzX5ClRI8zlI0yQ=

Key and signature format

ТоварФормат
AlgorithmEd25519 as defined in RFC 8032 (pure Ed25519 — no pre-hashing, no context string)
открытый ключthe raw 32-byte Ed25519 public key, standard Base64 with padding (44 characters). Not PEM, not DER, not hex.
Signaturethe raw 64-byte Ed25519 signature, standard Base64 with padding (88 characters)
Signed messagethe exact UTF-8 bytes of the payload string as it appears in the envelope — verify before decoding; never re-encode the JSON and verify that
kidshort string naming the key that signed this answer; pick the matching public key by it
TimestampsISO 8601 with offset, always UTC (+00:00)

Libraries that want PEM/SPKI instead of the raw key (OpenSSL, Java, .NET) can use this — the same key wrapped in the standard 12-byte Ed25519 SPKI header:

PEM (SPKI) · kid k1
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA5Uk3p1+J0PXzJkwS4wVFvTN9GcHpJzX5ClRI8zlI0yQ=
-----END PUBLIC KEY-----

Response format

Every answer — success or error — is an envelope. The signature covers the payload string exactly as received; verify it first, then decode the payload.

Envelope
{
  "payload": "{\"status\":\"success\",\"code\":\"activated\", … }",
  "signature": "base64 Ed25519 signature of payload",
  "kid": "k1",
  "alg": "Ed25519"
}
Decoded payload — activate
{
  "status": "success",
  "code": "activated",
  "domain": "customer-site.com",
  "license_type": "Regular",
  "activation_limit": 2,
  "activations_used": 1,
  "updates": { "active": true, "until": "2027-01-30T10:00:00+00:00" },
  "activation_token": "9f2c…e41a",
  "grace_until": "2026-10-16T08:00:00+00:00",
  "nonce": "the nonce you sent",
  "issued_at": "2026-10-09T08:00:00+00:00"
}

Fields returned

Envelope — the top level of every answer:

FieldТипОписание
payloadstringthe signed JSON document, as a string
signaturestringBase64 Ed25519 signature of payload
kidstringwhich key signed it
algstringalways "Ed25519"

Payload of a successful verify (HTTP 200, code "valid"). activate returns the same fields:

FieldТипОписание
statusstring"success"
codestringverify: "valid" · activate: "activated" or "already_activated"
domainstringthe domain as we stored it: lower case, no scheme, no www, no path
item.idintegeritem id on SellMyCode — check it is YOUR item, so a code bought for another product is not accepted
item.namestringitem title
item.versionstringlatest published version
item.urlstringitem page
license_typestring"Regular" | "Extended"
purchased_atstringISO 8601, UTC
activation_limitinteger | nullsites this licence may run on; null = unlimited
activations_usedintegersites currently active
updates.activebooleanwhether the buyer may still download updates — informational, never a reason to stop working
updates.untilstring | nullend of the update period, ISO 8601 UTC; null = no end date
updates.renew_urlstring | nullwhere to renew, set only once updates have ended
grace_untilstringrun on this answer until this time if we cannot be reached (currently 7 days ahead)
noncestringthe nonce you sent — must match
issued_atstringwhen we signed it, ISO 8601 UTC
activation_tokenstring | nullactivate only: 48 hex characters on "activated"; null on "already_activated"
hintstringactivate only, with "already_activated"

Payload of deactivate (HTTP 200, code "deactivated"): status, code, domain, activations_used, activation_limit, nonce, issued_at.

Payload of every error: status ("error"), code, msg, nonce (null if yours was missing or malformed), issued_at — plus, depending on the code, activation_limit, activations_used, hint and retry_after (seconds, with rate_limited).

New fields may be added to the payload over time; ignore fields you do not know. Existing fields keep their name and type.

Key rotation

Every answer names its key in kid. GET /api/license/public-key lists every key with a status:

statusЗначение
activesigns every answer now — exactly one key at a time
nextannounced, not signing yet — add it to your software now
retiredno longer signs; harmless to keep, never needed again
revokedits secret may have leaked — remove it and reject answers signed with it
  1. Planned rotation: the new key is published as "next" at least 90 days before it starts signing, and every author with a licensed item is emailed. Ship an update that knows both keys during that window.
  2. On the switch date the new key becomes "active" and the old one "retired". Installations that only know the old key can no longer verify new answers; they keep running on their cached answer until its grace_until, then report unlicensed.
  3. Emergency rotation, if a secret is ever exposed: the new key signs immediately and the old one is marked "revoked". Authors are emailed the same day and should release an update removing the revoked key.
  4. A kid is never reused.
Embed the keys in your code (a kid → key map). Never download the key at runtime and trust it — a fake server would simply hand out its own key. The public-key endpoint is for building and checking your release, not for installations. An answer whose kid you do not know is unverified: keep your cached state.

What your software must do

  1. Send a fresh random nonce with every call.
  2. Verify the signature over the payload string with the public key. If it fails, ignore the answer completely.
  3. Decode the payload and check that its nonce equals the one you sent.
  4. On activate, store activation_token — it is shown once and is the only way to deactivate this site through the API.
  5. Keep the last verified answer. When we cannot be reached, answer 5xx, or rate limit you, keep running on it until its grace_until — never treat an outage as "licence invalid". A signed error such as invalid_code or not_activated is authoritative and should replace it.
// PHP 7.2+ (sodium is built in)
function sellmycode_license(string $action, array $fields): ?array
{
    // kid => raw 32-byte public key (Base64). Add the "next" key here when one is announced.
    $keys = ['k1' => '5Uk3p1+J0PXzJkwS4wVFvTN9GcHpJzX5ClRI8zlI0yQ='];
    $fields['nonce'] = bin2hex(random_bytes(16));

    $ch = curl_init('https://sellmycode.net/api/license/' . $action);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 15,
        CURLOPT_POSTFIELDS => http_build_query($fields),
    ]);
    $res = json_decode((string) curl_exec($ch), true);

    // unreachable, 5xx, unsigned or forged: return null and keep the cached state
    if (!isset($res['payload'], $res['signature'], $keys[$res['kid'] ?? ''])
        || !sodium_crypto_sign_verify_detached(base64_decode($res['signature']), $res['payload'], base64_decode($keys[$res['kid']]))) {
        return null;
    }
    $data = json_decode($res['payload'], true);

    // must answer THIS request, not be an old answer replayed
    return ($data['nonce'] ?? '') === $fields['nonce'] ? $data : null;
}

// first run: activate and keep the token
$r = sellmycode_license('activate', ['purchase_code' => $code, 'domain' => $_SERVER['HTTP_HOST']]);
if ($r && $r['status'] === 'success' && $r['activation_token']) {
    save_setting('license_token', $r['activation_token']);
}

// daily: verify, cache the signed answer, run on it through an outage
$r = sellmycode_license('verify', ['purchase_code' => $code, 'domain' => $_SERVER['HTTP_HOST']]);
if ($r && $r['code'] !== 'rate_limited') {
    save_setting('license_state', $r);
}
$state = get_setting('license_state');
$licensed = $state && $state['status'] === 'success' && strtotime($state['grace_until']) > time();
// Node.js 18+
import crypto from 'node:crypto';

const PUBLIC_KEY = crypto.createPublicKey({
  format: 'jwk',
  key: { kty: 'OKP', crv: 'Ed25519',
         x: Buffer.from('5Uk3p1+J0PXzJkwS4wVFvTN9GcHpJzX5ClRI8zlI0yQ=', 'base64').toString('base64url') },
});

async function sellmycodeLicense(action, fields) {
  const nonce = crypto.randomBytes(16).toString('hex');
  let res;
  try {
    const r = await fetch('https://sellmycode.net/api/license/' + action, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...fields, nonce }),
    });
    res = await r.json();
  } catch { return null; }
  if (!res.payload || !res.signature) return null;
  const ok = crypto.verify(null, Buffer.from(res.payload), PUBLIC_KEY, Buffer.from(res.signature, 'base64'));
  if (!ok) return null;
  const data = JSON.parse(res.payload);
  return data.nonce === nonce ? data : null;
}
# Python 3 + requests + cryptography
import base64, json, secrets, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

PUBLIC_KEY = Ed25519PublicKey.from_public_bytes(base64.b64decode("5Uk3p1+J0PXzJkwS4wVFvTN9GcHpJzX5ClRI8zlI0yQ="))

def sellmycode_license(action, fields):
    nonce = secrets.token_hex(16)
    try:
        res = requests.post("https://sellmycode.net/api/license/" + action, json=dict(fields, nonce=nonce), timeout=15).json()
        PUBLIC_KEY.verify(base64.b64decode(res["signature"]), res["payload"].encode())
    except Exception:
        return None   # unreachable, unsigned or forged
    data = json.loads(res["payload"])
    return data if data.get("nonce") == nonce else None

Deactivating a site

Send purchase_code, domain, nonce and the activation_token you stored at activation. Without the token the call is refused, so knowing a buyer's code and domain is not enough to release their site. A buyer who lost the token can release the site under Workspace → Licenses; you can revoke one from the same page.

Код ошибкиHTTPЗначение
invalid_code403неизвестный, возвращенный или аннулированный код покупки
activation_limit_reached403все разрешенные сайты уже используются — сначала деактивируйте один из них
not_activated403этот домен никогда не активировался (или был освобожден)
invalid_activation_token403deactivate: the token does not belong to this site
invalid_request400a parameter is missing or malformed — most often the nonce
invalid_domain400не удалось разобрать домен
rate_limited429slow down and retry after retry_after seconds — not a licence problem, keep your cached state
signing_unavailable503temporary, and the only unsigned answer — keep your cached state
Limits: 30 calls per 10 minutes for each licence + domain, and 30 FAILED lookups per 10 minutes per IP. Valid calls do not count against the IP, so many sites sharing one hosting IP are not blocked.
Set the number of allowed sites per licence when you publish the item (Activation limit, separately for Regular and Extended). Leave it empty for unlimited.

Обновления товаров (Toolkit)

Позвольте сайту WordPress поддерживать купленные здесь темы и плагины в актуальном состоянии. Сообщите нам, какие пакеты установлены, получите список тех, у которых есть более новая версия, и скачайте ZIP-архив для установки. Это то, что использует плагин SellMyCode Toolkit — эти конечные точки нужны вам, только если вы создаете собственный механизм обновлений.

Требуется токен с разрешением updates:read. Обновления предоставляются, пока действует период поддержки обновлений вашей покупки; после его окончания мы всё равно сообщим вам о наличии более новой версии, но файл будет недоступен, пока вы не продлите поддержку.

Используете WordPress? Пропустите код

Плагин SellMyCode Toolkit уже подключает эти эндпоинты к обычным экранам обновлений «Темы» и «Плагины». Установите его, вставьте токен, и купленные товары будут обновляться так же, как и всё остальное в WordPress.

POSTПроверить наличие обновлений

POST/api/updates/check
ПараметрТипОписание
packagesarrayустановленные пакеты, до 100 за один запрос
packages[].slugstringназвание папки темы или плагина
packages[].typestringtheme | plugin
packages[].version необязательноstringтекущая установленная версия
Request
curl "https://sellmycode.net/api/updates/check" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"packages":[{"slug":"my-theme","type":"theme","version":"1.4.0"}]}'
Response 200
{
  "status": "success",
  "checked": 1,
  "updates": [
    {
      "item_id": 123,
      "slug": "my-theme",
      "type": "theme",
      "name": "My Theme",
      "installed_version": "1.4.0",
      "new_version": "1.6.2",
      "changelog": { "version": "1.6.2", "body": "Fixed ..." },
      "can_download": true,
      "reason": null,
      "update_window_ends_at": "2027-01-30T10:00:00+00:00",
      "download_url": "https://sellmycode.net/api/updates/download/123",
      "renew_url": null
    }
  ]
}
Возвращаются только реально купленные вами товары — всё остальное, установленное на сайте, игнорируется, поэтому отправлять полный список плагинов безопасно.

GETСкачать установочный пакет

GET/api/updates/download/{id}

Отвечает перенаправлением 302 на временный URL-адрес хранилища (действителен несколько минут). Следуйте перенаправлениям и сохраните ZIP-архив — он содержит только тему или плагин, готовые к установке.

Request
curl -L -o update.zip "https://sellmycode.net/api/updates/download/123" \
  -H "Authorization: Bearer YOUR_API_KEY"
Код ошибкиHTTPЗначение
not_purchased403этот аккаунт не покупал данный товар
update_window_expired403период поддержки обновлений закончился — renew_url включен в ответ
no_installable_file404автор не предоставил установочный пакет для этого товара
item_removed410товар больше недоступен
Эти конечные точки ограничены 60 запросами в минуту. Обычно сайту требуется всего несколько запросов в день — проверьте один раз, затем скачайте то, что изменилось.

Вебхуки для продаж

Вместо того чтобы постоянно запрашивать конечную точку продаж, зарегистрируйте URL-адрес HTTPS, и мы отправим на него подписанное событие JSON, как только что-то произойдет. Управляйте конечными точками в своем рабочем пространстве в разделе «Настройки» → «Ключ API» → «Вебхуки» (до 5, каждая со своим секретом подписи, который показывается один раз при создании).

СобытиеОтправлено, когда
sale.createdОдин из ваших товаров, комплектов или курсов продан
sale.refundedОдна из ваших продаж возвращена или отменена — отзовите лицензию со своей стороны

Доставка

Каждая доставка представляет собой HTTP POST с телом JSON. Ответьте любым статусом 2xx в течение 10 секунд; выполняйте тяжелую работу асинхронно. Неудачные доставки повторяются 3 раза (через 1 минуту, 15 минут и 1 час). После 10 последовательных сбоев — или ответа 410 Gone — конечная точка автоматически отключается, и вы получаете уведомление.

ЗаголовокОписание
X-SMC-EventИмя события, например sale.created
X-SMC-DeliveryУникальный идентификатор доставки (UUID) — дедуплицируйте повторные попытки с его помощью
X-SMC-Signaturet=<unix>,v1=<hex> — Подпись HMAC-SHA256 (см. ниже)
Пример полезной нагрузки — sale.created
{
  "id": "1c2f6a7e-9d31-4c3e-8f5b-2d9f8f4f1a10",
  "event": "sale.created",
  "created_at": "2026-07-18T10:30:00+00:00",
  "data": {
    "sale_id": 981,
    "purchase_code": "8f14e45f-ce95-41d8-a2b6-72e5f13d81a7",
    "type": "item",
    "item": { "id": 123, "name": "My Theme" },
    "buyer": "johndoe",
    "license_type": "Regular",
    "price": 29.0,
    "earning": 23.2,
    "currency": "USD",
    "date": "2026-07-18T10:30:00+00:00"
  }
}

Проверка подписи

Вычислите HMAC-SHA256 для строки "timestamp.rawBody" с вашим секретом подписи и сравните его (постоянное время) со значением v1. Отклоняйте доставки, timestamp которых старше ~5 минут, чтобы предотвратить повторы.

$secret  = 'whsec_...'; // from Settings → API Key → Webhooks
$body    = file_get_contents('php://input');
$header  = $_SERVER['HTTP_X_SMC_SIGNATURE'] ?? '';

parse_str(str_replace(',', '&', $header), $sig); // ['t' => ..., 'v1' => ...]

$expected = hash_hmac('sha256', $sig['t'] . '.' . $body, $secret);

if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) $sig['t']) > 300) {
    http_response_code(400); exit;
}

http_response_code(200); // ack fast, process async
// Express: use express.raw() so the RAW body is available for the HMAC
const crypto = require('crypto');
const SECRET = 'whsec_...'; // from Settings → API Key → Webhooks

app.post('/webhooks/sellmycode', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('X-SMC-Signature') || '';
  const sig = Object.fromEntries(header.split(',').map(p => p.split('=')));

  const expected = crypto.createHmac('sha256', SECRET)
    .update(sig.t + '.' + req.body.toString('utf8')).digest('hex');

  const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
  const valid = sig.v1 &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));

  if (!valid || !fresh) return res.sendStatus(400);

  const event = JSON.parse(req.body); // { id, event, created_at, data }
  res.sendStatus(200); // ack fast, process async
});
# Flask — request.get_data() is the RAW body needed for the HMAC
import hmac, hashlib, time
from flask import Flask, request

SECRET = 'whsec_...'  # from Settings → API Key → Webhooks

@app.route('/webhooks/sellmycode', methods=['POST'])
def webhook():
    header = request.headers.get('X-SMC-Signature', '')
    sig = dict(p.split('=', 1) for p in header.split(',') if '=' in p)

    body = request.get_data()  # bytes, unparsed
    expected = hmac.new(
        SECRET.encode(), (sig.get('t', '') + '.').encode() + body, hashlib.sha256
    ).hexdigest()

    fresh = abs(time.time() - float(sig.get('t', 0))) < 300
    if not (hmac.compare_digest(expected, sig.get('v1', '')) and fresh):
        return '', 400

    event = request.get_json()  # { id, event, created_at, data }
    return '', 200  # ack fast, process async
# Rails / Rack — request.body.read is the RAW body needed for the HMAC
SECRET = 'whsec_...' # from Settings → API Key → Webhooks

def webhook
  header = request.headers['X-SMC-Signature'].to_s
  sig = header.split(',').to_h { |p| p.split('=', 2) }

  body = request.body.read
  expected = OpenSSL::HMAC.hexdigest('SHA256', SECRET, "#{sig['t']}.#{body}")

  fresh = (Time.now.to_i - sig['t'].to_i).abs < 300
  valid = sig['v1'] &&
    ActiveSupport::SecurityUtils.secure_compare(expected, sig['v1'])

  return head :bad_request unless valid && fresh

  event = JSON.parse(body) # { id, event, created_at, data }
  head :ok # ack fast, process async
end
Конечные точки должны быть публичными URL-адресами HTTPS. Доставки никогда не следуют перенаправлениям. Используйте X-SMC-Delivery ID для дедупликации — повторная попытка после тайм-аута может прийти, даже если вы уже обработали оригинал.