Документация по API
Интегрируйте свои приложения с торговой площадкой: читайте данные своего аккаунта и товаров, получайте данные о продажах для пользовательских панелей мониторинга и проверяйте коды покупок покупателей с вашего собственного лицензионного сервера.
curl https://sellmycode.net/api
Аутентификация
Каждый запрос требует личного токена доступа. Создавайте именованные токены с ограниченными возможностями в своей рабочей области в разделе «Настройки» → «Ключ API» — каждое приложение получает свой токен только с необходимыми ему разрешениями, и вы можете отозвать любой из них в любое время.
curl https://sellmycode.net/api/account/details \ -H "Authorization: Bearer YOUR_API_KEY"
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.
curl https://sellmycode.net/api/account/details \ -H "Authorization: Bearer YOUR_API_KEY"
GETВсе товары
Все ваши утвержденные товары, сначала новейшие. Только для авторов.
curl https://sellmycode.net/api/items/all \ -H "Authorization: Bearer YOUR_API_KEY"
GETОтдельный товар
Один из ваших утвержденных товаров по его числовому ID.
| Параметр | Тип | Описание |
|---|---|---|
| item_id Обязательно | integer | ID товара (показан в списке товаров вашей рабочей области) |
curl "https://sellmycode.net/api/items/item?item_id=123" \ -H "Authorization: Bearer YOUR_API_KEY"
GETПродажи
Ваши продажи, сначала новейшие — создавайте дашборды доходов или синхронизируйте заказы в своих собственных инструментах. С пагинацией.
| Параметр | Тип | Описание |
|---|---|---|
| from необязательно | date | Y-m-d — только продажи на или после этой даты |
| to необязательно | date | Y-m-d — только продажи на или до этой даты |
| item_id необязательно | integer | фильтр по одному из ваших товаров |
| status необязательно | string | active | refunded | cancelled | held |
| per_page необязательно | integer | по умолчанию 25, максимум 50 |
| page необязательно | integer | номер страницы |
curl "https://sellmycode.net/api/sales?from=2026-01-01&status=active&per_page=25" \ -H "Authorization: Bearer YOUR_API_KEY"
{
"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Мои покупки
Ваши собственные покупки в качестве покупателя, от новых к старым — проверяйте лицензии и окна загрузки в ваших инструментах. С разбивкой по страницам.
| Параметр | Тип | Описание |
|---|---|---|
| status необязательно | string | active | refunded | cancelled | held |
| per_page необязательно | integer | по умолчанию 25, максимум 50 |
| page необязательно | integer | номер страницы |
curl "https://sellmycode.net/api/purchases?status=active" \ -H "Authorization: Bearer YOUR_API_KEY"
{
"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. При необходимости включите историю выписок.
| Параметр | Тип | Описание |
|---|---|---|
| statements необязательно | boolean | 1, чтобы включить недавние выписки |
| wallet необязательно | string | balance | store_credit — фильтровать выписки по кошельку |
| per_page необязательно | integer | по умолчанию 25, максимум 50 |
curl "https://sellmycode.net/api/balance?statements=1&wallet=balance" \ -H "Authorization: Bearer YOUR_API_KEY"
{
"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.
| Параметр | Тип | Описание |
|---|---|---|
| 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'
{
"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"
}
}
GETПубличный каталог
Просматривайте и ищите по всему утвержденному каталогу — открыто для ЛЮБОГО аккаунта, покупателя или автора. Создавайте трекеры цен, витрины портфолио или панели категорий. Возвращает только данные витрины: цены для покупателей (включая скидки), рейтинги и количество продаж — никогда файлы, ссылки для скачивания или идентификаторы покупателей.
| Параметр | Тип | Описание |
|---|---|---|
| search необязательно | string | полнотекстовый поиск по названиям и тегам товаров |
| category необязательно | string | слаг категории (см. конечную точку категорий) |
| sort необязательно | string | latest (по умолчанию) | popular | rating |
| per_page необязательно | integer | по умолчанию 25, максимум 50 |
| page необязательно | integer | номер страницы |
curl "https://sellmycode.net/api/catalog/items?search=woocommerce&sort=popular" \ -H "Authorization: Bearer YOUR_API_KEY"
{
"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"
}
]
}
Полная общедоступная информация об одном утвержденном товаре — поля списка плюс описание, предварительный просмотр медиа, список включенных файлов и фреймворк.
curl https://sellmycode.net/api/catalog/items/123 \ -H "Authorization: Bearer YOUR_API_KEY"
Все категории с количеством утвержденных товаров — используйте слаг в качестве фильтра категорий выше.
{
"status": "success",
"categories": [
{ "id": 4, "name": "WordPress", "slug": "wordpress", "url": "https://sellmycode.net/categories/wordpress", "items_count": 128 }
]
}
Активация лицензии
Для программного обеспечения, обращающегося к серверу — скриптов PHP, настольных или мобильных приложений. Ваша установка отправляет код покупки и свой домен; мы подтверждаем лицензию и считаем, на скольких сайтах она работает, чтобы вам не пришлось размещать собственный сервер лицензирования.
| Параметр | Тип | Описание |
|---|---|---|
| purchase_code Обязательно | string | код, полученный покупателем |
| domain Обязательно | string | сайт, на котором работает программное обеспечение; протокол, www и путь удаляются автоматически |
| nonce Обязательно | string | 16–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_token | string | deactivate 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.
5Uk3p1+J0PXzJkwS4wVFvTN9GcHpJzX5ClRI8zlI0yQ=
Key and signature format
| Товар | Формат |
|---|---|
| Algorithm | Ed25519 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. |
| Signature | the raw 64-byte Ed25519 signature, standard Base64 with padding (88 characters) |
| Signed message | the 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 |
| kid | short string naming the key that signed this answer; pick the matching public key by it |
| Timestamps | ISO 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:
-----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.
{
"payload": "{\"status\":\"success\",\"code\":\"activated\", … }",
"signature": "base64 Ed25519 signature of payload",
"kid": "k1",
"alg": "Ed25519"
}
{
"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 | Тип | Описание |
|---|---|---|
| payload | string | the signed JSON document, as a string |
| signature | string | Base64 Ed25519 signature of payload |
| kid | string | which key signed it |
| alg | string | always "Ed25519" |
Payload of a successful verify (HTTP 200, code "valid"). activate returns the same fields:
| Field | Тип | Описание |
|---|---|---|
| status | string | "success" |
| code | string | verify: "valid" · activate: "activated" or "already_activated" |
| domain | string | the domain as we stored it: lower case, no scheme, no www, no path |
| item.id | integer | item id on SellMyCode — check it is YOUR item, so a code bought for another product is not accepted |
| item.name | string | item title |
| item.version | string | latest published version |
| item.url | string | item page |
| license_type | string | "Regular" | "Extended" |
| purchased_at | string | ISO 8601, UTC |
| activation_limit | integer | null | sites this licence may run on; null = unlimited |
| activations_used | integer | sites currently active |
| updates.active | boolean | whether the buyer may still download updates — informational, never a reason to stop working |
| updates.until | string | null | end of the update period, ISO 8601 UTC; null = no end date |
| updates.renew_url | string | null | where to renew, set only once updates have ended |
| grace_until | string | run on this answer until this time if we cannot be reached (currently 7 days ahead) |
| nonce | string | the nonce you sent — must match |
| issued_at | string | when we signed it, ISO 8601 UTC |
| activation_token | string | null | activate only: 48 hex characters on "activated"; null on "already_activated" |
| hint | string | activate 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).
Key rotation
Every answer names its key in kid. GET /api/license/public-key lists every key with a status:
| status | Значение |
|---|---|
| active | signs every answer now — exactly one key at a time |
| next | announced, not signing yet — add it to your software now |
| retired | no longer signs; harmless to keep, never needed again |
| revoked | its secret may have leaked — remove it and reject answers signed with it |
- 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.
- 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.
- 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.
- A kid is never reused.
What your software must do
- Send a fresh random nonce with every call.
- Verify the signature over the payload string with the public key. If it fails, ignore the answer completely.
- Decode the payload and check that its nonce equals the one you sent.
- On activate, store activation_token — it is shown once and is the only way to deactivate this site through the API.
- 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_code | 403 | неизвестный, возвращенный или аннулированный код покупки |
| activation_limit_reached | 403 | все разрешенные сайты уже используются — сначала деактивируйте один из них |
| not_activated | 403 | этот домен никогда не активировался (или был освобожден) |
| invalid_activation_token | 403 | deactivate: the token does not belong to this site |
| invalid_request | 400 | a parameter is missing or malformed — most often the nonce |
| invalid_domain | 400 | не удалось разобрать домен |
| rate_limited | 429 | slow down and retry after retry_after seconds — not a licence problem, keep your cached state |
| signing_unavailable | 503 | temporary, and the only unsigned answer — keep your cached state |
Обновления товаров (Toolkit)
Позвольте сайту WordPress поддерживать купленные здесь темы и плагины в актуальном состоянии. Сообщите нам, какие пакеты установлены, получите список тех, у которых есть более новая версия, и скачайте ZIP-архив для установки. Это то, что использует плагин SellMyCode Toolkit — эти конечные точки нужны вам, только если вы создаете собственный механизм обновлений.
Используете WordPress? Пропустите код
Плагин SellMyCode Toolkit уже подключает эти эндпоинты к обычным экранам обновлений «Темы» и «Плагины». Установите его, вставьте токен, и купленные товары будут обновляться так же, как и всё остальное в WordPress.
POSTПроверить наличие обновлений
| Параметр | Тип | Описание |
|---|---|---|
| packages | array | установленные пакеты, до 100 за один запрос |
| packages[].slug | string | название папки темы или плагина |
| packages[].type | string | theme | plugin |
| packages[].version необязательно | string | текущая установленная версия |
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"}]}'
{
"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Скачать установочный пакет
Отвечает перенаправлением 302 на временный URL-адрес хранилища (действителен несколько минут). Следуйте перенаправлениям и сохраните ZIP-архив — он содержит только тему или плагин, готовые к установке.
curl -L -o update.zip "https://sellmycode.net/api/updates/download/123" \ -H "Authorization: Bearer YOUR_API_KEY"
| Код ошибки | HTTP | Значение |
|---|---|---|
| not_purchased | 403 | этот аккаунт не покупал данный товар |
| update_window_expired | 403 | период поддержки обновлений закончился — renew_url включен в ответ |
| no_installable_file | 404 | автор не предоставил установочный пакет для этого товара |
| item_removed | 410 | товар больше недоступен |
Вебхуки для продаж
Вместо того чтобы постоянно запрашивать конечную точку продаж, зарегистрируйте 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-Signature | t=<unix>,v1=<hex> — Подпись HMAC-SHA256 (см. ниже) |
{
"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