PVA Markets API

PVA Markets API

Integrate with PVA Markets programmatically. Browse the catalogue, buy accounts and manage orders through our RESTful API — the same catalogue and the same balance the storefront uses, without a browser in the middle.

Base URLhttps://pvamarkets.com/api/v1
Apply for API

Introduction

The PVA Markets Reseller API is a RESTful API that uses JSON for request and response bodies. Every endpoint is prefixed with /api/v1 and served over HTTPS only — plain HTTP requests are refused rather than redirected, so a misconfigured client never puts a key on the wire in the clear.

Response format

All responses follow the same JSON envelope, with success, message and data fields. Read success before data — an error body carries the same three keys, so branching on the HTTP status alone is enough but branching on success is clearer.

Standard response structure
{
  "success": true,
  "message": "Operation successful",
  "data": { ... }
}

Money is returned as a decimal string, never a float. Accounts on this marketplace go down to $0.004 apiece, so a price can carry up to six decimal places — parse it with a decimal type and format it yourself. Quantities are integers, and every timestamp is ISO 8601 in UTC.

Sandbox

Every endpoint accepts a X-Sandbox: true header. Sandbox requests read the live catalogue but never move stock or funds, and purchases return placeholder credentials. Use it to shake out an integration before the first real order.

Authentication

The API uses API key authentication. Generate a key in your reseller cabinet and send it in the X-API-Key header on every request. There is no session, no OAuth dance and no token refresh — the key is the whole credential, so treat it like a password and keep it server-side.

API key header

Include the header in all requests. A missing or malformed key returns 401; a valid key that has been revoked or belongs to a suspended account returns 403.

Authenticated request
curl https://pvamarkets.com/api/v1/user/profile \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Accept: application/json"

Keys can be restricted to a set of IP addresses and to a scope — read for catalogue access, trade for purchases and balance movements. A key with the read scope calling POST /orders returns 403 with the forbidden code, which is the failure you want during development rather than a surprise purchase.

Error Codes

Errors reuse the standard envelope with success set to false. The HTTP status carries the class of failure and data.code carries the specific one, so a client can branch on the status and log the code.

Error response
{
  "success": false,
  "message": "Not enough stock to fill this order",
  "data": {
    "code": "insufficient_stock",
    "available": 12
  }
}
StatusCodeMeaning
400invalid_requestMalformed JSON, or a parameter of the wrong type.
401unauthorizedThe X-API-Key header is missing or the key does not exist.
403forbiddenThe key is revoked, out of scope, or calling from an unlisted IP.
404not_foundNo listing, order or category with that identifier.
409insufficient_stockStock dropped below the requested quantity before the order was placed.
409insufficient_fundsThe balance does not cover the order total.
422validation_failedThe body parsed but a field failed validation; data.errors lists them per field.
429rate_limitedToo many requests. See Rate Limiting below.
500server_errorSomething broke on our side. Safe to retry with backoff.

Retry only what is safe

409 and 422 will not resolve on their own — refetch the listing or fix the body first. 429 and 5xx are the only statuses worth retrying, and only with exponential backoff.

Rate Limiting

Requests are counted per API key in a rolling one-minute window. Catalogue reads get a generous allowance; purchases are held to a much tighter one because each of them moves stock and funds.

ScopeLimitApplies to
Catalogue reads600 / minuteCategories, listings and listing detail.
Account reads120 / minuteProfile, balance and order history.
Purchases30 / minutePOST /orders and POST /promo/validate.
Rate limit headers
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 574
X-RateLimit-Reset: 1739452800

Handling 429

A throttled response carries Retry-After in seconds. Wait it out rather than retrying immediately — repeated hammering after a 429 is what gets a key suspended.

Polling the catalogue every few seconds is the usual way resellers burn through the limit. Listings carry an updated_at field and the collection endpoints accept updated_since, so pull the delta instead of the whole catalogue.

User

Get Profile

GET/user/profile

Returns the reseller account the API key belongs to, along with the scope and IP allowlist the key itself carries. Useful as a health check on startup — it is the cheapest call that proves a key is live.

200 OK
{
  "success": true,
  "message": "Profile retrieved",
  "data": {
    "id": 48213,
    "email": "reseller@example.com",
    "username": "example_reseller",
    "status": "active",
    "created_at": "2024-11-02T09:14:38Z",
    "api_key": {
      "label": "production",
      "scope": ["read", "trade"],
      "allowed_ips": ["203.0.113.24"]
    }
  }
}

Update Profile

PATCH/user/profile

Updates the contact details on the account. Send only the fields you are changing — omitted fields are left alone. The email address and the balance cannot be changed here; both go through the cabinet.

Parameters

