Webhooks And Errors

Verify webhook signatures and handle SDK errors by type.

Two things the SDK handles that are easy to get wrong by hand: proving a webhook came from Kyshi, and telling a retryable failure apart from a permanent one.

Verifying Webhooks

Kyshi signs every webhook with HMAC-SHA256 and sends the signature in X-Kyshi-Signature. The SDK gives you three helpers.

constructEvent

The one to use. It verifies the signature and returns the parsed event, throwing if verification fails:

import express from 'express';
import { Kyshi } from '@kyshi/mor-sdk';

const kyshi = new Kyshi({ secretKey: process.env.KYSHI_SECRET_KEY! });

app.post(
  '/webhooks/kyshi',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    try {
      const event = kyshi.webhooks.constructEvent(
        req.body.toString('utf8'),
        req.get('X-Kyshi-Signature')!,
        process.env.KYSHI_WEBHOOK_SECRET!,
      );

      res.sendStatus(200);   // Acknowledge first.
      void handle(event);    // Then do the work.
    } catch {
      res.sendStatus(401);
    }
  },
);

Pass the raw body. If your framework parses JSON before you see it, re-serialising the object may not reproduce the exact bytes that were signed and verification will fail. express.raw() above is what keeps them identical.

Your endpoint must respond within 10 seconds, so acknowledge before processing.

verifySignature

Returns a boolean instead of throwing, when you want to branch rather than catch:

if (!kyshi.webhooks.verifySignature(rawBody, signature, secret)) {
  return res.sendStatus(401);
}

generateSignature

Produces a signature for a payload. Useful for testing your own handler without waiting for Kyshi to send something:

const signature = kyshi.webhooks.generateSignature(payload, secret);

Both comparisons are constant-time inside the SDK. Do not reimplement this with ===.

Deduplicate

The same business event can arrive more than once. Key on meta.kyshiEventId and make your handler idempotent — see Webhook Events for the full catalogue and payload shape.

Errors

Every failure throws a subclass of KyshiApiError, so you can catch by type rather than inspecting status codes.

ClassThrown whenRetry?
KyshiValidationError400 or 422 — bad request shape, or a business rule failedNo, fix the request
KyshiAuthenticationError401 or 403 — bad key, or missing permissionNo, fix the key
KyshiTimeoutErrorThe attempt exceeded timeoutMsMaybe — see below
KyshiNetworkErrorThe request never completedMaybe — see below
KyshiApiErrorEverything else, including 5xxDepends on the status

All of them carry:

PropertyMeaning
statusCodeHTTP status. 408 for timeouts, 0 for network errors.
messageKyshi's message. Log it; do not branch on it.
codeThe API's code field, where present.
dataThe response data, where present.
import {
  KyshiValidationError,
  KyshiAuthenticationError,
  KyshiTimeoutError,
  KyshiNetworkError,
} from '@kyshi/mor-sdk';

try {
  await kyshi.transactions.initialize(params);
} catch (error) {
  if (error instanceof KyshiValidationError) {
    return badRequest(error.message);      // Your payload is wrong.
  }
  if (error instanceof KyshiAuthenticationError) {
    return alertOps(error);                // Key or permission problem.
  }
  if (error instanceof KyshiTimeoutError || error instanceof KyshiNetworkError) {
    return markUnknown(params.reference);  // See below.
  }
  throw error;
}

The Case That Costs Money

KyshiTimeoutError and KyshiNetworkError do not mean the request failed. They mean you did not get an answer. The transaction or payout may well have been created.

Never blindly retry a write after one. Either:

  • pass an idempotencyKey so the retry is safe, or
  • look the record up by your own reference before deciding
catch (error) {
  if (error instanceof KyshiNetworkError) {
    const existing = await kyshi.transactions.verify(params.reference);
    if (existing?.status === 'SUCCESS') return existing;   // It went through.
    return retryOnce(params);
  }
}

This is also why the SDK will not auto-retry a POST unless you supply an idempotency key. See Configuration.

Next


Did this page help you?