Skip to content

Events reference

Each event is a JSON object containing a type, a unique ID, and the data for the resource involved. Subscribe to these events when creating a webhook endpoint.

Events are emitted with livemode: true in production and livemode: false in sandbox. A given endpoint only receives events for the mode matching its API key.

Idempotency — process each event only once

In the event of a retry, the same event may be delivered more than once with the same id. Store the IDs of events you’ve already processed and ignore duplicates to keep your handler idempotent.

Payment events

EventTrigger
payment_intent.createdPaymentIntent created.
payment_intent.succeededPayment succeeded — trigger fulfillment.
payment_intent.payment_failedPayment failed after the last attempt.
payment_intent.canceledPaymentIntent canceled manually.
checkout_session.completedCheckout session moved to complete status.
checkout_session.expiredSession expired without payment.
transaction.succeededTransaction finalized on the operator's side.
transaction.failedTransaction failed on the operator's side.
refund.createdRefund initiated.
refund.succeededRefund returned to the buyer.
refund.failedRefund failed on the operator's side.
payment_link.paidPayment link successfully used.

Customer events

EventTrigger
customer.createdNew Customer created.
customer.updatedCustomer data updated.
customer.deletedCustomer anonymized (GDPR compliance).
payment_method.attachedPayment method attached to the customer.
payment_method.detachedPayment method detached from the customer.

Subscription events

EventTrigger
subscription.createdNew subscription created.
subscription.updatedSubscription updated (plan, quantity, etc.).
subscription.renewedRenewal succeeded — invoice paid.
subscription.past_dueRenewal failed — trigger dunning.
subscription.canceledCancellation effective.
subscription.trial_will_end3 days before the trial period ends.
invoice.createdInvoice generated automatically.
invoice.finalizedInvoice finalized and sent to the customer.
invoice.paidInvoice paid.
invoice.payment_failedInvoice payment failed.

Account events

EventTrigger
kyc.submittedKYC application submitted for validation.
kyc.approvedKYC approved — account fully operational.
kyc.rejectedKYC rejected — action required from the merchant.
payout.createdTransfer to the bank account initiated.
payout.paidTransfer completed successfully.
payout.failedTransfer failed — check the bank details.

Structure of an event

ResponseEvent object
Common structure for all Sangho events.
json
{
  "id": "evt_xxxxxxxxxxxx",
  "object": "event",
  "type": "payment_intent.succeeded",
  "api_version": "v1",
  "created_at": "2026-03-01T10:05:00Z",
  "livemode": true,
  "data": {
    "object": {
      "id": "pi_xxxxxxxxxxxx",
      "object": "payment_intent",
      "amount": 15000,
      "currency": "XAF",
      "status": "succeeded",
      "customer": "cust_xxxx"
    }
  }
}

Implementing a handler

Your webhook handler must respond with a 2xx HTTP status code within 10 seconds. For long-running processing, queue the task in an asynchronous queue and respond immediately.

Always start by verifying the Sangho-Signature before any processing. See the signature verification guide for complete examples in each language.

Handling unknown events

Your handler should silently ignore event types it doesn’t recognize and return a 200. Sangho may emit new event types without notice — a handler that returns an error on an unknown type will be retried unnecessarily.

Test event

Use POST /webhooks/{id}/test/ to send a mock event to your endpoint and validate your integration without triggering an actual payment. The test event has livemode: false regardless of the environment.

Webhook handler

javascript
// Node.js / Express
app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sig = req.headers['sangho-signature'];
    let event;


    try {
      event = sangho.webhooks.constructEvent(
        req.body,
        sig,
        process.env.WEBHOOK_SECRET,
      );
    } catch (err) {
      return res.status(400).send(`Invalid signature: ${err.message}`);
    }


    switch (event.type) {
      case 'payment_intent.succeeded':
        await fulfillOrder(event.data.object.metadata.order_id);
        break;
      case 'subscription.past_due':
        await sendDunningEmail(event.data.object.customer);
        break;
      case 'refund.succeeded':
        await notifyCustomerRefund(event.data.object);
        break;
      default:
        // Unknown type — silently ignore
        break;
    }


    res.json({ received: true });
  },
);