Webhooks

Webhooks

Webhooks let MassPay notify your systems in real time instead of you polling our API. Every time a transaction, user, or verification object changes state, we send an HTTP POST to a URL you control.

On this page

  1. Getting started
  2. Registering and managing webhooks
  3. Verifying signatures
  4. Delivery, retries, and idempotency
  5. Event reference

Getting started

1. Build an endpoint

Your endpoint must:

  • Be reachable over HTTPS on the public internet (no self-signed certificates).
  • Accept POST requests with a Content-Type: application/json body.
  • Return a 2xx status code as soon as the payload is received. Do the actual work asynchronously — see Respond fast.

2. Register it

Create a webhook configuration through the API, in the client dashboard, or by sending the URL to your account representative.

3. Store the signing secret

Creating a webhook returns a webhook_secret. Store it in your secret manager immediately — it is used to verify every payload that webhook delivers, and it cannot be retrieved later. Never commit it to source control or expose it client-side.

4. Test

Trigger a test event and confirm your endpoint returns 2xx and that signature verification passes before going live.


Registering and managing webhooks

You can register multiple webhooks per account, each subscribed to a different set of events — for example, one endpoint for payout and payin events and a separate one for KYC and document events.

Requests are authenticated the same way as the rest of the API. See Authentication.

Base URL: https://{environment}.masspay.io/{VERSION}/payout/account/webhooks

ActionMethod and pathReference
Create a webhookPOST /payout/account/webhooksCreate a webhook configuration
List webhooksGET /payout/account/webhooksGet webhooks
Delete a webhookDELETE /payout/account/webhooks/{webhook_token}Delete a webhook configuration

Create a webhook

Provide a target URL and the list of events it should receive. On success you get back a webhook_token for managing the configuration, and a webhook_secret for verifying payloads.

Body parameters

FieldTypeRequiredDescription
webhook_urlstringYesThe HTTPS URL that will receive the payloads.
event_typesarray of stringsYesThe topics this webhook subscribes to. Must be values from the subscription topics list.
curl --request POST \
  --url https://api.masspay.io/v1.0.0/payout/account/webhooks \
  --header 'accept: application/json' \
  --header 'content-type: application/json' \
  --data '{
    "webhook_url": "https://example.com/masspay/webhooks",
    "event_types": ["payout.created", "payout.status", "payin.status", "user.status"]
  }'

201 Created

{
  "webhook_token": "3f8c2a91-5d47-4b6e-9a10-c72e5b4d81f0",
  "webhook_secret": "whsec_9K2mQ7pR4tX8vL1nB5cY3jH6dF0sA...",
  "webhook_url": "https://example.com/masspay/webhooks",
  "event_types": ["payout.created", "payout.status", "payin.status", "user.status"]
}

Save webhook_secret now. It is returned only on creation and is not available from GET /payout/account/webhooks. If you lose it, delete the webhook and create a new one.

List webhooks

Returns every webhook configuration on the account with its URL and subscribed events. No parameters required.

curl --request GET \
  --url https://api.masspay.io/v1.0.0/payout/account/webhooks \
  --header 'accept: application/json'

Delete a webhook

Pass the webhook_token as a path parameter. The webhook stops receiving events immediately and returns 204 No Content.

curl --request DELETE \
  --url https://api.masspay.io/v1.0.0/payout/account/webhooks/3f8c2a91-5d47-4b6e-9a10-c72e5b4d81f0 \
  --header 'accept: application/json'

Rotating a URL or secret

There is no update endpoint. To change a webhook's URL, secret, or event subscriptions:

  1. Create a new webhook with the desired configuration.
  2. Deploy your endpoint so it accepts both the old and new secrets.
  3. Confirm events are arriving on the new configuration.
  4. Delete the old webhook, then drop support for the old secret.

Running both briefly means you will receive duplicate events during the overlap — another reason handlers should be idempotent.


Verifying signatures

Every webhook includes an X-MassPay-Signature header. Verify it on every request — an unverified endpoint will accept forged payloads from anyone who learns your URL.

Header format

X-MassPay-Signature: t=1754500143,s=3a1f9c...e7b2
ElementDescription
tUnix timestamp (seconds) at which the signature was generated.
sLowercase hex-encoded HMAC SHA-512 of the signed payload.

Signed payload

Concatenate three things, in order:

<t value> + "|" + <raw request body>

