Signing requests

Sign the exact bytes you send. Serialise the JSON once, sign that string, and send that same string as the body.

Keep the signing secret on your server. Never put it in front-end code. Rotate it by contacting support if it leaks.

Node.js / TypeScript

easyvial-orders.ts — drop-in server helper
import { createHmac, randomUUID } from "node:crypto";

const STORE = process.env.EASYVIAL_STORE!;            // your slug
const SECRET = process.env.EASYVIAL_SIGNING_SECRET!;  // from Store setup

export type OrderStatus =
  | "pending" | "processing" | "on-hold" | "completed" | "cancelled" | "refunded" | "failed";

export interface AppOrder {
  id: string;
  orderKey: string;
  status: OrderStatus;
  total: number;
  refundedTotal?: number;
  currency?: string;
  utmSource?: string | null;
  utmMedium?: string | null;
  utmCampaign?: string | null;
  createdAt?: Date;
}

export async function reportOrder(o: AppOrder, attempt = 1): Promise<void> {
  const body = JSON.stringify({
    id: o.id,
    order_key: o.orderKey,
    status: o.status,
    total: o.total.toFixed(2),
    refunded_total: (o.refundedTotal ?? 0).toFixed(2),
    currency: o.currency ?? "USD",
    utm_source: o.utmSource ?? null,
    utm_medium: o.utmMedium ?? null,
    utm_campaign: o.utmCampaign ?? null,
    created_at: (o.createdAt ?? new Date()).toISOString(),
  });
  const signature = createHmac("sha256", SECRET).update(body).digest("hex");
  const res = await fetch("https://easyvial.app/api/v1/orders?store=" + STORE, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-EasyVial-Signature": signature,
      "X-EasyVial-Delivery-ID": randomUUID(),
    },
    body,
  });
  if (res.status >= 500 && attempt < 4) {
    await new Promise((r) => setTimeout(r, attempt * 30_000));
    return reportOrder(o, attempt + 1);
  }
  if (!res.ok) throw new Error(`Order API ${res.status}: ${await res.text()}`);
}

PHP

<?php
function report_app_order(array $order): void {
    $body = json_encode([
        'id' => (string) $order['number'],
        'order_key' => $order['secret_key'],
        'status' => $order['status'],          // pending|processing|on-hold|completed|cancelled|refunded|failed
        'total' => number_format($order['total'], 2, '.', ''),
        'refunded_total' => number_format($order['refunded'] ?? 0, 2, '.', ''),
        'currency' => $order['currency'] ?? 'USD',
        'created_at' => gmdate('c', $order['created_at']),
    ], JSON_UNESCAPED_SLASHES);
    $sig = hash_hmac('sha256', $body, getenv('EASYVIAL_SIGNING_SECRET'));
    $ch = curl_init('https://easyvial.app/api/v1/orders?store=' . getenv('EASYVIAL_STORE'));
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-EasyVial-Signature: ' . $sig],
    ]);
    curl_exec($ch);
    curl_close($ch);
}

Python

import hashlib, hmac, json, os, requests

def report_app_order(order: dict) -> None:
    body = json.dumps({
        "id": str(order["number"]),
        "order_key": order["secret_key"],
        "status": order["status"],
        "total": f'{order["total"]:.2f}',
        "refunded_total": f'{order.get("refunded", 0):.2f}',
        "currency": order.get("currency", "USD"),
    }, separators=(",", ":"))
    sig = hmac.new(os.environ["EASYVIAL_SIGNING_SECRET"].encode(), body.encode(), hashlib.sha256).hexdigest()
    requests.post(
        "https://easyvial.app/api/v1/orders?store=" + os.environ["EASYVIAL_STORE"],
        data=body,  # send the exact string you signed
        headers={"Content-Type": "application/json", "X-EasyVial-Signature": sig},
        timeout=10,
    ).raise_for_status()

Ruby

require "json"; require "net/http"; require "openssl"

def report_app_order(order)
  body = { id: order.number.to_s, order_key: order.secret_key, status: order.status,
           total: format("%.2f", order.total), currency: order.currency || "USD" }.to_json
  sig = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("EASYVIAL_SIGNING_SECRET"), body)
  uri = URI("https://easyvial.app/api/v1/orders?store=#{ENV.fetch('EASYVIAL_STORE')}")
  Net::HTTP.post(uri, body, "Content-Type" => "application/json", "X-EasyVial-Signature" => sig)
end

Go

func ReportAppOrder(o Order) error {
	body, _ := json.Marshal(map[string]any{
		"id": o.Number, "order_key": o.SecretKey, "status": o.Status,
		"total": fmt.Sprintf("%.2f", o.Total), "currency": "USD",
	})
	mac := hmac.New(sha256.New, []byte(os.Getenv("EASYVIAL_SIGNING_SECRET")))
	mac.Write(body)
	req, _ := http.NewRequest("POST", "https://easyvial.app/api/v1/orders?store="+os.Getenv("EASYVIAL_STORE"), bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-EasyVial-Signature", hex.EncodeToString(mac.Sum(nil)))
	_, err := http.DefaultClient.Do(req)
	return err
}

Verifying your signature

Getting 401 invalid_signature? Sign and send the identical string. Frameworks that re-encode JSON (adding spaces, reordering keys) or middleware that rewrites the body will break the signature.