Webhooks & notifications

Subscribe to terminal payment events, verify signatures, and handle retries.

View as MarkdownInstall skills

VINR delivers the result of every terminal transaction as a webhook event — typically within milliseconds of the card interaction completing. Your backend subscribes to these events and uses them to fulfil orders, update inventory, and trigger receipts. Do not poll the terminal payment object for status; use webhooks.

Register an endpointAsk

Go to Dashboard → Developers → Webhooks and click Add endpoint. Enter your HTTPS URL and select the events you want to receive. For in-person payments subscribe to at minimum:

  • terminal_payment.completed
  • terminal_payment.failed
  • terminal_payment.cancelled

VINR also sends terminal_payment.created and terminal_payment.updated if you need visibility into earlier lifecycle stages.

Your endpoint must respond with HTTP 200 within 5 seconds. Move any slow processing (database writes, third-party calls) off the request path — acknowledge immediately, then process asynchronously.

Verify the signatureAsk

Every request VINR sends to your endpoint includes a Vinr-Signature header. Always verify it before trusting the payload — this prevents replay attacks and spoofed events.

import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

export async function POST(request: Request) {
  const rawBody = await request.text();
  const signature = request.headers.get('Vinr-Signature') ?? '';

  let event;
  try {
    event = vinr.webhooks.constructEvent(
      rawBody,
      signature,
      process.env.VINR_WEBHOOK_SECRET,
    );
  } catch (err) {
    return new Response('Invalid signature', { status: 400 });
  }

  // safe to process event here
  return new Response(null, { status: 200 });
}
import vinr
import os

webhook_secret = os.environ['VINR_WEBHOOK_SECRET']

def handle_webhook(request):
    payload = request.body
    sig_header = request.headers.get('Vinr-Signature')

    try:
        event = vinr.Webhook.construct_event(payload, sig_header, webhook_secret)
    except vinr.error.SignatureVerificationError:
        return HttpResponse(status=400)

    # safe to process event here
    return HttpResponse(status=200)

The signature header has the form t=<timestamp>,v1=<hmac>. To verify manually:

  1. Extract t (Unix timestamp) and v1 (hex HMAC-SHA256).
  2. Construct the signed payload string: <t>.<raw_body>.
  3. Compute HMAC-SHA256(webhook_secret, signed_payload_string).
  4. Compare your digest with v1 using a constant-time comparison.
  5. Reject if |now - t| > 300 seconds (replay window).

Terminal payment eventsAsk

terminal_payment.completed

Fired when the card interaction succeeds and funds are captured (or authorized, for manual capture). This is the event to act on for order fulfilment.

{
  "id": "evt_01HZ5QB2CC",
  "type": "terminal_payment.completed",
  "createdAt": "2026-06-02T10:14:07Z",
  "data": {
    "object": {
      "id": "tpay_01HZ5QA7BK",
      "terminalId": "term_01HZ5QXYZ",
      "amount": 2500,
      "amountCaptured": 2500,
      "currency": "USD",
      "reference": "order_8821",
      "captureMethod": "automatic",
      "status": "completed",
      "entryMethod": "contactless",
      "last4": "4242",
      "brand": "visa",
      "authCode": "123456",
      "createdAt": "2026-06-02T10:14:00Z",
      "completedAt": "2026-06-02T10:14:07Z"
    }
  }
}

For manual-capture payments, status is "authorized" and amountCaptured is 0 until you call capture. See Accept a payment — Capture modes.

terminal_payment.failed

Fired when the card interaction fails for any reason. Contains a declineCode for routing logic. See Handle responses for the full decline code reference.

{
  "id": "evt_01HZ5QB3DD",
  "type": "terminal_payment.failed",
  "createdAt": "2026-06-02T10:14:09Z",
  "data": {
    "object": {
      "id": "tpay_01HZ5QA8EE",
      "terminalId": "term_01HZ5QXYZ",
      "amount": 2500,
      "currency": "USD",
      "reference": "order_8822",
      "status": "failed",
      "declineCode": "card_declined",
      "createdAt": "2026-06-02T10:14:05Z",
      "failedAt": "2026-06-02T10:14:09Z"
    }
  }
}

terminal_payment.cancelled

Fired when the merchant cancels the session before the customer presents a card, or when the terminal times out waiting for card presentation (default 60 seconds).

Additional events

EventWhen it fires
terminal_payment.createdSession created on your backend
terminal_payment.updatedCapture amount adjusted, metadata changed
terminal_payment.refundedFull or partial refund issued
terminal.activatedTerminal registered to your account
terminal.deactivatedTerminal removed or factory-reset
terminal.offlineTerminal has not checked in for 15 minutes
terminal.onlineTerminal reconnected after an offline period

Retry behaviourAsk

If your endpoint does not respond with HTTP 2xx within 5 seconds, VINR retries the event on an exponential back-off schedule:

AttemptDelay after previous
1 (initial)
25 minutes
330 minutes
42 hours
58 hours
624 hours

After 6 failed attempts the event is marked undelivered and appears in the Dashboard under Developers → Webhooks → [endpoint] → Failed events where you can replay it manually.

Your handler must be idempotent — VINR may deliver the same event more than once. Use event.id as a deduplication key, not event.data.object.id.


Test webhooks locallyAsk

Use the VINR CLI to forward events to your local dev server without a tunnel:

# Install the CLI
npm install -g @vinr/cli

# Forward terminal events to your local server
vinr listen --forward-to localhost:3000/webhooks/vinr \
  --events terminal_payment.completed,terminal_payment.failed

The CLI prints each event as it arrives and shows your server's response code, making it easy to debug your handler.


Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page