Use the raw body bytes, exactly as received. Deserializing the JSON and re-serializing it will change key order and whitespace, and the signature will not match. Most frameworks require explicit configuration to preserve the raw body (for example, express.raw() in Express or request.get_data() in Flask).

Compute the HMAC of that string using the webhook_secret for the webhook configuration that sent the request, then compare it to s using a constant-time comparison function.

If you have registered multiple webhooks, each has its own secret. Route by endpoint path so each handler verifies against the right one.

Timestamp tolerance

Reject requests whose t value is more than 5 minutes away from your current time. This limits how long a captured payload can be replayed. Make sure your server clock is synced via NTP.

Examples

Node.js

const crypto = require("crypto");

function verifyMassPaySignature(header, rawBody, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=").map((s) => s.trim()))
  );
  const timestamp = parts.t;
  const received = parts.s;
  if (!timestamp || !received) return false;

  // Reject stale or future-dated requests.
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (age > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha512", secret)
    .update(`${timestamp}|${rawBody}`, "utf8")
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(received, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hashlib
import hmac
import time


def verify_masspay_signature(header, raw_body, secret, tolerance_seconds=300):
    parts = dict(kv.strip().split("=", 1) for kv in header.split(","))
    timestamp, received = parts.get("t"), parts.get("s")
    if not timestamp or not received:
        return False

    # Reject stale or future-dated requests.
    if abs(int(time.time()) - int(timestamp)) > tolerance_seconds:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        f"{timestamp}|{raw_body}".encode("utf-8"),
        hashlib.sha512,
    ).hexdigest()

    return hmac.compare_digest(expected, received)

PHP

function verify_masspay_signature($header, $raw_body, $secret, $tolerance = 300) {
    parse_str(str_replace(',', '&', $header), $parts);
    if (empty($parts['t']) || empty($parts['s'])) {
        return false;
    }
    if (abs(time() - (int) $parts['t']) > $tolerance) {
        return false;
    }
    $expected = hash_hmac('sha512', $parts['t'] . '|' . $raw_body, $secret);
    return hash_equals($expected, $parts['s']);
}

Delivery, retries, and idempotency

Fill in the bracketed values below with your production policy before publishing. These are the questions integrators ask support first, so they belong here rather than in an email thread.

BehaviorValue
Success responseAny 2xx
Request timeout[N] seconds
Retry attempts[N]
Retry schedule[e.g. exponential backoff: 1m, 5m, 30m, 2h, 12h]
After final failure[e.g. endpoint disabled; alert email sent]
Delivery guaranteeAt least once
OrderingNot guaranteed
Source IPs[list, for allowlisting]

Respond fast

Acknowledge with a 2xx immediately, then queue the payload for processing. If your handler does its work before responding and exceeds the timeout, we treat the delivery as failed and retry it — even though you already processed it.

Expect duplicates

Retries and network conditions mean the same event can arrive more than once. Make your handler idempotent: key off the token in the payload (payout_token, payin_token, load_token, and so on) plus status, and ignore anything you have already applied.

Handle out-of-order events

Events are not guaranteed to arrive in the order they occurred. Every payload carries an occurred_at timestamp — compare it against the last one you stored for that object and discard anything older. Do not infer ordering from arrival time.

Verify before trusting

Treat the payload as a notification, not as authoritative state. For anything financially significant, verify against the corresponding API endpoint before releasing funds or marking a record final.


Event reference

Every payload includes:

FieldTypeDescription
event_typestringIdentifies which event fired. Switch on this value.
occurred_atstringRFC 3339 UTC timestamp of when the event occurred.

Handle unrecognized event_type values gracefully — new events are added over time, and a handler that throws on an unknown type will start failing after a release.

All events

The values below are what you pass in event_types when creating a webhook. A webhook only receives the topics it is subscribed to; if an expected event never arrives, check the subscription with GET /payout/account/webhooks before investigating your endpoint.

Subscription topics and payload event_type values are two different vocabularies. You subscribe to a topic; the payload that arrives identifies itself with an event_type. Some topics deliver more than one event_type. Always branch on the event_type in the body, not on what you subscribed to.

Subscription topics

TopicDeliversPayload event_type
payout.createdA payout is createdpayout
payout.statusA payout changes statuspayout
payout_reversal.createdA reversal is created against a payoutpayout_reversal_created
payout_reversal.statusA payout reversal changes statuspayout_reversal_status
payin.createdA payin is createdpayin_status
payin.statusA payin changes statuspayin_status
payin_deposit.return_completedA deposit against a payin is returnedpayin_deposit_return_completed
load.createdA load is createdload
load.statusA load changes statusload
load.reversedA load is cancelled or reversedload_reversal
spendback.createdA spendback is createdspendback
spendback.statusA spendback changes statusspendback
spendback.inactivityAn inactivity spendback is appliedinactivity_spendback
user.createdA user is createduser_created
user.statusA user's status changesuser_status_update
business.createdA business is createdbusiness_created
business.statusA business's status changesundocumented
kyc.idIdentity verification activity, including forwarded ID provider payloadsid_requested, veriff, file_upload_req_created, file_upload_req_uploaded, file_upload_req_verified
kyc.taxTax interview and TIN matching activitytax_wh_interview, tax_tin_matching
avs.completedAccount ownership verification completesavs.completed
attributes.storedUser attributes are storedstored_attrs
deposit_account.createdA deposit account is issuednew_deposit_account
balance.creditYour account balance is creditedbalance_credit

Fill in the gaps. Rows marked undocumented are subscribable but have no payload example on this page. Either add one or note that the topic is reserved.

Confirm the kyc.id and kyc.tax groupings — the mapping above is inferred from the payloads, not from the schema.

Payload event_type values

This is what arrives in the body. Jump to any event for its full payload.

event_typeCategoryFires when
payoutPayoutsA payout transaction is created or changes status
payout_reversal_createdPayoutsA reversal is created against a payout
payout_reversal_statusPayoutsA payout reversal changes status
payin_statusPayinsA payin is created or changes status
payin_deposit.return_completedPayin depositsA deposit against a payin is returned
loadLoadsA scheduled load is created or changes status
load_reversalLoadsA load is cancelled or reversed
spendbackLoadsA spendback is created or changes status
inactivity_spendbackLoadsAn inactivity spendback is applied
user_createdUsersA user is created
business_createdBusinessesA business is created
stored_attrsAttributesUser attributes are stored
user_status_updateUsersA user's status changes
id_requestedVerificationID verification is requested from a user
veriffVerificationAn ID provider returns a verification decision
avs.completedVerificationAccount ownership verification completes
file_upload_req_createdDocumentsA file upload link is created
file_upload_req_uploadedDocumentsA file is uploaded against a link
file_upload_req_verifiedDocumentsAn uploaded file is verified
tax_wh_interviewTaxA tax interview is completed
tax_tin_matchingTaxTIN matching returns a result
new_deposit_accountAccountA deposit account is issued
balance_creditAccountYour account balance is credited

Payouts

payout

Sent when a payout is created and on every subsequent status change. Subscribe to payout.created, payout.status, or both — the payload shape is identical; use status to tell them apart.

{
  "payout_token": "payout_ba4275f2-bae1-488d-9d6f-20af1cd83574",
  "client_transfer_id": "aEjn345",
  "source_currency_code": "USD",
  "destination_currency_code": "MXN",
  "country_code": "MEX",
  "payer_name": "Bank Deposit",
  "source_token": "clnt_wlt_ba4275f2-bae1-488d-9d6f-20af1cd83574",
  "destination_token": "dst_d2138fd0-00be-45a8-985f-4f5bde500962",
  "destination_amount": 1864.28,
  "source_amount": 100.5,
  "attr_set_token": "attr_set_b1a867c1-6e36-4525-b6d5-a20bac80e3b0",
  "exchange_rate": 18.55,
  "fee": 2.99,
  "expiration": "2026-08-28T05:40:58.475Z",
  "status": "PENDING",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "status_reason": "Beneficiary information is incorrect",
  "event_type": "payout"
}

Notes

  • destination_amount is source_amount × exchange_rate. fee is charged in the source currency and is not deducted from destination_amount.
  • status_reason is populated only when a status carries an explanatory reason, such as a failure or a hold.
  • Use client_transfer_id to reconcile against your own records.

payout_reversal_created

Sent when a reversal is created against an existing payout. Subscribe to payout_reversal.created. Reversals may be partial.

{
  "payout_reversal_token": "payout_rev_00900dc5-de31-4d37-8ba2-6f7460b19fb0",
  "source_amount": 100.5,
  "source_currency_code": "USD",
  "exchange_rate": 1,
  "destination_amount": 100.5,
  "destination_currency_code": "USD",
  "status": "PROCESSING",
  "payout_token": "payout_ea4b5932-91f0-4b91-9590-b27f6d666dcd",
  "metadata": {
    "key": "value"
  },
  "fee": 1.5,
  "client_transfer_id": "Reversal54543",
  "original_destination_amount": 250.0,
  "total_reversed_amount": 100.5,
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "payout_reversal_created"
}

Notes

  • original_destination_amount is the full amount of the original payout; destination_amount is the amount of this reversal.
  • total_reversed_amount is the cumulative amount reversed against the payout, including this reversal. In the example above, 100.50 of an original 250.00 has now been reversed.

payout_reversal_status

Sent on every status change for an existing payout reversal. Subscribe to payout_reversal.status.

{
  "payout_reversal_token": "payout_rev_5266a2a3-8811-4f78-9fc2-c97a2706584b",
  "user_token": "usr_ce29989d-6764-11f0-a026-06178c1a380f",
  "source_amount": 214.99,
  "original_destination_amount": 232.01,
  "source_currency_code": "USD",
  "exchange_rate": 1,
  "destination_amount": 214.99,
  "total_reversed_amount": 214.99,
  "destination_currency_code": "USD",
  "status": "COMPLETED",
  "client_transfer_id": "porev_pnc560IDoY46",
  "payout_token": "payout_2ec83f4c-894c-11f0-8023-0266f44cc279",
  "metadata": {
    "clawback_request_id": "claw_GQAZp3FCFjAsF"
  },
  "fee": 1,
  "event_type": "payout_reversal_status",
  "occurred_at": "2026-08-06T16:00:09.778113446Z",
  "created": "2026-08-01T15:32:17",
  "completed": "2026-08-06T16:00:09"
}

Notes

  • Match on payout_reversal_token to update the record created by payout_reversal_created.
  • This event carries user_token, created, and completed, which the creation event does not.
  • completed is populated only once the reversal reaches a terminal status.
  • In the example above, 214.99 of an original 232.01 payout has been reversed, leaving 17.02 outstanding.

Payins

payin_status

Sent when a payin is created and on every subsequent status change. Subscribe to payin.created, payin.status, or both. The deposits array contains the individual deposits applied against the payin.

{
  "payin_token": "payin_7264211a-a946-4193-afaf-2e0b16976866",
  "event_type": "payin_status",
  "client_transfer_id": "Invoice #1343",
  "source_currency_code": "USD",
  "destination_currency_code": "USD",
  "source_token": "orgn_35b2feae-2be7-44df-bdc7-fa7114a36dee",
  "destination_token": "clnt_wlt_3256e586-cdb1-433e-8390-687ee771d60d",
  "destination_amount": 12.1,
  "source_amount_collected": 12.1,
  "source_amount": 12.1,
  "exchange_rate": 1,
  "fee": 0.06,
  "time_initiated": "2026-07-17T06:43:02.248Z",
  "funding_code": "87108cc0-8c8f-41ae-a66f-3b20391d432e",
  "status": "COMPLETED",
  "country_code": "USA",
  "delivery_type": "BANK_DEPOSIT",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "payer_name": "Bank Deposit",
  "deposits": [
    {
      "token": "payin_dep_255f64a2-d026-49a3-abf3-143d261db741",
      "external_id": "4d035e3d-9618-4c26-b343-6d50a2ffad15",
      "status": "COMPLETED",
      "deposit_amount": 12.1,
      "deposit_currency": "USD",
      "fiat_amount": 12.1,
      "fee": 0.06,
      "fiat_currency": "USD",
      "transaction_reference": "021000021987654",
      "metadata": {},
      "created_at": "2026-07-17T06:43:12",
      "completed_at": "2026-07-17T06:43:12",
      "updated_at": "2026-07-17T06:43:12"
    }
  ]
}

Notes

  • source_amount is the expected amount; source_amount_collected is what actually arrived. They can differ when a payer under- or over-funds.
  • A payin can be satisfied by multiple deposits. Reconcile on deposits[].token, not on array position.
  • completed_at is null until the deposit reaches a terminal status.

Payin deposits

Deposit-level events describe the individual deposits that fund a payin. Each corresponds to an entry in the deposits array of a payin_status event.

payin_deposit.return_completed

Sent when an individual deposit against a payin is returned by the banking network. Subscribe to payin_deposit.return_completed. The funds have left your balance.

{
  "token": "payin_dep_7d3b0c15-9a2f-4e6b-8c41-2f5d9e8a7b64",
  "external_id": "4987aa65-8449-41f6-b828-b4a100fcec2c",
  "status": "RETURNED",
  "deposit_amount": 100,
  "deposit_currency": "USD",
  "fee": 0,
  "fiat_amount": 100,
  "fiat_currency": "USD",
  "transaction_reference": "20260805I1B7031R00019876",
  "metadata": "{\"deposit_source\":\"fedwire\"}",
  "created_at": "2026-08-05T09:15:33",
  "completed_at": "2026-08-05T09:15:34",
  "updated_at": "2026-08-09T11:20:51",
  "return_reason_code": "REFUSED_BY_BENEFICIARY",
  "network_return_code": "CUST",
  "return_reason": "Sender not recognized; customer refused the credit",
  "return_fee": 1,
  "fee_payer": "DEPOSITOR",
  "returned_at": "2026-08-09T11:20:51",
  "event_type": "payin_deposit_return_completed",
  "payin_token": "payin_ba4275f2-bae1-488d-9d6f-20af1cd83574",
  "user_token": "usr_1f2e3d4c-5b6a-7980-a1b2-c3d4e5f60718",
  "trace_code": "20260810MFP00057000244",
  "occurred_at": "2026-08-09T21:05:25.043Z"
}

Notes

  • token is the deposit token. It matches an entry in the deposits array of the corresponding payin_status event — use it to locate the record to reverse, and payin_token to find the parent payin.
  • A returned deposit does not necessarily return the whole payin. A payin satisfied by several deposits can have one returned while the others stand; recompute the payin's funded position rather than assuming it is now unfunded.
  • completed_at remains the original completion time. returned_at is when the return settled, and updated_at tracks it.

Return fields

FieldDescription
return_reason_codeMassPay's normalized reason for the return.
network_return_codeThe raw code from the banking network, such as an ACH return code or a wire reason code.
return_reasonHuman-readable explanation. Suitable for display or support tooling, but do not branch on its text.
return_feeFee charged for processing the return, separate from the original fee.
fee_payerWho bears return_fee.
trace_codeNetwork trace identifier for the return, distinct from transaction_reference on the original deposit. Quote it when raising a case with the network or with support.

metadata is a JSON-encoded string on this event, not an object — note the escaped quotes in the example. The same field is a native object inside deposits[] on payin_status. Parse it a second time here, and guard against it being absent or empty.


Loads

load

Sent when a load is created and on every subsequent status change. Subscribe to load.created, load.status, or both.

{
  "load_token": "ld_ba4275f2-bae1-488d-9d6f-20af1cd83574",
  "time_of_load": "2026-07-24T22:54:54.793Z",
  "client_load_id": "aEjn345",
  "source_token": "usr_wlt_ba4275f2-bae1-488d-9d6f-20af1cd83574",
  "wallet_token": "usr_wlt_ba4275f2-bae1-488d-9d6f-20af1cd83574",
  "amount": 100.5,
  "source_currency_code": "USD",
  "notes": "Commission payment for July",
  "status": "COMPLETED",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "load"
}

load_reversal

Sent when a load is cancelled or reversed. Subscribe to load.reversed.

{
  "load_token": "ld_ba4275f2-bae1-488d-9d6f-20af1cd83574",
  "time_of_reversal": "2026-07-26T22:54:54.793Z",
  "user_token": "usr_123e4567-e89b-12d3-a456-426614174000",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "load_reversal"
}

spendback

Sent when a spendback is created and on every subsequent status change. Subscribe to spendback.created, spendback.status, or both.

{
  "spendback_token": "123e4567-e89b-12d3-a456-426614174000",
  "client_spendback_id": "aEjn345",
  "status": "success",
  "event_type": "spendback",
  "metadata": { "property": "value" },
  "user_token": "usr_6bee1675-5201-4b19-a44b-5c76b7e70a26",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "amount": 100.5
}

inactivity_spendback

Sent when an inactivity spendback is applied against a user's wallet. Subscribe to spendback.inactivity.

This event uses the same schema as spendback and differs only in event_type. A single handler can process both — branch on event_type if inactivity charges need different downstream treatment, such as notifying the user or recording the charge as a fee rather than a transfer.

{
  "spendback_token": "7c1e9b40-3d52-4a8f-b6d1-9e40f2a7c531",
  "event_type": "inactivity_spendback",
  "occurred_at": "2026-08-06T16:00:09.778113Z",
  "user_token": "usr_6bee1675-5201-4b19-a44b-5c76b7e70a26",
  "amount": 5.0,
  "client_spendback_id": "inactivity_2026_08",
  "status": "success",
  "metadata": {
    "reason": "account_inactive_12_months",
    "last_activity_at": "2025-08-04T09:14:22Z"
  }
}

Notes

  • metadata is a free-form object and its contents are not guaranteed. Do not require any particular key.
  • Precision on occurred_at varies between events. Parse it as an RFC 3339 timestamp rather than matching a fixed number of fractional digits.

Users

user_created

Sent when a user is created. Subscribe to user.created.

{
  "user_token": "usr_fbe54cb8-91cc-11f1-af32-0613251ca259",
  "status": "ACTIVE",
  "created_on": "2026-08-06T19:28:21.000Z",
  "internal_user_id": "a7f3c2e9-4b81-4d6a-9f52-31c8e0b7d4a6",
  "address1": "111 Wall Street",
  "address2": "Suite 400",
  "city": "New York",
  "state_province": "NY",
  "postal_code": "10043",
  "country": "USA",
  "first_name": "Jordan",
  "middle_name": "",
  "last_name": "Rivera",
  "email": "[email protected]",
  "language": "en",
  "mobile_number": "+12125550142",
  "date_of_birth": "1994-07-21",
  "activation_url": "https://members.masspay.io/activation?activation=eyJhbGciOiJIUzI1NiJ9.<jwt>",
  "metadata": {
    "resource_owner_id": "user_9UM4mCcPiRGya"
  },
  "event_type": "user_created",
  "occurred_at": "2026-08-06T19:28:21.215505377Z"
}

Notes

  • This is the most sensitive payload MassPay sends. It contains the user's name, email, phone number, date of birth, and full address. Exclude it from application logs and error-reporting tools, and make sure your endpoint is not behind a shared or third-party proxy that retains request bodies.
  • activation_url contains a signed, single-use activation token. Anyone who obtains this URL can activate the account. Treat it as a credential: do not log it, do not store it beyond what is needed to deliver it to the user, and deliver it only over a channel the user controls. The token expires; see the exp claim.
  • middle_name is an empty string rather than null when not supplied. Do not test for null alone.
  • status is the user's status at creation, using the same values as user_status_update.
  • country is a three-letter ISO 3166-1 alpha-3 code, matching payout and payin events.

user_status_update

Sent whenever a user's status changes. Subscribe to user.status. Both the previous and new status are included so you can detect specific transitions.

{
  "user_token": "usr_123e4567-e89b-12d3-a456-426614174000",
  "new_status": "LOCKED",
  "previous_status": "ACTIVE",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "user_status_update"
}

Businesses

business_created

Sent when a business is created. Subscribe to business.created.

{
  "address1": "6942 Richmond Dr.",
  "phone": "12125550142",
  "company_name": "Northwind Trading Corp",
  "rep_first_name": "Alex",
  "rep_last_name": "Chen",
  "rep_email": "[email protected]",
  "rep_dob": "1988-04-07",
  "rep_id": {
    "dob": "1988-04-07",
    "name": "Alex Chen",
    "state": "",
    "number": "XXX-XX-8214",
    "address": "",
    "exp_date": "2034-08-06",
    "issue_date": "2025-08-05",
    "type": "SOCIAL_SECURITY",
    "country_code": "USA"
  },
  "incorporation_date": "2024-05-29",
  "registration_number": "00-0000000",
  "legal_structure": "Ccorp",
  "event_type": "business_created",
  "occurred_at": "2026-08-06T01:47:16.465932013Z"
}

Notes

  • This payload identifies a named individual and includes their date of birth and government identifier. Exclude it from application logs and error-reporting tools.
  • rep_id.number holds a government-issued identifier of the type given in rep_id.type. Store it only if you have a business reason to, and encrypt it at rest.
  • rep_id.state and rep_id.address are empty strings when not supplied, not null.
  • phone has no leading +, unlike mobile_number on user_created. Normalize before comparing across events.
  • The payload carries no business or user token. Correlate on registration_number or company_name.

Attributes

stored_attrs

Sent when a set of attributes is stored against a user. Subscribe to attributes.stored.

{
  "user_token": "usr_685a6135-b7c0-44aa-9e10-dc403acb2cc8",
  "attr_set_token": "attr_set_b1a867c1-6e36-4525-b6d5-a20bac80e3b0",
  "internal_user_id": "e21b95d7-8c04-4a3f-b7e6-5d92f1a08c34",
  "event_type": "stored_attrs",
  "occurred_at": "2026-08-06T19:28:21.215505Z"
}

Notes

  • This is a notification, not the data. It reports that attributes were stored, not what they contain. Fetch the values with Get user attributes using the attr_set_token.
  • attr_set_token is the same identifier that appears on payout, so you can link a stored attribute set to the payouts that use it.
  • Precision on occurred_at varies between events. Parse it as an RFC 3339 timestamp rather than matching a fixed number of fractional digits.

Verification

id_requested

Sent when identity verification is requested from a user. Delivered under the kyc.id topic. Surface link to the user so they can complete verification.

{
  "time_of_request": "2026-07-29 14:32:17",
  "link": "https://l.maspay.io/veriff",
  "event_type": "id_requested",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "user_token": "usr_685a6135-b7c0-44aa-9e10-dc403acb2cc8"
}

veriff

Sent when an ID verification provider returns a decision. Delivered under the kyc.id topic.

This event forwards the provider's own webhook payload. MassPay works with several identity providers — Veriff, EVS, Persona, and others — and the body you receive is whatever that provider sent, wrapped with MassPay's event_type and occurred_at. Which provider applies depends on how your account is configured.

The consequence for integrators: the payload shape is not stable across providers. Field names, nesting, status vocabularies, and how the MassPay user_token is carried all differ. Write your handler against the provider configured for your account, and treat a provider change as a breaking change to this event. If you are unsure which provider your account uses, ask your account representative.

The example below is a Veriff payload.

{
  "status": "success",
  "verification": {
    "id": "d73bf06a-1c5b-499c-ab54-5ad680a64543",
    "code": 9121,
    "person": {
      "gender": "M",
      "lastName": "MOUSE",
      "addresses": [
        {
          "fullAddress": "1600 PENNSYLVANIA AVENUE NW, WASHINGTON, DC 20500 UNITED STATES",
          "parsedAddress": {
            "city": "WASHINGTON",
            "street": "PENNSYLVANIA AVENUE",
            "country": "UNITED STATES",
            "postcode": "20500",
            "houseNumber": "1600"
          }
        }
      ],
      "firstName": "MICKEY",
      "dateOfBirth": "1998-03-09",
      "nationality": "US"
    },
    "reason": "Attempted deceit, device screen used",
    "status": "review",
    "comments": [],
    "document": {
      "type": "ID_CARD",
      "number": "123456DISNEY",
      "country": "US",
      "validFrom": "2021-12-22",
      "validUntil": "2031-12-21"
    },
    "reasonCode": 504,
    "vendorData": "usr_123e4567-e89b-12d3-a456-426614174000",
    "decisionTime": "2026-07-29T22:12:26.339342Z",
    "acceptanceTime": "2026-07-29T22:11:30.149961Z",
    "additionalVerifiedData": {}
  },
  "technicalData": {
    "ip": "192.168.1.1"
  },
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "veriff"
}

Notes on the Veriff shape specifically

  • There are two status fields. The top-level one reports whether the provider's call succeeded; the verification decision is verification.status. Branch on the nested one.
  • verification.vendorData carries the MassPay user_token. Other providers carry it elsewhere — Persona, for example, uses its own reference field — so do not treat vendorData as a general lookup path.
  • Everything under verification and technicalData is Veriff's schema, including its camelCase naming, which differs from the snake_case used everywhere else in this reference.

avs.completed

Sent when account ownership verification completes. Subscribe to avs.completed.

{
  "event_type": "avs.completed",
  "user_token": "usr_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "country_code": "BR",
  "status": "CLOSE_MATCH",
  "ownership_verified": true,
  "occurred_at": "2026-08-06T18:22:41.371Z",
  "match_score": 83
}

Note: country_code is a two-letter ISO 3166-1 alpha-2 code on this event, unlike the three-letter codes used on payout and payin events.


Documents

All three file upload events share the same shape and differ only in event_type. All are delivered under the kyc.id topic — subscribing to it gives you all three.

FieldDescription
file_upload_tokenIdentifies the upload request across all three events.
files_types_acceptedDocument types the user may upload.
is_requiredWhether the upload blocks the user's progress.
linkThe URL to surface to the user. Present on creation.
ref_attributesMasked reference values to help the user identify what is being verified.
noteOptional instructions to display to the user.

file_upload_req_created

Sent when a file upload link is created.

{
  "file_upload_token": "file_req_501199fc-b914-4a45-b5d4-6273ec62b31f",
  "files_types_accepted": [
    "Bank Statement",
    "Physical Check"
  ],
  "time_of_request": "2026-07-29 14:32:17",
  "is_required": true,
  "link": "https://l.maspay.io/abc123",
  "event_type": "file_upload_req_created",
  "ref_attributes": {
    "BankAccountNumber": "**********6789",
    "Routing Number": "**********4321"
  },
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "note": "Please include all pages of the bank statement",
  "user_token": "usr_685a6135-b7c0-44aa-9e10-dc403acb2cc8"
}

file_upload_req_uploaded

Sent when the user uploads a file against an existing link.

{
  "file_upload_token": "file_req_501199fc-b914-4a45-b5d4-6273ec62b31f",
  "files_types_accepted": [
    "Government issued ID"
  ],
  "time_of_request": "2026-07-29 15:32:17",
  "is_required": false,
  "ref_attributes": {},
  "event_type": "file_upload_req_uploaded",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "user_token": "usr_685a6135-b7c0-44aa-9e10-dc403acb2cc8"
}

file_upload_req_verified

Sent when an uploaded file has been reviewed and verified.

{
  "file_upload_token": "file_req_501199fc-b914-4a45-b5d4-6273ec62b31f",
  "files_types_accepted": [
    "Bank Statement"
  ],
  "time_of_request": "2026-07-29 16:32:17",
  "is_required": true,
  "ref_attributes": {},
  "event_type": "file_upload_req_verified",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "user_token": "usr_685a6135-b7c0-44aa-9e10-dc403acb2cc8"
}

Tax

tax_wh_interview

Sent when a user completes a tax interview. Delivered under the kyc.tax topic. pdf_base64 contains the completed form.

{
  "user_token": "usr_c06de89a-b6cb-4c75-8707-10ba8c5b69e4",
  "pdf_base64": "H4sAAAA",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "tax_wh_interview"
}

Handle with care. This payload contains a tax document. It is significantly larger than other webhooks — size your request body limits accordingly — and it should be excluded from application logs and error-reporting tools.

tax_tin_matching

Sent when TIN matching returns a result. Delivered under the kyc.tax topic. tin is masked.

{
  "user_token": "usr_da518958-1413-11f0-9de5-06cab49e9a05",
  "name": "MICKEY MOUSE",
  "tin_type": "SSN",
  "tin": "XXX-XX-0657",
  "status": "SUCCESS",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "tax_tin_matching"
}

Account

new_deposit_account

Sent when a deposit account is issued for funding your balance. Subscribe to deposit_account.created.

{
  "account_number": "12345678",
  "routing_number": "012345678",
  "institution": "CRB",
  "topup_token": "topup_68f4fdfb-6586-441a-8cbb-9f9e352c0b1e",
  "funding_token": "fund_9767edf2-04e4-4a4a-9d8c-afe5fa5f2ba8",
  "title": "Company ABC",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "new_deposit_account"
}

balance_credit

Sent when your account balance is credited. Subscribe to balance.credit.

{
  "amount": 15.1,
  "currency": "USD",
  "source": "Wells Fargo",
  "occurred_at": "2026-07-29T15:25:43.412751752Z",
  "event_type": "balance_credit"
}

Troubleshooting

SymptomLikely cause
Signature never matchesThe body is being re-serialized before verification. Use the raw bytes.
Signature matches locally, fails in productionLoad balancer or middleware is rewriting the body, or a different secret is configured per environment.
Requests rejected as staleServer clock drift. Sync via NTP.
Duplicate records createdHandler is not idempotent, or is responding after the timeout and triggering retries.
Status appears to move backwardsOut-of-order delivery. Compare occurred_at before applying.
Events stop arrivingEndpoint may have been disabled after repeated failures. Check the dashboard.
One event type never arrivesThe webhook is not subscribed to its topic. Check event_types via GET /payout/account/webhooks.
Subscribed topic doesn't match the event_type receivedExpected. Topics and payload event types are separate vocabularies — see Subscription topics.
webhook_secret was lostIt is only returned at creation. Delete the webhook and create a replacement.