Webhook Events

Each object in the platform has its own lifecycle and its status changes as it transitions between states. Whenever one such transition occurs, a webhook event is fired.

Setup

You can configure your webhook destinations from the MoveUSD dashboard:

From the settings page you can add a destination URL, manage your signing secret, and choose which event topics to subscribe to.

Authentication

All webhook requests are signed using the Standard Webhooks specification. Every request includes the following headers:

HeaderDescription
webhook-idA unique identifier for the webhook message. This ID is stable across retries and can be used for idempotency.
webhook-timestampThe Unix timestamp (in seconds) when the message was sent.
webhook-signatureA base64-encoded HMAC-SHA256 signature in the format v1,<base64>.

The signature is computed over the message ID, timestamp, and request body:

signed_content = "${webhook-id}.${webhook-timestamp}.${body}"
signature = base64(HMAC-SHA256(secret, signed_content))

Signing Secret

Your signing secret is available on the webhook settings page and follows the format whsec_<base64>. Keep this value secure — it is used to verify that incoming requests genuinely originated from MoveUSD.

Secret Rotation

You can rotate your signing secret from the settings page. During the rotation window (24 hours by default), both the old and new secrets are valid for signature verification, so you can update your consumer without downtime.

Verifying Webhook Signatures

We strongly recommend verifying signatures on every incoming webhook to ensure authenticity and protect against replay attacks.

Using the Standard Webhooks SDK (recommended)

The easiest approach is to use the official standardwebhooks library, available for JavaScript/TypeScript, Python, Go, Ruby, Java/Kotlin, PHP, C#, Rust, and Elixir.

JavaScript / TypeScript:

npm install standardwebhooks
# or
yarn add standardwebhooks
import { Webhook } from "standardwebhooks";

const secret = "whsec_YOUR_SIGNING_SECRET";
const wh = new Webhook(secret);

// headers from the incoming request
const headers = {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
};

// IMPORTANT: use the raw request body string, not a parsed/re-serialized object
const payload = req.body;

try {
  wh.verify(payload, headers);
  // Signature is valid — process the event
} catch (err) {
  // Signature verification failed — reject the request
  res.status(401).send("Invalid signature");
}

Python:

pip install standardwebhooks
from standardwebhooks import Webhook

secret = "whsec_YOUR_SIGNING_SECRET"
wh = Webhook(secret)

headers = {
    "webhook-id": request.headers["webhook-id"],
    "webhook-timestamp": request.headers["webhook-timestamp"],
    "webhook-signature": request.headers["webhook-signature"],
}

# Use the raw request body bytes
payload = request.body

try:
    wh.verify(payload, headers)
    # Valid — process the event
except Exception:
    # Invalid — reject
    pass

Important: Always use the raw request body when verifying signatures. Parsing the body as JSON and re-serializing it can alter the content (e.g. whitespace, number formatting), which will cause verification to fail.

Manual Verification

If you prefer not to use the SDK, you can verify signatures manually:

  1. Extract the webhook-id, webhook-timestamp, and webhook-signature headers from the request.
  2. Build the signed content string: ${webhook-id}.${webhook-timestamp}.${body} (where body is the raw request body).
  3. Decode the base64 portion of your signing secret (the part after whsec_).
  4. Compute the HMAC-SHA256 of the signed content using the decoded secret.
  5. Base64-encode the result and compare it to the signature value (the part after v1, in the header) using a constant-time comparison.
  6. Optionally, reject requests where the webhook-timestamp is more than 5 minutes from the current time to prevent replay attacks.

Delivery & Retries

MoveUSD operates on an at-least-once delivery guarantee — events are guaranteed to be delivered but may occasionally be sent more than once. Use the webhook-id header to deduplicate events you have already processed.

Automatic Retries

If your endpoint is unavailable, returns a non-2xx HTTP status code, or fails to respond within the timeout window, delivery will be retried automatically using exponential backoff (base 2) for up to 10 attempts.

As a best practice, accept the webhook quickly (return a 200 response), then process the event asynchronously. This avoids timeouts and reduces the chance of unnecessary retries.

Disabled Destinations

