Webhooks

Pulse calls your endpoints when an order reaches a milestone.

Pulse sends a POST request to each active endpoint when an order reaches a milestone.

Pulse supports seven order events. A delivery retry can send the same event more than once.

Set up webhook endpoints

Open Webhooks in the Dashboard and select the test or live environment. You can register up to three public HTTPS endpoints in each environment. Test endpoints receive environment: "test" events. Live endpoints receive environment: "live" events.

Pulse generates a unique signing secret for each endpoint. The Dashboard shows the secret after endpoint creation or rotation. Store each secret before you close the dialog. Configure each handler with the secret for its endpoint.

Select an endpoint to inspect its delivery attempts or replay a delivery. You can also enable, disable, rotate, or delete the endpoint. Disabled endpoints count toward the three-endpoint limit. Deleting an endpoint frees one endpoint slot.

The previous secret stops working immediately after rotation. Replace it when the Dashboard shows the new secret.

Handle endpoint verification

When you add an endpoint, Pulse sends a verification request to your URL and saves the endpoint only after it returns a 2xx response. The request can use POST or GET, and its body can be empty. It carries no X-Pulse-Signature header, because Pulse generates the signing secret after verification.

Check for X-Pulse-Signature before you read the body. If the header is absent, return 200 and take no other action. Pulse signs every event delivery.

Event structure

Every event uses the same top-level structure. The data object depends on the event type.

{
  "id": "0198e2f1-3b7c-7a41-9e05-1c8d2f60b933",
  "type": "order.payment_received",
  "environment": "test",
  "created_at": "2026-07-28T10:04:52Z",
  "data": {
    "order_id": "0198e2d0-…",
    "reference": "partner-order-88213"
  }
}

An order event contains your order reference.

Events from the test environment carry environment: "test". Events from the live environment carry environment: "live". Check this field before you process the event.

The event type identifies the event. Event payloads do not contain a separate status field.

Event sequence

Pulse creates events in the following sequence. Deliveries can arrive out of order.

order.awaiting_payment

order.payment_received

order.conversion_initiated

order.payout_initiated

order.completed          ← terminal

order.expired            ← terminal, instead
order.failed             ← terminal, instead

Each order produces one terminal event: completed, expired, or failed.

See Testing to trigger each event.

Handle deliveries

Return a 2xx response promptly

Verify and record the event, then return a 2xx response. Complete longer work after the response. A timeout can cause another delivery of the same event.

Delivery retries

Pulse retries a failed delivery for up to 8 hours with an exponential backoff. A timeout or non-2xx response starts the retries. You can inspect and replay a delivery in the Dashboard.

Duplicate events

Pulse reuses the top-level event ID (id) for every delivery attempt. Record each processed event ID, and skip a duplicate.

Delivery order

Do not depend on delivery order. Record each event that arrives, even when an earlier event has not arrived yet.

Verify deliveries

Verify each delivery before you process its event. Pulse sets X-Pulse-Signature to the lowercase hexadecimal HMAC-SHA256 digest of the raw request body. Use the endpoint signing secret as the HMAC key.

Use the raw request body

Verify the signature before you decode the JSON. Decoding and re-encoding the body can change its bytes and invalidate the signature.

Go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

func verifyPulseSignature(rawBody []byte, signature, secret string) bool {
	if signature == "" || secret == "" {
		return false
	}

	providedDigest, err := hex.DecodeString(signature)
	if err != nil || len(providedDigest) != sha256.Size {
		return false
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)

	expectedDigest := mac.Sum(nil)
	return hmac.Equal(providedDigest, expectedDigest)
}

hmac.Equal compares the digests in constant time. Reject an invalid signature with a non-2xx response.

Events

order.awaiting_payment

The order is ready for payment. Use this event to recover from a timed-out create request. It also provides an offramp deposit address that was not ready in the create response.

{
  "id": "0198e2d1-0a44-7f19-8c33-b7e2409d5a18",
  "type": "order.awaiting_payment",
  "environment": "test",
  "created_at": "2026-07-28T10:02:11Z",
  "data": {
    "order_id": "0198e2d0-…",
    "reference": "partner-order-88213",
    "rate": "1538.46",
    "amount": "150000",
    "currency": "NGN",
    "funding_account": {
      "account_name": "Ada Obi",
      "account_number": "9901234567",
      "bank_code": "090175",
      "bank_name": "Rubies MFB",
      "amount": "150000"
    },
    "expires_at": "2026-07-28T10:32:11Z"
  }
}

Only this event contains rate. Pulse fixes it at quote acceptance.

