New — the DCMS Voluntary Code for prize-draw operators is now in effect. Here's what independent draw verification means for UK competitions.Read the guide →
Developer docs

Technical integration guide

Everything needed to wire Verified Draws into a site: connect the plugin with a site token, register each competition against a sealed seed commitment, allocate provably-fair ticket numbers instantly per order, reveal the seed at close, pull results back through the endpoint contract, and render the stored verify links in your own theme.

Install & connect the plugin (site token)

Download the plugin zip from the /api/plugin/download endpoint, then upload it under Plugins → Add New → Upload in WordPress. Open Verified Draws → Settings and run the connection self-test.

The site key (site token) is generated locally on first use. It is sent only server-to-server over HTTPS as a Bearer token — never shown in a browser or placed in a URL. Each competition is registered once against POST /api/plugin/competition, which is idempotent per (site, competitionKey).

The integration contract: register → allocate → close

Three calls run a competition end to end. All are authenticated with the Bearer site token and all are idempotent, so retries are always safe.

  • Register POST /api/plugin/competition. Verified Draws mints a secret ticket seed server-side and answers ready immediatelywith the competition's public publicCode and its ticketSeedCommitment — the sha256 of that seed (scheme vd-committed-v2). The seed itself is never returned; publishing its hash up front is what seals the number mapping before the first ticket sells.
  • Allocate POST /api/plugin/tickets. Assigns blocks of ticket numbers on demand, instantly — there is no waiting on a randomness round. Send { count } to allocate the next block (an optional Idempotency-Key header makes retries replay the same window), or { positions } to re-issue freed positions after a refund.
  • Close POST /api/plugin/competition/close. Halts the competition and reveals the seed. From that moment the competition's verify page returns the seed plus three self-contained scripts (Node, Python, PHP) any buyer can run fully offline to recompute their exact numbers.
curl — register
curl -X POST https://verifieddraws.uk/api/plugin/competition \
  -H "Authorization: Bearer $VD_SITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"competitionKey":"woo:1234","rangeMin":1,"rangeMax":5000,"prefix":"A","suffix":"-UK","padWidth":4,"label":"Summer Cash Draw"}'
Response — ready immediately, commitment only
{
  "status": "ready",
  "publicCode": "VCODE-7Q2F",
  "ticketSeedCommitment": "9f1a4c…e2b7 (sha256 of the secret seed)",
  "rangeMin": 1,
  "rangeMax": 5000,
  "prefix": "A",
  "suffix": "-UK",
  "padWidth": 4
}

One optional register field, hasInstantWins (boolean), declares that the competition carries instant-win prizes. It is set by RaffleForge-hosted competitions — instant wins are a hosted-platform feature. The flag is monotonic: once registered as true it stays true across re-registration, so a later push can never quietly hide a declared instant-win section. Numbers-only integrations, including the WordPress plugin, omit it, and the competition's verify page then shows no instant-win section at all.

Competitions registered before this scheme used the legacy drand-v1 flow — a committed future drand round plus a secret salt, answering 202 pending until the round published. Those competitions still verify exactly as before; every new registration is vd-committed-v2 and is ready the instant it is created.

Push the roster

The final entrant roster is pushed to POST /api/ingest, authenticated with the Bearer site token. It sends only ticket values — numeric tickets only; a shared prefix or suffix like A001-UK is fine, but text or name rosters are rejected at ingest. It never sends payment details, addresses, or emails.

Ticket generation

Ticket numbers are assigned the moment an order is paid — allocation is instant, with no waiting on a future randomness round. The competition's secret seed is minted by Verified Draws at registration and never leaves the server; while the competition sells, only its sha256 commitment (ticketSeedCommitment) is public. Tickets are positions in a seed-keyed permutation over the number range, computed server-side by POST /api/plugin/tickets: deterministic, tamper-evident, and unpredictable while selling — the commitment published before the first sale seals the mapping, and the seed revealed at close proves it. Assignment is keyed on the order id (get-or-create / idempotent). When an order is refunded or cancelled, its contiguous block of ticket positions returns to a reuse pool and is reissued to the next purchase — so refunded numbers stay in play and the draw space stays tight, while assignment stays idempotent per order id.

