# VoteTracker Shop Economy Guide

Voters earn coins by voting for your project, and they spend those coins in a shop you run in
a server you manage, usually your community or support server. A **developer item** is an item
VoteTracker sells on your behalf and never interprets: it takes the coins, tells your backend
about the purchase, and waits for you to confirm you delivered the goods.

Request and response schemas for each endpoint live in the [API Reference](/docs).

**Official SDK:** [`@votetracker/sdk`](https://www.npmjs.com/package/@votetracker/sdk)
(TypeScript/JavaScript) verifies webhook deliveries and wraps the endpoints below.
Source at [votetracker-bot/sdks](https://github.com/votetracker-bot/sdks).

## Overview

1. You create a shop item of kind `DEVELOPER` on the dashboard shop page of your server,
   optionally with an `external_config` payload of your own design (a SKU, a tier id, whatever
   your backend needs).
2. A member buys it. The coins are debited immediately and the purchase waits for your confirmation.
3. VoteTracker forwards a `PURCHASE_CREATED` webhook to your configured destination.
4. Your backend grants the goods and calls the deliver endpoint.
5. If you cannot deliver, call the refund endpoint instead. The buyer gets the coins back and the
   item is restocked.
6. If nobody acks within the item's fulfillment window, VoteTracker refunds the purchase itself
   and sends `PURCHASE_REFUNDED`.

Configure forwarding and API keys in the
[Vote Tracker Dashboard](https://votetracker.bot/dashboard/api).

## Events

Both events follow the shape every forwarded event uses: a top-level `event_type`, `occurred_at`
and `data`. Delivery, retries, signatures and delivery logs are documented in the
[Webhook Forwarding Guide](/docs/forwarding). Verify every request with `X-VT-Signature` before
acting on it, exactly as described there.

### PURCHASE_CREATED

Fires for every purchase in the shop, not only developer items. `requires_fulfillment` tells you
which ones are yours to resolve. `external_config` is your own JSON, echoed back verbatim, and is
absent when the item has none.

```json
{
  "event_type": "PURCHASE_CREATED",
  "occurred_at": "2026-08-13T18:30:00Z",
  "data": {
    "purchase_id": "pur_2k5k5f2nY1Rk08Jxq8Nqf2Q",
    "item": {
      "public_id": "itm_9f1c2d3e4a5b6c7d8e9f0a1",
      "name": "Gold rank",
      "kind": "DEVELOPER",
      "external_config": {
        "sku": "rank.gold",
        "duration_days": 30
      }
    },
    "buyer": {
      "user_platform_id": "223456683337318402"
    },
    "price_paid": 500,
    "currency": {
      "name": "coins",
      "emoji": "🪙"
    },
    "guild_id": "706877503645679676",
    "requires_fulfillment": true,
    "fulfill_deadline_at": "2026-08-14T18:30:00Z",
    "purchased_at": "2026-08-13T18:30:00Z"
  }
}
```

Field notes:

- `item.kind` is one of `ROLE_PERMANENT`, `ROLE_TEMP`, `STREAK_FREEZE`, `CUSTOM`, `DEVELOPER`.
- `requires_fulfillment` is true only for developer items, the ones your backend must resolve.
- `fulfill_deadline_at` is the moment the automatic refund runs if nothing acked the purchase.
  It is present only on developer purchases and absent for every other kind.
- `currency.name` falls back to `coins` when the server never renamed its currency.
  `currency.emoji` is absent when unset.

### PURCHASE_REFUNDED

Fires when a pending purchase resolves as a refund. Use it to revoke anything you granted late,
and to keep your own records in step.

```json
{
  "event_type": "PURCHASE_REFUNDED",
  "occurred_at": "2026-08-14T18:30:00Z",
  "data": {
    "purchase_id": "pur_2k5k5f2nY1Rk08Jxq8Nqf2Q",
    "item": {
      "public_id": "itm_9f1c2d3e4a5b6c7d8e9f0a1",
      "name": "Gold rank"
    },
    "buyer": {
      "user_platform_id": "223456683337318402"
    },
    "price_refunded": 500,
    "reason": "FULFILLMENT_TIMEOUT",
    "guild_id": "706877503645679676",
    "refunded_at": "2026-08-14T18:30:00Z"
  }
}
```

`reason` is one of:

- `FULFILLMENT_TIMEOUT`: the fulfillment window expired with no ack.
- `DEVELOPER_REFUND`: you called the refund endpoint, or a server manager refunded the claim
  from the dashboard.
- `ROLE_GRANT_BLOCKED`: a role purchase could not be granted. Not a developer item, included
  because the event covers every refund in the shop.

The refunded event carries no `item.kind`. Read the purchase if you need it.

## The Fulfillment Contract

A developer purchase is a promise with a deadline. You pick the window per item when creating
it (1 hour, 6 hours, 24 hours by default, 3 days or 7 days). Within that window you must either
deliver or refund.

- Deliver as soon as the goods are granted, not before. The dashboard shows the buyer a pending
  claim until you ack.
- Refund whenever you cannot deliver. The coins go back and the item is restocked.
- Do nothing and VoteTracker refunds for you at `fulfill_deadline_at`, then sends
  `PURCHASE_REFUNDED` with reason `FULFILLMENT_TIMEOUT`. A deliver call after that returns `409`.
- Webhook deliveries retry, so treat `purchase_id` as an idempotency key on your side. Both
  endpoints are safe to call again: repeating the action that already happened returns `200`.
- If you missed webhooks entirely, reconcile with the list endpoint filtered to
  `status=PENDING_CLAIM&kind=DEVELOPER`.

Claims can also be resolved from the dashboard, by you or anyone else managing the shop. When
both sides act on the same claim, the first action counts and the second returns a `409`.

## Endpoints

The purchase endpoints live under `/api/v1/integrations/{integrationId}/purchases`, gated by
the `purchases:read` and `purchases:write` scopes. Schemas, parameters, error responses and
rate limits live in the [API Reference](/docs). Deliver and refund accept only `DEVELOPER`
purchases, the rest of the shop is resolved from the dashboard.
