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
- Getting started
- Registering and managing webhooks
- Verifying signatures
- Delivery, retries, and idempotency
- Event reference
Getting started
1. Build an endpoint
Your endpoint must:
- Be reachable over HTTPS on the public internet (no self-signed certificates).
- Accept
POSTrequests with aContent-Type: application/jsonbody. - Return a
2xxstatus 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
| Action | Method and path | Reference |
|---|---|---|
| Create a webhook | POST /payout/account/webhooks | Create a webhook configuration |
| List webhooks | GET /payout/account/webhooks | Get webhooks |
| Delete a webhook | DELETE /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
| Field | Type | Required | Description |
|---|---|---|---|
webhook_url | string | Yes | The HTTPS URL that will receive the payloads. |
event_types | array of strings | Yes | The 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_secretnow. It is returned only on creation and is not available fromGET /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:
- Create a new webhook with the desired configuration.
- Deploy your endpoint so it accepts both the old and new secrets.
- Confirm events are arriving on the new configuration.
- 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
| Element | Description |
|---|---|
t | Unix timestamp (seconds) at which the signature was generated. |
s | Lowercase 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 orrequest.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.
| Behavior | Value |
|---|---|
| Success response | Any 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 guarantee | At least once |
| Ordering | Not 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:
| Field | Type | Description |
|---|---|---|
event_type | string | Identifies which event fired. Switch on this value. |
occurred_at | string | RFC 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
| Topic | Delivers | Payload event_type |
|---|---|---|
payout.created | A payout is created | payout |
payout.status | A payout changes status | payout |
payout_reversal.created | A reversal is created against a payout | payout_reversal_created |
payout_reversal.status | A payout reversal changes status | payout_reversal_status |
payin.created | A payin is created | payin_status |
payin.status | A payin changes status | payin_status |
payin_deposit.return_completed | A deposit against a payin is returned | payin_deposit_return_completed |
load.created | A load is created | load |
load.status | A load changes status | load |
load.reversed | A load is cancelled or reversed | load_reversal |
spendback.created | A spendback is created | spendback |
spendback.status | A spendback changes status | spendback |
spendback.inactivity | An inactivity spendback is applied | inactivity_spendback |
user.created | A user is created | user_created |
user.status | A user's status changes | user_status_update |
business.created | A business is created | business_created |
business.status | A business's status changes | undocumented |
kyc.id | Identity verification activity, including forwarded ID provider payloads | id_requested, veriff, file_upload_req_created, file_upload_req_uploaded, file_upload_req_verified |
kyc.tax | Tax interview and TIN matching activity | tax_wh_interview, tax_tin_matching |
avs.completed | Account ownership verification completes | avs.completed |
attributes.stored | User attributes are stored | stored_attrs |
deposit_account.created | A deposit account is issued | new_deposit_account |
balance.credit | Your account balance is credited | balance_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.idandkyc.taxgroupings — the mapping above is inferred from the payloads, not from the schema.
Payload event_type values
event_type valuesThis is what arrives in the body. Jump to any event for its full payload.
event_type | Category | Fires when |
|---|---|---|
payout | Payouts | A payout transaction is created or changes status |
payout_reversal_created | Payouts | A reversal is created against a payout |
payout_reversal_status | Payouts | A payout reversal changes status |
payin_status | Payins | A payin is created or changes status |
payin_deposit.return_completed | Payin deposits | A deposit against a payin is returned |
load | Loads | A scheduled load is created or changes status |
load_reversal | Loads | A load is cancelled or reversed |
spendback | Loads | A spendback is created or changes status |
inactivity_spendback | Loads | An inactivity spendback is applied |
user_created | Users | A user is created |
business_created | Businesses | A business is created |
stored_attrs | Attributes | User attributes are stored |
user_status_update | Users | A user's status changes |
id_requested | Verification | ID verification is requested from a user |
veriff | Verification | An ID provider returns a verification decision |
avs.completed | Verification | Account ownership verification completes |
file_upload_req_created | Documents | A file upload link is created |
file_upload_req_uploaded | Documents | A file is uploaded against a link |
file_upload_req_verified | Documents | An uploaded file is verified |
tax_wh_interview | Tax | A tax interview is completed |
tax_tin_matching | Tax | TIN matching returns a result |
new_deposit_account | Account | A deposit account is issued |
balance_credit | Account | Your account balance is credited |
Payouts
payout
payoutSent 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_amountissource_amount × exchange_rate.feeis charged in the source currency and is not deducted fromdestination_amount.status_reasonis populated only when a status carries an explanatory reason, such as a failure or a hold.- Use
client_transfer_idto reconcile against your own records.
payout_reversal_created
payout_reversal_createdSent 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_amountis the full amount of the original payout;destination_amountis the amount of this reversal.total_reversed_amountis 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
payout_reversal_statusSent 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_tokento update the record created bypayout_reversal_created. - This event carries
user_token,created, andcompleted, which the creation event does not. completedis 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
payin_statusSent 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_amountis the expected amount;source_amount_collectedis 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_atisnulluntil 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
payin_deposit.return_completedSent 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
tokenis the deposit token. It matches an entry in thedepositsarray of the correspondingpayin_statusevent — use it to locate the record to reverse, andpayin_tokento 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_atremains the original completion time.returned_atis when the return settled, andupdated_attracks it.
Return fields
| Field | Description |
|---|---|
return_reason_code | MassPay's normalized reason for the return. |
network_return_code | The raw code from the banking network, such as an ACH return code or a wire reason code. |
return_reason | Human-readable explanation. Suitable for display or support tooling, but do not branch on its text. |
return_fee | Fee charged for processing the return, separate from the original fee. |
fee_payer | Who bears return_fee. |
trace_code | Network 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. |
metadatais a JSON-encoded string on this event, not an object — note the escaped quotes in the example. The same field is a native object insidedeposits[]onpayin_status. Parse it a second time here, and guard against it being absent or empty.
Loads
load
loadSent 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
load_reversalSent 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
spendbackSent 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
inactivity_spendbackSent 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
metadatais a free-form object and its contents are not guaranteed. Do not require any particular key.- Precision on
occurred_atvaries between events. Parse it as an RFC 3339 timestamp rather than matching a fixed number of fractional digits.
Users
user_created
user_createdSent 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_urlcontains 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 theexpclaim.middle_nameis an empty string rather thannullwhen not supplied. Do not test for null alone.statusis the user's status at creation, using the same values asuser_status_update.countryis a three-letter ISO 3166-1 alpha-3 code, matching payout and payin events.
user_status_update
user_status_updateSent 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
business_createdSent 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.numberholds a government-issued identifier of the type given inrep_id.type. Store it only if you have a business reason to, and encrypt it at rest.rep_id.stateandrep_id.addressare empty strings when not supplied, notnull.phonehas no leading+, unlikemobile_numberonuser_created. Normalize before comparing across events.- The payload carries no business or user token. Correlate on
registration_numberorcompany_name.
Attributes
stored_attrs
stored_attrsSent 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_tokenis the same identifier that appears onpayout, so you can link a stored attribute set to the payouts that use it.- Precision on
occurred_atvaries between events. Parse it as an RFC 3339 timestamp rather than matching a fixed number of fractional digits.
Verification
id_requested
id_requestedSent 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
veriffSent 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
statusfields. The top-level one reports whether the provider's call succeeded; the verification decision isverification.status. Branch on the nested one. verification.vendorDatacarries the MassPayuser_token. Other providers carry it elsewhere — Persona, for example, uses its own reference field — so do not treatvendorDataas a general lookup path.- Everything under
verificationandtechnicalDatais Veriff's schema, including its camelCase naming, which differs from the snake_case used everywhere else in this reference.
avs.completed
avs.completedSent 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_codeis 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.
| Field | Description |
|---|---|
file_upload_token | Identifies the upload request across all three events. |
files_types_accepted | Document types the user may upload. |
is_required | Whether the upload blocks the user's progress. |
link | The URL to surface to the user. Present on creation. |
ref_attributes | Masked reference values to help the user identify what is being verified. |
note | Optional instructions to display to the user. |
file_upload_req_created
file_upload_req_createdSent 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
file_upload_req_uploadedSent 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
file_upload_req_verifiedSent 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
tax_wh_interviewSent 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
tax_tin_matchingSent 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
new_deposit_accountSent 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
balance_creditSent 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
| Symptom | Likely cause |
|---|---|
| Signature never matches | The body is being re-serialized before verification. Use the raw bytes. |
| Signature matches locally, fails in production | Load balancer or middleware is rewriting the body, or a different secret is configured per environment. |
| Requests rejected as stale | Server clock drift. Sync via NTP. |
| Duplicate records created | Handler is not idempotent, or is responding after the timeout and triggering retries. |
| Status appears to move backwards | Out-of-order delivery. Compare occurred_at before applying. |
| Events stop arriving | Endpoint may have been disabled after repeated failures. Check the dashboard. |
| One event type never arrives | The webhook is not subscribed to its topic. Check event_types via GET /payout/account/webhooks. |
Subscribed topic doesn't match the event_type received | Expected. Topics and payload event types are separate vocabularies — see Subscription topics. |
webhook_secret was lost | It is only returned at creation. Delete the webhook and create a replacement. |