PHP
$assignment = vd_assign_tickets(
    $competition_id, // WP post id of the registered competition
    $order_id,       // the buyer's order — the idempotency key
    $count           // how many tickets to assign
);

$assignment->tickets;        // e.g. ["A001-UK", "A002-UK"]
$assignment->start_position; // first block index for this order
$assignment->verify_url;     // /verify/order/<code>-<start>-<count>

The per-order verify link follows the format https://verifieddraws.uk/verify/order/<code>-<start>-<count>.

Per-order verify URL
https://verifieddraws.uk/verify/order/VCODE-7Q2F-0-3
What buyers see on /verify/order

Every per-order verify link resolves to one of two tiers, decided by the competition's lifecycle — plus an instant-win panel that appears only where it applies.

  • OPEN — while selling. The page is confirm-only: a buyer pastes the tickets they hold and gets a match verdict against the sealed order. The seed is never shown and numbers cannot be enumerated. The page also shows the ticketSeedCommitment published at registration, so anyone can record the commitment before the reveal.
  • CLOSED — after close. The seed is revealed. The page recomputes the buyer's exact numbers, checks that sha256(seed) matches the published commitment, and hands over three self-contained scripts (Node, Python, PHP) that recompute the numbers fully offline — trusting no one, not even Verified Draws.
  • Instant-win map. Shown only on competitions that actually have instant wins (RaffleForge-hosted): the published prize map is checked against its public commitment — green when the recomputation matches, red if the map is missing or has been altered. Numbers-only competitions, including everything run through the WordPress plugin, show no instant-win section at all.
Results pull-back endpoint contract

The endpoint is POST /api/plugin/results, authenticated with the Bearer site token. The request body takes three forms:

  • { "sourceId": "adapter:ref" } — a single competition. Returns { competition, draws } (404 if not found or not owned by this install).
  • { "sourceIds": ["...","..."] } — a batch over an explicit list. Returns { competitions: [ { competition, draws }, ... ] }.
  • empty / absent body {} — a batch over all of this install's competitions. Returns { competitions: [...] }. This is what the plugin's one-click "Pull verification results" button uses.
curl
curl -X POST https://verifieddraws.uk/api/plugin/results \
  -H "Authorization: Bearer $VD_SITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sourceId":"woo:1234"}'
curl
curl -X POST https://verifieddraws.uk/api/plugin/results \
  -H "Authorization: Bearer $VD_SITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
Response — { competition, draws }
{
  "competition": { "name": "Summer Cash Draw", "sourceId": "woo:1234" },
  "draws": [
    {
      "rollIndex": 0,
      "winner": { "number": 42, "ticket": "A042-UK" },
      "publicCode": "VCODE-7Q2F",
      "verifyUrl": "https://verifieddraws.uk/verify/VCODE-7Q2F",
      "verified": true,
      "revealedAt": "2026-06-13T20:14:00.000Z",
      "skipped": [ { "number": 7, "ticket": "A007-UK" } ]
    }
  ]
}

In the API response, skipped is an array of objects { number, ticket }: the unsold / generated numbers the draw skipped before landing on a sold ticket.

The _vd_results meta key

The plugin persists the pulled payload as a JSON string in post meta under the key _vd_results. One important nuance: when stored by the plugin, each draw's skipped is normalised to a string[] of full ticket strings — not objects.

_vd_results (stored JSON shape)
{
  "competition": { "name": "Summer Cash Draw", "sourceId": "woo:1234" },
  "draws": [
    {
      "rollIndex": 0,
      "winner": { "number": 42, "ticket": "A042-UK" },
      "publicCode": "VCODE-7Q2F",
      "verifyUrl": "https://verifieddraws.uk/verify/VCODE-7Q2F",
      "verified": true,
      "revealedAt": "2026-06-13T20:14:00.000Z",
      "skipped": [ "A007-UK", "A019-UK" ]
    }
  ]
}
Render the links in a custom theme template

The default path: automatic on-page display

Out of the box, the plugin renders the winning ticket and verify link on each competition (WooCommerce product) page the moment results are pulled — via woocommerce_after_single_product_summary on classic themes and a render_block filter on block themes. The operator controls this with the Show results on competition page toggle (option vd_show_results_on_competition, default on) under Verified Draws → Settings. Turn it off when you want to place the results yourself with one of the options below.