For an offramp, funding_account is { "deposit_address": "0x…", "network": "POLYGON" }. The currency value identifies the asset to send.

order.payment_received

Pulse received the customer's payment. For an onramp, naira reached the virtual account. For an offramp, crypto reached the deposit address.

{
  "id": "0198e3f8-4c11-7e63-9a07-2d5b81f4c920",
  "type": "order.payment_received",
  "environment": "test",
  "created_at": "2026-07-29T14:33:52Z",
  "data": {
    "order_id": "0198e3f7-…",
    "reference": "partner-order-88301",
    "amount": "200",
    "currency": "USDT",
    "network": "POLYGON"
  }
}

Use the amount received

amount reports the payment received and can differ from the quote. A payment of 190 USDT for a 200 USDT order produces amount: "190". Pulse uses this amount and the effective order rate to calculate the NGN payout.

network appears only for an on-chain payment. An onramp event contains amount and currency without network.

order.conversion_initiated

Pulse started the conversion. No action is required. Use this event to update the order's progress.

{
  "id": "0198e2f2-6e05-7a88-b410-93cd7e2f6045",
  "type": "order.conversion_initiated",
  "environment": "test",
  "created_at": "2026-07-28T10:05:30Z",
  "data": {
    "order_id": "0198e2d0-…",
    "reference": "partner-order-88213",
    "from": { "currency": "NGN", "amount": "150000" },
    "to": { "currency": "USDT", "network": "POLYGON" }
  }
}

The to object omits amount because conversion is not complete. order.completed reports the delivered amount.

order.payout_initiated

Pulse sent the payout. Settlement awaits confirmation. An onramp sends crypto to the customer's address. An offramp sends naira to the beneficiary's bank account.

{
  "id": "0198e2f3-11ba-7c47-9e82-40d6b5197ce3",
  "type": "order.payout_initiated",
  "environment": "test",
  "created_at": "2026-07-28T10:06:58Z",
  "data": {
    "order_id": "0198e2d0-…",
    "reference": "partner-order-88213",
    "amount": "97.50",
    "currency": "USDT",
    "network": "POLYGON",
    "transaction_hash": "0x7d4f1e83a6c2905b41fd7e3820ac95661b0d8f47e2c31a95604fb8d72e0a1c36"
  }
}

Store the payout identifier with your order. An onramp contains transaction_hash, which you can link to a block explorer. An offramp contains payout_reference for the bank transfer. Include this reference in a support request about a delayed payout. The two fields never appear together.

order.completed

The payout completed, and the order is final.

{
  "id": "0198e3fb-9d27-7b04-a165-6c80f213ae59",
  "type": "order.completed",
  "environment": "test",
  "created_at": "2026-07-29T14:41:07Z",
  "data": {
    "order_id": "0198e3f7-…",
    "reference": "partner-order-88301",
    "amount": "304600",
    "currency": "NGN",
    "payout_reference": "PLS-8f3a2c91"
  }
}

Use amount to reconcile the payout. It reports the amount delivered. For an offramp, Pulse calculates it from the crypto received and the effective order rate. An onramp event contains network and transaction_hash instead of payout_reference.

order.expired

The funding window closed without payment. No funds moved. Create a new quote and order if the customer wants to continue. You cannot reopen an expired order or reuse its rate.

{
  "id": "0198e30c-15b8-7d93-8e41-77a0c2f6b501",
  "type": "order.expired",
  "environment": "test",
  "created_at": "2026-07-28T10:32:11Z",
  "data": {
    "order_id": "0198e2ff-…",
    "reference": "partner-order-88240",
    "amount": "75000",
    "currency": "NGN"
  }
}

order.expired cannot follow order.payment_received. Pulse does not attribute a payment sent after expiry to this order.

order.failed

The order stopped before completion. Use reason to determine the next action and whether Pulse holds the customer's funds.

{
  "id": "0198e41a-2f66-7b15-9d80-8e37c05a1b44",
  "type": "order.failed",
  "environment": "test",
  "created_at": "2026-07-29T15:02:38Z",
  "data": {
    "order_id": "0198e414-…",
    "reference": "partner-order-88318",
    "reason": "payout_failed"
  }
}
ReasonMeaningWhat to do
payment_rejectedThe incoming payment could not be accepted.No funds are held. Start a new order.
payout_failedPulse received the payment but could not complete the payout.Pulse holds and reconciles the funds. Contact support with the reference. Do not retry.
internal_errorPulse could not complete the order.Contact support with the reference.

On this page