API Documentation

Integrate your applications with the marketplace: read your account and items, pull your sales for custom dashboards, and verify buyer purchase codes from your own license server.

Base URL: https://sellmycode.net/api Format: JSON Rate limit: 60 req/min
Quick check
curl https://sellmycode.net/api

Authentication

Every request needs a personal access token. Create named, scoped tokens in your workspace under Settings → API Key — each app gets its own token with only the permissions it needs, and you can revoke any of them at any time.

account:read items:read sales:read purchases:validate purchases:read balance:read catalog:read
Send the key in the Authorization header (recommended). The api_key query parameter is also accepted for backwards compatibility, but keys in URLs can leak into logs.
Recommended — Authorization header
curl https://sellmycode.net/api/account/details \
  -H "Authorization: Bearer YOUR_API_KEY"
Alternative — X-Api-Key header
curl https://sellmycode.net/api/account/details \
  -H "X-Api-Key: YOUR_API_KEY"

Errors & Limits

CodeMeaning
200success
400Validation error — a required parameter is missing or malformed
401Invalid or missing API key
404Resource not found (also returned for an invalid purchase code)
429Rate limit exceeded — wait and retry (60 requests per minute)
Error shape
{
  "status": "error",
  "msg": "Invalid request"
}

GETAccount Details

Returns the profile of the account that owns the API key.

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

GETAll Items

All of your approved items, newest first. Authors only.

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

GETSingle Item

One of your approved items by its numeric ID.

GET/api/items/item?item_id={id}
ParameterTypeDescription
item_id RequiredintegerThe item ID (shown in your workspace item list)
Request
curl "https://sellmycode.net/api/items/item?item_id=123" \
  -H "Authorization: Bearer YOUR_API_KEY"

GETSales

Your sales, newest first — build revenue dashboards or sync orders into your own tools. Paginated.

GET/api/sales
ParameterTypeDescription
from optionaldateY-m-d — only sales on or after this date
to optionaldateY-m-d — only sales on or before this date
item_id optionalintegerfilter by one of your items
status optionalstringactive | refunded | cancelled | held
per_page optionalintegerdefault 25, max 50
page optionalintegerpage number
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"
    }
  ]
}

GETMy Purchases

Your own purchases as a buyer, newest first — check licenses and download windows from your tooling. Paginated.

GET/api/purchases
ParameterTypeDescription
status optionalstringactive | refunded | cancelled | held
per_page optionalintegerdefault 25, max 50
page optionalintegerpage number
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"
    }
  ]
}

GETBalance

A read-only snapshot of your wallets: withdrawable earnings, earnings still in the clearing period, and Store Credit. Optionally include your statement history.

GET/api/balance
ParameterTypeDescription
statements optionalboolean1 to include recent statements
wallet optionalstringbalance | store_credit — filter statements by wallet
per_page optionalintegerdefault 25, max 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"
    }
  ]
}

POSTPurchase Validation

Verify a purchase code a buyer gives you — the core of any license system. Returns the purchase details when the code belongs to one of YOUR items and is still active; otherwise 404.

POST/api/purchases/validation
ParameterTypeDescription
purchase_code RequiredstringThe code from the buyer (shown on their Purchases page and license certificate)
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"
  }
}
A 404 is returned for BOTH an unknown code and a refunded/blocked purchase — treat any non-200 as "license invalid".

GETPublic Catalog

Browse and search the whole approved catalog — open to EVERY account, buyer or author. Build price trackers, portfolio showcases or category dashboards. Returns storefront data only: buyer-facing prices (discounts included), ratings and sales counts — never files, download links or buyer identities.

GET/api/catalog/items
ParameterTypeDescription
search optionalstringfull-text search across item names and tags
category optionalstringa category slug (see the categories endpoint)
sort optionalstringlatest (default) | popular | rating
per_page optionalintegerdefault 25, max 50
page optionalintegerpage number
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}

Full public detail of one approved item — the list fields plus description, media previews, included files list and framework.

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

All categories with their approved-item counts — use the slug as the category filter above.

Response 200
{
  "status": "success",
  "categories": [
    { "id": 4, "name": "WordPress", "slug": "wordpress", "url": "https://sellmycode.net/categories/wordpress", "items_count": 128 }
  ]
}
Any registered account can create a token with the catalog:read permission — you do not need to be an author or have made a purchase.

Licence Activation