The easy path: the shortcode

Drop the [vd_results] shortcode into the page. An optional id attribute targets a specific post. The same markup is also auto-appended to the post content via the_content as a manual fallback.

Shortcode
[vd_results]

[vd_results id="1234"]

The custom path: read the two fields yourself

Read _vd_results from post meta, decode the JSON, and loop draws. For each draw there are exactly two fields you need to render anywhere in a custom theme:

  • The winning ticket draws[].winner.ticket (e.g. "A042-UK"). The drawn ticket string to show as the result.
  • The verify link draws[].verifyUrl (e.g. https://verifieddraws.uk/verify/VCODE-7Q2F). The public, shareable URL anyone can open to confirm the draw against the beacon.

In the PHP below those two fields are read as $draw['winner']['ticket'] and $draw['verifyUrl'].

PHP — custom theme template
<?php
$raw  = get_post_meta( get_the_ID(), '_vd_results', true );
$data = $raw ? json_decode( $raw, true ) : null;

if ( $data && ! empty( $data['draws'] ) ) :
    echo '<div class="vd-results">';
    foreach ( $data['draws'] as $draw ) :
        $ticket = esc_html( $draw['winner']['ticket'] ?? '' );
        $verify = esc_url( $draw['verifyUrl'] ?? '' );
        ?>
        <p class="vd-results__winner">
            Winning ticket: <strong><?php echo $ticket; ?></strong>
            <a href="<?php echo $verify; ?>" rel="nofollow noopener" target="_blank">Verify this draw</a>
        </p>
        <?php if ( ! empty( $draw['skipped'] ) ) : ?>
            <details>
                <summary>Unsold / generated tickets skipped</summary>
                <ul>
                <?php foreach ( (array) $draw['skipped'] as $sk ) : ?>
                    <li><?php echo esc_html( (string) $sk ); ?></li>
                <?php endforeach; ?>
                </ul>
            </details>
        <?php endif;
    endforeach;
    echo '</div>';
endif;
?>

The winner verify link points at /verify/<code> and the per-order ticket link at /verify/order/<code>-<start>-<count> — both are already public.

Build a Past Winners page in your theme

The single-page recipe above shows one competition's result on its own page. To build a site-wide “Past Winners” listing, run a WP_Query for every post that carries the _vd_results meta key, decode each stored payload, and render the competition name, the winning ticket (draws[].winner.ticket) and its verify link (draws[].verifyUrl). One query, one loop, every drawn competition on the site.

PHP — a Past Winners archive template
<?php
// Past Winners — list every competition that has a stored Verified Draws result.
$winners = new WP_Query( array(
    'post_type'      => 'product',          // your competition post type
    'posts_per_page' => -1,
    'meta_key'       => '_vd_results',
    'meta_compare'   => 'EXISTS',
    'orderby'        => 'date',
    'order'          => 'DESC',
) );

if ( $winners->have_posts() ) :
    echo '<ul class="vd-past-winners">';
    while ( $winners->have_posts() ) : $winners->the_post();
        $data = json_decode( get_post_meta( get_the_ID(), '_vd_results', true ), true );
        if ( empty( $data['draws'] ) ) { continue; }
        foreach ( $data['draws'] as $draw ) :
            $ticket = esc_html( $draw['winner']['ticket'] ?? '' );
            $verify = esc_url( $draw['verifyUrl'] ?? '' );
            ?>
            <li class="vd-past-winners__item">
                <span class="vd-past-winners__comp"><?php the_title(); ?></span>
                <span class="vd-past-winners__ticket">Winning ticket: <strong><?php echo $ticket; ?></strong></span>
                <a class="vd-past-winners__verify" href="<?php echo $verify; ?>" rel="nofollow noopener" target="_blank">Verify this draw</a>
            </li>
            <?php
        endforeach;
    endwhile;
    echo '</ul>';
    wp_reset_postdata();
endif;
?>

The meta_compare => 'EXISTS' clause selects only competitions that have already been drawn and pulled back, so the list stays clean. Each draws[].verifyUrl is a public, shareable link anyone can open to confirm the draw against the beacon.