If your endpoint consistently fails to accept events (20 consecutive failures), the destination will be automatically disabled and further events will be discarded. You can re-enable the destination from the webhook settings page once the issue is resolved.

Payload

All webhook events have a consistent JSON payload.

{
  "event": "<event code>",
  "createdAt": "<ISO timestamp of when the event was generated>",
  "customerId": "<Your customer ID>",
  "data": {
    "id": "<ID of the object that has been updated>",
    "status": "<The new status of the object>"
  }
}

If the event is relevant to an Identity or Organization, additional fields are available at the root of the structure: identityId and identityReferenceId (or organizationId and organizationReferenceId).

Some events may include additional fields in the data section. These have been documented in the next section per event.

That said, most events will include only id and status. The expected integration pattern is to perform a GET operation on the given resource to get the latest data for that resource, when a new event arrives. This is to protect consumers from out-of-order delivery issues and they are always working with the latest information about an object.

Events

The following is the exhaustive list of events that you may receive.

event

GET operation

Notes

account.ledgerAccount.created

Get a ledger account details

No status field

card.cardTransaction.statusUpdated

N/A

customer.terms.statusUpdated

N/A

Example of data field: [{"type":"MOVEUSD_TERMS_OF_USE","status":"ACCEPTED"},{"type":"COOKIES_POLICY","status":"ACCEPTED"},{"type":"ESIGN_POLICY","status":"ACCEPTED"},{"type":"PRIVACY_POLICY","status":"ACCEPTED"}]

deposit.cashRequest.statusUpdated

Get Cash Deposit Request

Example of data field: {"status":"EXPIRED","cashDepositRequestId":"cshdprq_nloJZiL5DWaxkdx3joPsV","cashDepositRequestReference":"ad4e0289-6b55-4bcd-a31c-50b8ec418b9c","cashDepositTransactionReference":"180f02ad-abe3-4a88-b190-ea62807429ee","amount":{"currency":"MOVEUSD","amount":20}}

deposit.direct.statusUpdated

N/A

deposit.deposit.statusUpdated

Get Deposit

Example of data field: {"status":"PROCESSING","type":"US_BANK_ACH","depositId":"dp_AM6nDeazjlh9kq7xuoqEl","depositReference":"063b5d0e-668f-4fae-87fd-8e81b6b5e9b5"}

identity.identity.registered

Get an identity

No id field; Already provided at root level via identityId.

identity.identity.statusUpdated

Get an identity

No id field; Already provided at root level via identityId.

identity.verification.statusUpdated

N/A

Examples of data field:

  • {"type": "AML", "status": "APPROVED"}

  • {"type": "IDENTITY_DOCUMENT", "status": "DECLINED"}

Refer to *Verification fields in Get an identity for possible values for status.

organization.organization.created

Get Organization

No id field; Already provided at root level via organizationId.

organization.organization.statusUpdated

Get Organization

No id field; Already provided at root level via organizationId.

paymentInstrument.afBank.statusUpdated

Get Payment Instrument

paymentInstrument.afMomo.statusUpdated

Get Payment Instrument

paymentInstrument.mxClabe.statusUpdated

Get Payment Instrument

paymentInstrument.networkWallet.statusUpdated

Get Payment Instrument

paymentInstrument.swiftWire.statusUpdated

Get Payment Instrument

paymentInstrument.usAch.statusUpdated

Get Payment Instrument

paymentInstrument.usWire.statusUpdated

Get Payment Instrument

paymentInstrument.wallet.statusUpdated

Get Payment Instrument

redemption.transfer.statusUpdated

Get an existing transfer by ID

reward.reward.created

Get Reward

Example of data field: {"id":"rew_XvMToLmvLm74ve0GVTBOb","transactionType":"DEBIT","amount":{"amount":50,"currency":"MOVEUSD"},"phone":"+123456789","idempotencyKey":"dc5a2616-7711-43dc-b376-4b7d1f4b3c5e","category":"Rewards","note":"Good one!","createdAt":"2025-10-21T20:14:29.771Z"}

swap.swap.statusUpdated

Get Swap

withdrawal.withdrawal.statusUpdated

Get Withdrawal

Please refer to the provided GET operation for the possible values for status.