NameTypeDescription
usernamestringDisplay name, 3–32 characters.
telegramstringTelegram handle for order notifications, with or without the leading @.
notify_on_orderbooleanWhether to send a notification for each API order.
Request body
{
  "username": "example_reseller",
  "telegram": "@example_reseller",
  "notify_on_order": true
}
200 OK
{
  "success": true,
  "message": "Profile updated",
  "data": {
    "id": 48213,
    "username": "example_reseller",
    "telegram": "@example_reseller",
    "notify_on_order": true
  }
}

Get Balance

GET/user/balance

Returns the wallet the API buys from. Orders are settled from this balance, so check it before a batch rather than discovering the shortfall as an insufficient_funds error halfway through.

200 OK
{
  "success": true,
  "message": "Balance retrieved",
  "data": {
    "currency": "USD",
    "available": "418.250000",
    "held": "12.400000",
    "updated_at": "2026-08-13T07:22:11Z"
  }
}

held covers orders that are placed but not yet delivered. It is not spendable, so budget against available.

Categories

List Categories

GET/categories

Returns every top-level category — Instagram, Facebook, Gmail, TikTok and the rest — with a live stock count and the lowest price currently offered in each. The tree changes rarely; cache it and refresh daily.

Parameters

NameTypeDescription
with_countsbooleanInclude stock and min_price per category. Defaults to true.
200 OK
{
  "success": true,
  "message": "Categories retrieved",
  "data": [
    {
      "id": 3,
      "slug": "instagram",
      "name": "Instagram",
      "stock": 18420,
      "min_price": "0.140000"
    },
    {
      "id": 5,
      "slug": "gmail",
      "name": "GMail",
      "stock": 9317,
      "min_price": "0.004000"
    }
  ]
}

Subcategories

GET/categories/{slug}/subcategories

Returns the subcategories under one category — Softreg, Aged, With Followers and so on. The slug is the same one the storefront uses in its URLs, so /catalog/instagram/pva maps to the pva subcategory here.

Parameters

NameTypeDescription
slugrequiredstringPath parameter. The parent category slug, e.g. instagram.
200 OK
{
  "success": true,
  "message": "Subcategories retrieved",
  "data": [
    {
      "id": 31,
      "slug": "pva",
      "name": "Softreg",
      "stock": 6204,
      "min_price": "0.140000"
    },
    {
      "id": 32,
      "slug": "s-otlezhkoj",
      "name": "Aged",
      "stock": 2881,
      "min_price": "0.910000"
    }
  ]
}

Listings

Browse Listings

GET/listings

The catalogue itself, paginated and filterable. Out of stock means out of stock: a listing with no stock returns stock 0 and no price, never a fallback figure from elsewhere in the catalogue.

Parameters

NameTypeDescription
categorystringCategory slug, e.g. facebook.
subcategorystringSubcategory slug, e.g. aged1.
tagsarray<string>Repeatable. Matches listings carrying all given tags, e.g. tags=2fa&tags=email.
price_mindecimalLowest per-item price to include.
price_maxdecimalHighest per-item price to include.
in_stockbooleanDrop sold-out listings. Defaults to true.
updated_sincestringISO 8601 timestamp. Returns only listings changed since then.
sortstringprice_asc, price_desc, stock_desc or newest. Defaults to newest.
pageinteger1-based page number. Defaults to 1.
per_pageinteger1–100. Defaults to 50.
Example request
curl -G https://pvamarkets.com/api/v1/listings \
  -H "X-API-Key: YOUR_API_KEY" \
  -d category=instagram \
  -d subcategory=pva \
  -d price_max=0.5 \
  -d per_page=2
