Documentation menuServer snippets

Server snippets

Copy-paste verification helpers for curl, Node.js, Python, PHP, Go, Ruby, Java and C#, each with a timeout and fail-closed handling.

On this page

Every helper sends the token with a fixed hostname and action, uses a 3-second timeout, does not follow redirects, and returns "allowed" only when the answer is success: true. Replace example.com with your hostname and keep the secret in the BLOCKALT_SECRET environment variable.

curl #

Shell
curl -sS --max-time 3 https://blockalt.click/v1/siteverify \
  -H "Authorization: Bearer $BLOCKALT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"token":"<token>","hostname":"example.com","action":"signup"}'

Node.js with fetch #

No dependency, Node 18 or newer (also Bun and Deno):

blockalt.jsJavaScript
export async function verifyBlockAlt({ token, action, remoteip, idempotencyKey }) {
  try {
    const response = await fetch("https://blockalt.click/v1/siteverify", {
      method: "POST",
      redirect: "error",
      headers: {
        Authorization: `Bearer ${process.env.BLOCKALT_SECRET}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        token: typeof token === "string" ? token : "",
        hostname: "example.com", // fixed per deployment
        action, // fixed per route
        ...(remoteip ? { remoteip } : {}),
        ...(idempotencyKey ? { idempotencyKey } : {}),
      }),
      signal: AbortSignal.timeout(3000),
    });
    const result = await response.json();
    return { success: response.ok && result.success === true, reason: result.reason, id: result.id };
  } catch {
    return { success: false, reason: "service_unavailable" };
  }
}

Node.js with @blockalt/server #

ESM with TypeScript types and no runtime dependencies, for Node.js 20+ and Bun. Install version 0.3.0 straight from BlockAlt:

Shell
npm install https://blockalt.click/sdk/blockalt-server-0.3.0.tgz

The tarball's npm integrity is sha512-nMWLvnV6txpNPo8agA3SxDOFlAWiFPoYd3jgQqNlVU4kXKCZSzdlhj/lyz+NOfbZw0N4vQQ+nv4Ui8Xj8U2y4Q==; your lockfile records it, so later installs get exactly the same file. All versions are listed at /sdk/.

server.jsJavaScript
import { createVerifier } from "@blockalt/server";

// One verifier per protected action: hostname and action are fixed here, never taken from the request.
const verifySignup = createVerifier({
  apiUrl: "https://blockalt.click",
  secret: process.env.BLOCKALT_SECRET,
  hostname: "example.com",
  action: "signup",
});

const result = await verifySignup(req.body["blockalt-token"] ?? "", {
  remoteip: req.ip, // optional, see remoteip
  idempotencyKey: requestId, // optional, makes one retry safe
});
if (!result.success) return res.status(403).send("Please try again.");
  • success is true only for accepted, unpaid and paused. Unknown reasons from newer API versions are refusals, never exceptions.
  • HTTP failures have their own reasons: invalid_secret (401), invalid_request (400, 413, 415 and other 4xx), rate_limited (429, with retryAfter in seconds) and service_unavailable (5xx, network errors and timeouts, 3 seconds by default). Calls are never retried automatically.
  • Some refusals are answered locally, marked local: true, without calling BlockAlt: a token this verifier has already seen accepted (a replay), tokens whose readable claims are expired or bound to another hostname or action, and the test keys' dummy token with a real secret. Tokens are authenticated with a key only BlockAlt holds, so the SDK cannot check their tag: a well-formed forged token goes to siteverify, which checks the tag first and refuses it without spending your site's verification budget, and refusals are never billed. Only legacy version 1 tokens have a signature the SDK checks locally, against the keys published at /v1/keys. siteverify always makes the final decision on every token sent to it. precheck: false and tokenKeys: false turn the local checks off.
  • An empty token (the browser was told the site is unpaid or paused) is sent at most once every 15 seconds per hostname and action: its answer, whatever it is, is reused locally for empty tokens (local: true) during that time. For 15 seconds after an unpaid or paused answer, other tokens skip the local checks and go to siteverify, which decides each one, so a lockdown or a top-up applies to the next token.
  • With the test secrets, every call goes to siteverify and nothing is cached.
  • The object form verify({ token, hostname, action }) also works with a verifier created without a fixed hostname or action.

Python #

Standard library only:

blockalt.pyPython
import json
import os
import urllib.error
import urllib.request


class _NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None  # a redirect raises HTTPError instead of resending the secret


_opener = urllib.request.build_opener(_NoRedirect)


def verify_blockalt(token: str, action: str, remoteip: str | None = None) -> bool:
    payload = {"token": token or "", "hostname": "example.com", "action": action}
    if remoteip:
        payload["remoteip"] = remoteip
    request = urllib.request.Request(
        "https://blockalt.click/v1/siteverify",
        data=json.dumps(payload).encode(),
        headers={
            "Authorization": f"Bearer {os.environ['BLOCKALT_SECRET']}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    try:
        with _opener.open(request, timeout=3) as response:
            result = json.load(response)
    except (urllib.error.URLError, TimeoutError, ValueError):
        return False  # 4xx, 5xx, timeouts and bad JSON all refuse
    return result.get("success") is True

PHP #

blockalt.phpPHP
<?php
function blockalt_verify(string $token, string $action, ?string $remoteip = null): bool
{
    $payload = ['token' => $token, 'hostname' => 'example.com', 'action' => $action];
    if ($remoteip !== null) {
        $payload['remoteip'] = $remoteip;
    }
    $ch = curl_init('https://blockalt.click/v1/siteverify');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('BLOCKALT_SECRET'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_TIMEOUT => 3,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($body === false || $status !== 200) {
        return false;
    }
    $result = json_decode($body, true);
    return is_array($result) && ($result['success'] ?? false) === true;
}

// $ok = blockalt_verify($_POST['blockalt-token'] ?? '', 'signup');

Go #

blockalt.goGo
package blockalt

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

type Result struct {
	Success bool   `json:"success"`
	Reason  string `json:"reason"`
	ID      string `json:"id"`
}

var client = &http.Client{
	Timeout: 3 * time.Second,
	CheckRedirect: func(*http.Request, []*http.Request) error {
		return http.ErrUseLastResponse
	},
}

// Verify returns an error for every non-decision; allow the action only when
// err == nil && result.Success.
func Verify(ctx context.Context, token, action, remoteIP string) (Result, error) {
	payload := map[string]string{"token": token, "hostname": "example.com", "action": action}
	if remoteIP != "" {
		payload["remoteip"] = remoteIP
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return Result{}, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		"https://blockalt.click/v1/siteverify", bytes.NewReader(body))
	if err != nil {
		return Result{}, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOCKALT_SECRET"))
	req.Header.Set("Content-Type", "application/json")
	res, err := client.Do(req)
	if err != nil {
		return Result{}, err
	}
	defer res.Body.Close()
	var result Result
	if err := json.NewDecoder(io.LimitReader(res.Body, 8192)).Decode(&result); err != nil {
		return Result{}, err
	}
	if res.StatusCode != http.StatusOK {
		return result, fmt.Errorf("blockalt: HTTP %d (%s)", res.StatusCode, result.Reason)
	}
	return result, nil
}

Ruby #

blockalt.rbRuby
require "json"
require "net/http"

def blockalt_verify(token, action, remoteip: nil)
  uri = URI("https://blockalt.click/v1/siteverify")
  payload = { token: token.to_s, hostname: "example.com", action: action }
  payload[:remoteip] = remoteip if remoteip
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 2, read_timeout: 3) do |http|
    request = Net::HTTP::Post.new(uri, "Content-Type" => "application/json",
                                       "Authorization" => "Bearer #{ENV.fetch("BLOCKALT_SECRET")}")
    request.body = JSON.generate(payload)
    http.request(request)
  end
  response.is_a?(Net::HTTPOK) && JSON.parse(response.body)["success"] == true
rescue StandardError
  false
end

Java #

Java 11 or newer, with Jackson for JSON:

BlockAlt.javaJava
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

public final class BlockAlt {
  private static final HttpClient HTTP = HttpClient.newBuilder()
      .connectTimeout(Duration.ofSeconds(2))
      .followRedirects(HttpClient.Redirect.NEVER)
      .build();
  private static final ObjectMapper JSON = new ObjectMapper();

  public static boolean verify(String token, String action, String remoteIp) {
    Map<String, String> payload = new HashMap<>();
    payload.put("token", token == null ? "" : token);
    payload.put("hostname", "example.com");
    payload.put("action", action);
    if (remoteIp != null) payload.put("remoteip", remoteIp);
    try {
      HttpRequest request = HttpRequest.newBuilder(URI.create("https://blockalt.click/v1/siteverify"))
          .timeout(Duration.ofSeconds(3))
          .header("Authorization", "Bearer " + System.getenv("BLOCKALT_SECRET"))
          .header("Content-Type", "application/json")
          .POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(payload)))
          .build();
      HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 200) return false;
      JsonNode success = JSON.readTree(response.body()).path("success");
      return success.isBoolean() && success.booleanValue();
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      return false;
    } catch (Exception e) {
      return false;
    }
  }
}

C# / .NET #

.NET 6 or newer:

BlockAlt.csC#
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

public static class BlockAlt
{
    private static readonly HttpClient Http = new(new HttpClientHandler { AllowAutoRedirect = false })
    {
        Timeout = TimeSpan.FromSeconds(3),
    };

    private sealed record Result(bool Success, string? Reason, string? Id);

    public static async Task<bool> VerifyAsync(string? token, string action, string? remoteIp = null,
        CancellationToken cancellationToken = default)
    {
        var payload = new Dictionary<string, string>
        {
            ["token"] = token ?? "",
            ["hostname"] = "example.com",
            ["action"] = action,
        };
        if (remoteIp is not null) payload["remoteip"] = remoteIp;
        using var request = new HttpRequestMessage(HttpMethod.Post, "https://blockalt.click/v1/siteverify")
        {
            Content = JsonContent.Create(payload),
        };
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer",
            Environment.GetEnvironmentVariable("BLOCKALT_SECRET"));
        try
        {
            using var response = await Http.SendAsync(request, cancellationToken);
            if (response.StatusCode != HttpStatusCode.OK) return false;
            var result = await response.Content.ReadFromJsonAsync<Result>(
                new JsonSerializerOptions(JsonSerializerDefaults.Web), cancellationToken);
            return result?.Success == true;
        }
        catch (Exception e) when (e is HttpRequestException or TaskCanceledException or JsonException)
        {
            return false;
        }
    }
}

Something unclear or missing on this page? Write to support@blockalt.click.