How webhooks work
Webhooks notify your server about order events so your team can update its own records and workflows. Tyga Pay sends a signed HTTPS POST to your endpoint; your customer's browser does not receive it.
- Tyga Pay records an order event. An active endpoint is needed to prepare a delivery.
- Tyga Pay sends a signed event to your endpoint. Your server verifies it, stores or queues it once, and returns a
2xxresponse. - Your system reads the latest order before acting. A webhook is a snapshot of a transition; later events or payment review can change the current state.
Webhooks are optional for order creation. Without an active endpoint, the order can still be created, but no delivery is prepared for your server.
For managers: order.created does not mean the customer paid. A payment event reports a change in payment status; it does not confirm a payout. “Delivered” means your endpoint returned 2xx, not that your team finished processing the order. A checkout redirect is not payment proof either.
Set up an endpoint
- Sign in to the developer portal and open the merchant workspace you want to configure.
- Open Webhooks, select Add webhook, and save your server's public HTTPS URL. Active administrator and developer members can manage it. The portal may ask you to verify your passkey again.
- Copy the signing secret shown after first creation into your server's secret store. It is shown once and is separate from your merchant API key. If an existing endpoint has no secret available to copy, use Rotate secret to receive a new one.
- Use Send test webhook and check Last test plus Webhook activity. A test sends
webhook.testthrough the same signed delivery path; it does not create an order or prove payment processing.
Each webhook configuration has one destination and its own signing secret. If you configure more than one API environment, set up each destination separately, even when the URLs match.
Use an HTTPS URL that resolves to a public IPv4 address and accepts direct POST requests. Localhost, private addresses, IPv6-only destinations, custom ports, credentials and URL fragments are unsupported. Tyga Pay does not follow redirects.
Editing the URL, rotating the secret or disabling the endpoint changes its version. Pending deliveries for an older version are disabled instead of being sent to the new destination. Rotate only after your receiver can use the replacement secret. An already in-flight request might still carry the previous signature.
Events and payloads
order.created- A new order was created.
data.statusisopen; this event does not mean funds arrived. order.paid- Verified payment credit changed the payment state to
paid. A later review event can supersede this snapshot. order.payment_review_required- The payment state changed to
review_required, for example after a late, cancelled or revoked deposit. Pause fulfillment and inspect the current order. webhook.test- A connection check sent only when you select Send test webhook. It has no order;
data.environmentidentifies the connection used for the test.
{
"id": "0000000000000000000000000000000000000000000000000000000000000000",
"type": "order.paid",
"apiVersion": "v2",
"createdAt": "2026-09-25T10:00:00.000Z",
"data": {
"orderId": "00000000-0000-4000-8000-000000000001",
"merchantReference": "MERCHANT-ORDER-001",
"amountMinor": 100,
"currency": "USD",
"status": "paid",
"assetId": "USDC",
"networkId": "ethereum-sepolia",
"amountBaseUnits": "1000000",
"receivedBaseUnits": "1000000"
}
}Every event has a stable id, type, apiVersion: "v2", UTC createdAt, and data. Order events contain orderId, merchantReference, amountMinor in USD cents, currency: "USD", and status. For payment events, status is the payment state at that transition. It is not the order's lifecycle status.
Payment events can also include assetId, networkId, amountBaseUnits (expected token amount), receivedBaseUnits (credited token amount), overpaymentBaseUnits when nonzero, and reviewReason when present. Current review reasons are order_cancelled, deposit_revoked and late_or_missing_inclusion. Base-unit amounts are decimal strings in the token's smallest unit, not USD cents. Treat optional fields as optional.
order.paid captures the transition. Additional surplus after an already-paid state can update the order without another merchant webhook. Read the current order and payment history when you need the latest amount.
Verify signatures
Tyga Pay sends Content-Type: application/json, Tyga-Event-Id and Tyga-Signature. The event ID header matches the JSON id. The signature has the form t=<Unix seconds>,v1=<hex HMAC>. Compute HMAC-SHA256 over the exact raw request bytes prefixed with the timestamp and a period: <t>.<raw body>.
Verify before parsing or changing the body. Reject signatures outside a five-minute window and compare digests in constant time. The signature timestamp describes the send attempt; createdAt describes the event. Use the secret for that endpoint. Never send it to a browser, put it in a URL or log it.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifyTygaWebhook(rawBody: Buffer, signatureHeader: string, secret: string): boolean {
const match = /^t=(\d{1,12}),v1=([a-f0-9]{64})$/.exec(signatureHeader)
if (!match || Buffer.byteLength(secret) < 32) return false
const timestamp = Number(match[1])
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false
const expected = createHmac('sha256', secret)
.update(`${timestamp}.`).update(rawBody).digest()
const received = Buffer.from(match[2], 'hex')
return timingSafeEqual(received, expected)
}Pass the unmodified request body as a Buffer and the Tyga-Signature header to this helper. Reject a failed check before handling the event. Then confirm Tyga-Event-Id equals the parsed id, and persist that ID with your business update so retries cannot apply the update twice.
Delivery and recovery
Return any 2xx status only after you have durably accepted the event. Non-2xx responses, redirects, network failures and the 10-second request timeout are treated as unsuccessful. Tyga Pay retries with backoff, up to ten send attempts; retry timing is not an exact schedule. Delivery can be repeated or arrive out of order, including after a timeout when your server did process the first attempt.
- Sending
pending - Queued, in flight or waiting for a retry. An event can remain here while delivery is still being attempted.
- Delivered
delivered - Your endpoint returned
2xx. Check your own processing separately. - Failed
failed - The send limit was reached without a
2xx. Fix the receiver, then select Retry webhook in the delivery details. Reconcile the order through the API as well. - Disabled
disabled - The endpoint was disabled or its version changed before delivery. These old deliveries are not sent to the replacement endpoint.
In portal Webhooks, the newest five deliveries appear first. Select Load 5 more to browse older deliveries, or search for an exact order ID, order reference or event ID. Select a delivery to inspect its attempts, last HTTP response and payload. Activity remains visible if the endpoint is later disabled.
A failed delivery can be retried from its details after an active endpoint is configured. The retry sends the original event ID and payload to the current endpoint and begins a new cycle of up to ten attempts. Your receiver must handle duplicates. A missing row does not prove that no order changed: if no active endpoint existed when the event was prepared, no delivery was created. A test can show Queued or Sending before its final result; refresh later if it is still in progress.
For each new event ID, verify and store it once, then retrieve the latest order with GET /v2/orders/:orderId using an orders_read key. Check paymentStatus before fulfillment; the order lifecycle status can still read open after payment. Cancelled orders return 404 from that lookup, while their payment history remains available for reconciliation. If a later review event arrives, stop automatic fulfillment and investigate. Keep your own recovery path for failed or missed notifications.