200 OK
{
  "success": true,
  "message": "Listings retrieved",
  "data": {
    "items": [
      {
        "id": 90142,
        "slug": "instagram-softreg-eu-2fa",
        "title": "Instagram Softreg | EU IP | 2FA included",
        "category": "instagram",
        "subcategory": "pva",
        "price": "0.320000",
        "stock": 1840,
        "min_quantity": 10,
        "tags": ["2fa", "email", "eu"],
        "updated_at": "2026-08-13T06:58:02Z"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 2,
      "total": 87,
      "total_pages": 44
    }
  }
}

Listing Detail

GET/listings/{id}

One listing in full, including the seller's description and the format the credentials are delivered in. Read format before you buy in volume — it is what your parser has to handle.

Parameters

NameTypeDescription
idrequiredintegerPath parameter. The listing id.
200 OK
{
  "success": true,
  "message": "Listing retrieved",
  "data": {
    "id": 90142,
    "slug": "instagram-softreg-eu-2fa",
    "title": "Instagram Softreg | EU IP | 2FA included",
    "description": "Registered from EU residential IPs. Email included, 2FA key included.",
    "category": "instagram",
    "subcategory": "pva",
    "price": "0.320000",
    "stock": 1840,
    "min_quantity": 10,
    "format": "login:password:email:email_password:2fa_key",
    "warranty_hours": 24,
    "tags": ["2fa", "email", "eu"],
    "seller": { "id": 771, "rating": 4.8 },
    "updated_at": "2026-08-13T06:58:02Z"
  }
}

Stock is a snapshot, not a reservation. Between this call and POST /orders another buyer can take the last of it, which is what the 409 insufficient_stock response is for.

Orders

Purchase

POST/orders

Buys a quantity from one listing and settles it against your balance. Requires a key with the trade scope. Delivery is immediate for in-stock listings, and the accounts come back on the same response.

Parameters

NameTypeDescription
listing_idrequiredintegerThe listing to buy from.
quantityrequiredintegerHow many accounts. Must be at least the listing's min_quantity.
promo_codestringOptional. Validate it first with POST /promo/validate.
idempotency_keyrequiredstringA UUID you generate. Replaying the same key returns the original order instead of buying twice.
Request body
{
  "listing_id": 90142,
  "quantity": 50,
  "promo_code": "SUMMER10",
  "idempotency_key": "5c9f2b74-1a3e-4d80-9f2a-7c1d0e6b8a41"
}
201 Created
{
  "success": true,
  "message": "Order completed",
  "data": {
    "order_id": 611904,
    "status": "delivered",
    "listing_id": 90142,
    "quantity": 50,
    "unit_price": "0.320000",
    "discount": "1.600000",
    "total": "14.400000",
    "balance_after": "403.850000",
    "format": "login:password:email:email_password:2fa_key",
    "items": [
      "user_ffa21:Pa55w0rd!:user_ffa21@rambler.ru:M4ilPass:JBSWY3DPEHPK3PXP"
    ],
    "created_at": "2026-08-13T07:31:44Z"
  }
}

Send idempotency_key on every purchase. A timeout tells you nothing about whether the order went through, and retrying with the same key is the only safe way to find out.

List Orders

GET/orders

Your order history, newest first. The items array is omitted here to keep the payload small — fetch a single order to get the credentials back.

Parameters

NameTypeDescription
statusstringpending, delivered, refunded or failed.
fromstringISO 8601 timestamp. Orders created at or after it.
tostringISO 8601 timestamp. Orders created before it.
pageinteger1-based page number. Defaults to 1.
per_pageinteger1–100. Defaults to 50.
200 OK
{
  "success": true,
  "message": "Orders retrieved",
  "data": {
    "items": [
      {
        "order_id": 611904,
        "status": "delivered",
        "listing_id": 90142,
        "title": "Instagram Softreg | EU IP | 2FA included",
        "quantity": 50,
        "total": "14.400000",
        "created_at": "2026-08-13T07:31:44Z"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 50,
      "total": 1284,
      "total_pages": 26
    }
  }
}

Order Detail

GET/orders/{id}

One order with the delivered credentials. Orders stay retrievable for 90 days, after which the items array is cleared and only the metadata remains — store what you buy on your own side.

Parameters

NameTypeDescription
idrequiredintegerPath parameter. The order id.
200 OK
{
  "success": true,
  "message": "Order retrieved",
  "data": {
    "order_id": 611904,
    "status": "delivered",
    "listing_id": 90142,
    "quantity": 50,
    "unit_price": "0.320000",
    "total": "14.400000",
    "format": "login:password:email:email_password:2fa_key",
    "items": [
      "user_ffa21:Pa55w0rd!:user_ffa21@rambler.ru:M4ilPass:JBSWY3DPEHPK3PXP"
    ],
    "warranty_expires_at": "2026-08-14T07:31:44Z",
    "created_at": "2026-08-13T07:31:44Z"
  }
}

A replacement claim inside the warranty window still goes through a support ticket; the API does not issue refunds.

Promo

Validate Code

POST/promo/validate

Checks a promo code against a listing and quantity before you commit to the purchase. It reserves nothing — the same code still has to be sent on POST /orders, where it is re-checked.

Parameters

NameTypeDescription
coderequiredstringThe promo code, case-insensitive.
listing_idintegerScopes the check to one listing. Some codes are category-bound.
quantityintegerUsed to check the code's minimum order value.
Request body
{
  "code": "SUMMER10",
  "listing_id": 90142,
  "quantity": 50
}
200 OK
{
  "success": true,
  "message": "Promo code is valid",
  "data": {
    "code": "SUMMER10",
    "valid": true,
    "discount_type": "percent",
    "discount_value": "10",
    "discount_amount": "1.600000",
    "total_before": "16.000000",
    "total_after": "14.400000",
    "expires_at": "2026-08-31T23:59:59Z"
  }
}

An unknown or expired code returns 200 with valid set to false and a reason, not a 404 — that keeps code checking off your error path.

Ready to integrate?

API access is granted per account. Open a ticket with your use case and the volume you expect, and we will issue a key with the scope you need — read-only first if you would rather test against the sandbox before trading.