For software that phones home — PHP scripts, desktop or mobile apps. Your installation sends the purchase code and its domain; we confirm the licence and count how many sites it runs on, so you do not have to host a licensing server yourself.

These three endpoints need NO API token — the purchase code is the credential, so the code you ship to buyers can call them directly. They are rate limited to 60 calls per 10 minutes per IP.
The right to USE the software never expires. When update support ends, verify still returns valid — only the updates block reports that renewal is due. An expired update period must never break a live site.
POST/api/license/activate
POST/api/license/verify
POST/api/license/deactivate
ParameterTypeDescription
purchase_codestringthe code the buyer received
domainstringthe site running the software; scheme, www and path are stripped automatically
Request
curl "https://sellmycode.net/api/license/activate" \
  -H "Content-Type: application/json" \
  -d '{"purchase_code":"d71c21a3-...","domain":"customer-site.com"}'
Response 200
{
  "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" }
}
Error codeHTTPMeaning
invalid_code403unknown, refunded or cancelled purchase code
activation_limit_reached403every allowed site is in use — deactivate one first
not_activated403this domain has never been activated (or was released)
invalid_domain400the domain could not be parsed
rate_limited429too many calls from this IP
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. Buyers release their own sites under Workspace → Licenses, and you can revoke one from the same page.

Item Updates (Toolkit)

Let a WordPress site keep the themes and plugins it bought here up to date. Tell us which packages are installed, get back the ones with a newer version, then pull the installable ZIP. This is what the SellMyCode Toolkit plugin uses — you only need these endpoints if you are building your own updater.

Requires a token with the updates:read permission. Updates are served while the update-support period of your purchase is still running; after it ends we still tell you a newer version exists, but the file is no longer available until you renew.

On WordPress? Skip the code

The SellMyCode Toolkit plugin already wires these endpoints into the normal Themes and Plugins update screens. Install it, paste a token, and purchased items update like anything else in WordPress.

POSTCheck for updates

POST/api/updates/check
ParameterTypeDescription
packagesarrayinstalled packages, up to 100 per call
packages[].slugstringthe theme or plugin folder name
packages[].typestringtheme | plugin
packages[].version optionalstringthe version currently installed
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
    }
  ]
}
Only items you actually bought are returned — anything else installed on the site is ignored, so it is safe to send your whole plugin list.

GETDownload the installable package

GET/api/updates/download/{id}

Responds with a 302 redirect to a short-lived storage URL (valid a few minutes). Follow redirects and save the ZIP — it contains only the theme or plugin, ready to install.

Request
curl -L -o update.zip "https://sellmycode.net/api/updates/download/123" \
  -H "Authorization: Bearer YOUR_API_KEY"
Error codeHTTPMeaning
not_purchased403this account did not buy the item
update_window_expired403update support has ended — renew_url is included in the response
no_installable_file404the author has not provided an installable package for this item
item_removed410the item is no longer available
These endpoints are limited to 60 requests per minute. A site normally needs only a few calls per day — check once, then download what changed.

Sale Webhooks

Instead of polling the sales endpoint, register an HTTPS URL and we POST a signed JSON event to it the moment something happens. Manage endpoints in your workspace under Settings → API Key → Webhooks (up to 5, each with its own signing secret shown once at creation).

EventSent when
sale.createdOne of your items, bundles or courses is sold
sale.refundedOne of your sales is refunded or cancelled — revoke the license on your side

Delivery

Each delivery is an HTTP POST with a JSON body. Respond with any 2xx status within 10 seconds; do heavy work asynchronously. Failed deliveries are retried 3 times (after 1 minute, 15 minutes and 1 hour). After 10 consecutive failures — or a 410 Gone response — the endpoint is disabled automatically and you are notified.

HeaderDescription
X-SMC-EventThe event name, e.g. sale.created
X-SMC-DeliveryUnique delivery ID (UUID) — deduplicate retries with it
X-SMC-Signaturet=<unix>,v1=<hex> — HMAC-SHA256 signature (see below)
Example payload — 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"
  }
}

Verifying the signature

Compute HMAC-SHA256 over the string "timestamp.rawBody" with your signing secret and compare it (constant-time) to the v1 value. Reject deliveries whose timestamp is older than ~5 minutes to prevent replays.

$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
Endpoints must be public HTTPS URLs. Deliveries never follow redirects. Use the X-SMC-Delivery ID to deduplicate — a retry after a timeout can arrive even though you already processed the original.