Get your first payment
This walks you through the entire integration loop once: create a payment, check its status, and verify the webhook telling you it completed. Every example on this page uses test mode keys and settles nothing real — safe to run exactly as written.
-
Get a test-mode Store API key pair
Section titled “Get a test-mode Store API key pair”From your dashboard, create a store and issue a secret key. You’ll get a public API key (
pk_test_…) and a secret key (sk_test_…, shown once — save it). Every Store API call authenticates with both, as HTTP Basic auth:Authorization: Basic base64(apiKey:secretKey). -
Create a payment
Section titled “Create a payment”Terminal window API_KEY="pk_test_3f9c...b2"SECRET_KEY="sk_test_7ac1...e40f9b"curl -s https://api.sendchain.example/v1/store-api/payments \-u "$API_KEY:$SECRET_KEY" \-H "Content-Type: application/json" \-d '{"asset": "USDC","chain": "base","amount": { "minor": 500, "currency": "USD" }}'const API_KEY = "pk_test_3f9c...b2";const SECRET_KEY = "sk_test_7ac1...e40f9b";const res = await fetch("https://api.sendchain.example/v1/store-api/payments", {method: "POST",headers: {"Authorization": "Basic " + Buffer.from(`${API_KEY}:${SECRET_KEY}`).toString("base64"),"Content-Type": "application/json",},body: JSON.stringify({asset: "USDC",chain: "base",amount: { minor: 500, currency: "USD" },}),});const payment = await res.json();console.log(payment);import requestsAPI_KEY = "pk_test_3f9c...b2"SECRET_KEY = "sk_test_7ac1...e40f9b"response = requests.post("https://api.sendchain.example/v1/store-api/payments",auth=(API_KEY, SECRET_KEY),json={"asset": "USDC","chain": "base","amount": {"minor": 500, "currency": "USD"},},)payment = response.json()print(payment)<?php$apiKey = "pk_test_3f9c...b2";$secretKey = "sk_test_7ac1...e40f9b";$ch = curl_init("https://api.sendchain.example/v1/store-api/payments");curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,CURLOPT_POST => true,CURLOPT_USERPWD => "$apiKey:$secretKey",CURLOPT_HTTPHEADER => ["Content-Type: application/json"],CURLOPT_POSTFIELDS => json_encode(["asset" => "USDC","chain" => "base","amount" => ["minor" => 500, "currency" => "USD"],]),]);$payment = json_decode(curl_exec($ch), true);curl_close($ch);print_r($payment);You’ll get back a
201with anid, a receivingaddress, and apayAmount— everything you need to show the payer a QR code or deep link. See Create a payment for every field. -
Send the test payment
Section titled “Send the test payment”On the local/staging test network, send the exact
payAmountshown in the response toaddress. In test mode this uses testnet funds, never real money — see your environment’s test-funding instructions. -
Poll for status (optional — webhooks arrive faster)
Section titled “Poll for status (optional — webhooks arrive faster)”GET /v1/store-api/payments/{id}with the same Basic auth returns{ "id": "...", "status": "...", "confirmations": N }. Useful for a manual check, but the webhook below is the reliable signal. -
Receive and verify the webhook
Section titled “Receive and verify the webhook”Once your store account has a webhook endpoint registered and subscribed to
payment.completed, Tribute POSTs a signed event to it the moment this payment clears:Terminal window # Illustrative — verification happens in your receiver's code. Given a# received body $BODY and header "Tribute-Signature: t=…,v1=…":SIGNED_PAYLOAD="${TIMESTAMP}.${BODY}"EXPECTED=$(printf '%s' "$SIGNED_PAYLOAD" | openssl dgst -sha256 -hmac "$ENDPOINT_SECRET" | sed 's/^.* //')# Compare $EXPECTED against each v1= value in the header.import crypto from "node:crypto";function verify(secret, headerValue, body, toleranceSeconds = 300) {const parts = Object.fromEntries(headerValue.split(",").map((p) => p.split("=", 2)));const t = Number(parts.t);if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;const expected = crypto.createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");const received = headerValue.match(/v1=([0-9a-f]+)/g) ?? [];return received.some((v1) =>crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1.split("=")[1])),);}import hmac, hashlib, timedef verify(secret, header_value, body, tolerance_seconds=300):parts = dict(p.split("=", 1) for p in header_value.split(","))t = int(parts.get("t", 0))if not t or abs(time.time() - t) > tolerance_seconds:return Falseexpected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()received = [v for k, v in (p.split("=", 1) for p in header_value.split(",")) if k == "v1"]return any(hmac.compare_digest(expected, mac) for mac in received)<?phpfunction verify(string $secret, string $headerValue, string $body, int $tolerance = 300): bool {$parts = [];foreach (explode(",", $headerValue) as $pair) {[$k, $v] = array_pad(explode("=", $pair, 2), 2, null);$parts[$k][] = $v;}$t = isset($parts["t"][0]) ? (int) $parts["t"][0] : 0;if (!$t || abs(time() - $t) > $tolerance) return false;$expected = hash_hmac("sha256", "{$t}.{$body}", $secret);foreach ($parts["v1"] ?? [] as $mac) {if (hash_equals($expected, $mac)) return true;}return false;}Full header format, retry behavior, and every event’s payload shape are in the Webhooks overview.
That’s the whole loop. From here: browse the API Reference for every field and error case, or the Webhooks section for every event type you can subscribe to.