POST /portal/admin/users
SMP staff: create a merchant dashboard login
Host https://app.socialmatterspay.com
Request body
| Field | Notes |
|---|---|
merchant_id requiredstring | — |
email requiredstring (email) | — |
roleenum | owner when left out. one of owner, admin, finance, developer, viewer |
passwordstring | Kept for scripted setups only. Without it (the normal way) the login is emailed a one-time link to set its own password, valid 72 hours. at least 12 characters |
send_emailboolean | false when the CRM sends the link itself, inside its approval email (one email, not two) |
Responses
| Status | Meaning |
|---|---|
201 | OK |
400 | Error — error shape and codes |
401 | Error — error shape and codes |
Fields of data (201)
| Field | Notes |
|---|---|
objectstring = "portal_user" | — |
idstring | — |
merchant_idstring | — |
emailstring | The address, lower-cased |
roleenum | one of owner, admin, finance, developer, viewer |
set_password_urlstring | The one-time link where the login chooses a password. Present only when the request set no password |
set_password_expires_atstring (date-time) | When the link stops working (72 hours). Present only when set_password_url is |
Example
Create a dashboard login (SMP staff): its owner is emailed a link to set a password
curl -X POST https://app.socialmatterspay.com/portal/admin/users \
-H 'Authorization: Bearer smp_staff_token' \
-H 'Content-Type: application/json' \
-d '{
"merchant_id": "a5fc4446-e0a0-4260-ac10-e06060393364",
"email": "owner@northwind.example"
}'
HTTP 201
Cache-Control: no-store
{
"data": {
"object": "portal_user",
"id": "cb3f1ea5-3fbd-4960-9bfe-8b40da3f296e",
"merchant_id": "a5fc4446-e0a0-4260-ac10-e06060393364",
"email": "owner@northwind.example",
"role": "owner",
"set_password_url": "https://portal.smp.test/set-password?token=one_time_link_token",
"set_password_expires_at": "2026-10-31T00:22:37.851Z"
}
}
POST /portal/login
Step 1 of signing in: a right password returns a challenge (enrol an authenticator on the first sign-in, else verify a code); five failures in a row lock the login for 15 minutes
Host https://app.socialmatterspay.com
Request body
| Field | Notes |
|---|---|
email requiredstring (email) | — |
password requiredstring | — |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Error — error shape and codes |
403 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "sign_in_challenge" | — |
challengestring | The one-time challenge to send back with the authenticator code to POST /portal/login/verify |
stepenum | enroll: the login has no authenticator yet, so secret and otpauth_uri are included and the first code proves the enrolment; verify: the login's enrolled authenticator is asked for a code. one of enroll, verify |
expires_atstring (date-time) | — |
secretstring | The new authenticator secret, base32. Present only when step is enroll |
otpauth_uristring | The otpauth:// address of the secret, for the QR code. Present only when step is enroll |
Example
Sign in, step 1: the password
curl -X POST https://app.socialmatterspay.com/portal/login \
-H 'Content-Type: application/json' \
-d '{
"email": "owner@northwind.example",
"password": "a new long dashboard password"
}'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "sign_in_challenge",
"challenge": "sign_in_challenge",
"step": "verify",
"expires_at": "2026-10-28T00:33:02.361Z"
}
}
POST /portal/api/logout
Revoke the dashboard session
Host https://app.socialmatterspay.com
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "portal_session" | — |
revokedboolean = true | — |
Example
Sign out
curl -X POST https://app.socialmatterspay.com/portal/api/logout \
-H 'Authorization: Bearer portal_session_token'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "portal_session",
"revoked": true
}
}
GET /portal/api/me
The signed-in merchant: name, legal name, status, statement name, environment
Host https://app.socialmatterspay.com
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Error — error shape and codes |
Fields of data (200)
| Field | Notes | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
objectstring = "portal_me" | — | ||||||||||
merchant_idstring | — | ||||||||||
user_idstring | — | ||||||||||
merchant_namestring | null | — | ||||||||||
legal_namestring | null | — | ||||||||||
merchant_statusenum | A login on an account in any other status cannot sign in. one of approved, active, terminated, | ||||||||||
statement_descriptorstring | null | — | ||||||||||
emailstring | — | ||||||||||
roleenum | one of owner, admin, finance, developer, viewer | ||||||||||
closedboolean | True when the account is terminated: its records stay readable and nothing can be changed | ||||||||||
permissionsarray of enum | What this login may do, from its role; every change permission is left out when the account is closed. one of account, money, payments, developer, developer.write, payout.write, bank.write, settings.write, team.write | ||||||||||
environmentenum | one of production, sandbox | ||||||||||
buildobject | null | The account's website or app under review. Null unless the account is approved and has submitted a build5 child fields
|
Example
Who is signed in, and what their role may do
curl https://app.socialmatterspay.com/portal/api/me \
-H 'Authorization: Bearer portal_session_token'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "portal_me",
"merchant_id": "a5fc4446-e0a0-4260-ac10-e06060393364",
"user_id": "cb3f1ea5-3fbd-4960-9bfe-8b40da3f296e",
"merchant_name": "Northwind Supply",
"legal_name": "Northwind Supply LLC",
"merchant_status": "active",
"statement_descriptor": "NORTHWIND",
"email": "owner@northwind.example",
"role": "owner",
"closed": false,
"permissions": [
"account",
"money",
… 7 more
],
"environment": "sandbox",
"build": null
}
}
POST /portal/login/verify
Step 2 of signing in: an authenticator code (or a backup code) for the challenge; enrolment returns 10 backup codes once
Host https://app.socialmatterspay.com
Request body
| Field | Notes |
|---|---|
challenge requiredstring | — |
codestring | pattern ^[0-9]{6}$ |
backup_codestring | — |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Error — error shape and codes |
401 | Error — error shape and codes |
429 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "portal_session" | — |
tokenstring | The bearer token for the dashboard's API, shown only here. It lasts 12 hours |
merchant_idstring | — |
namestring | The account's display name |
roleenum | one of owner, admin, finance, developer, viewer |
expires_atstring (date-time) | — |
backup_codesarray of string | Ten one-time backup codes, shown only here. Present only when this sign-in enrolled the authenticator |
Examples (2)
Step 2: the first code from the authenticator app; enrolment returns 10 backup codes, once
curl -X POST https://app.socialmatterspay.com/portal/login/verify \
-H 'Content-Type: application/json' \
-d '{
"challenge": "sign_in_challenge",
"code": "123456"
}'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "portal_session",
"token": "portal_session_token",
"merchant_id": "a5fc4446-e0a0-4260-ac10-e06060393364",
"name": "Northwind Supply",
"role": "owner",
"expires_at": "2026-10-28T12:22:37.903Z",
"backup_codes": [
"backup_code_1",
"backup_code_2",
… 8 more
]
}
}
Step 2 on a later sign-in: a current code
curl -X POST https://app.socialmatterspay.com/portal/login/verify \
-H 'Content-Type: application/json' \
-d '{
"challenge": "sign_in_challenge",
"code": "123456"
}'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "portal_session",
"token": "portal_session_token",
"merchant_id": "a5fc4446-e0a0-4260-ac10-e06060393364",
"name": "Northwind Supply",
"role": "owner",
"expires_at": "2026-10-28T12:23:30.392Z"
}
}
POST /portal/password/set
Choose a password from a one-time link (72 hours), then go on to the second step
Host https://app.socialmatterspay.com
Request body
| Field | Notes |
|---|---|
token requiredstring | — |
password requiredstring | at least 12 characters |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Error — error shape and codes |
410 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "sign_in_challenge" | — |
challengestring | The one-time challenge to send back with the authenticator code to POST /portal/login/verify |
stepenum | enroll: the login has no authenticator yet, so secret and otpauth_uri are included and the first code proves the enrolment; verify: the login's enrolled authenticator is asked for a code. one of enroll, verify |
expires_atstring (date-time) | — |
secretstring | The new authenticator secret, base32. Present only when step is enroll |
otpauth_uristring | The otpauth:// address of the secret, for the QR code. Present only when step is enroll |
Example
Set the password from the link; the answer enrols an authenticator
curl -X POST https://app.socialmatterspay.com/portal/password/set \
-H 'Content-Type: application/json' \
-d '{
"token": "one_time_link_token",
"password": "correct horse battery staple"
}'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "sign_in_challenge",
"challenge": "sign_in_challenge",
"step": "enroll",
"expires_at": "2026-10-28T00:32:37.900Z",
"secret": "authenticator_secret",
"otpauth_uri": "otpauth://totp/Social%20Matter%20Payments%3Aowner%40northwind.example?secret=authenticator_secret&issuer=Social%20Matter%20Payments&algorithm=SHA1&digits=6&period=30"
}
}
POST /portal/password/forgot
Email a reset link (60 minutes); the same answer whether or not the email has a login
Host https://app.socialmatterspay.com
Request body
| Field | Notes |
|---|---|
email requiredstring (email) | — |
Responses
| Status | Meaning |
|---|---|
202 | OK |
400 | Error — error shape and codes |
Fields of data (202)
| Field | Notes |
|---|---|
objectstring = "password_reset_request" | — |
sent_if_knownboolean = true | Always true, whether or not the address has a login |
Example
Forgot the password: a reset link by email (the same answer for any address)
curl -X POST https://app.socialmatterspay.com/portal/password/forgot \
-H 'Content-Type: application/json' \
-d '{
"email": "owner@northwind.example"
}'
HTTP 202
Cache-Control: no-store
{
"data": {
"object": "password_reset_request",
"sent_if_known": true
}
}
POST /portal/password/reset
Set a new password from a reset link; every session of the login ends
Host https://app.socialmatterspay.com
Request body
| Field | Notes |
|---|---|
token requiredstring | — |
password requiredstring | at least 12 characters |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Error — error shape and codes |
410 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "password_reset" | — |
nextstring = "sign_in" | — |
Example
Set a new password from the reset link
curl -X POST https://app.socialmatterspay.com/portal/password/reset \
-H 'Content-Type: application/json' \
-d '{
"token": "one_time_link_token",
"password": "a new long dashboard password"
}'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "password_reset",
"next": "sign_in"
}
}
POST /portal/admin/users/{id}/set-password-link
SMP staff: email a fresh 72-hour set-password link; the previous one stops working
Host https://app.socialmatterspay.com
Parameters
| Field | Notes |
|---|---|
id requiredpath · string | — |
Responses
| Status | Meaning |
|---|---|
201 | OK |
401 | Error — error shape and codes |
404 | Error — error shape and codes |
Fields of data (201)
| Field | Notes |
|---|---|
objectstring = "set_password_link" | — |
user_idstring | — |
urlstring | The one-time link where the login chooses a password |
expires_atstring (date-time) | When the link stops working (72 hours) |
Example
Send a fresh set-password link (SMP staff); the first stops working
curl -X POST https://app.socialmatterspay.com/portal/admin/users/cb3f1ea5-3fbd-4960-9bfe-8b40da3f296e/set-password-link \
-H 'Authorization: Bearer smp_staff_token'
HTTP 201
Cache-Control: no-store
{
"data": {
"object": "set_password_link",
"user_id": "cb3f1ea5-3fbd-4960-9bfe-8b40da3f296e",
"url": "https://portal.smp.test/set-password?token=one_time_link_token",
"expires_at": "2026-10-31T00:22:37.858Z"
}
}
POST /portal/admin/users/{id}/reset-two-step
SMP staff: clear a login’s authenticator and backup codes; its sessions end and the next sign-in enrols again
Host https://app.socialmatterspay.com
Parameters
| Field | Notes |
|---|---|
id requiredpath · string | — |
Request body
| Field | Notes |
|---|---|
reason requiredstring | — |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Error — error shape and codes |
401 | Error — error shape and codes |
404 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "two_step_reset" | — |
user_idstring | — |
Example
Clear a login's authenticator after an identity check (SMP staff); the next sign-in enrols again
curl -X POST https://app.socialmatterspay.com/portal/admin/users/e8c75368-c8e9-4e94-8555-da1323d48889/reset-two-step \
-H 'Authorization: Bearer smp_staff_token' \
-H 'Content-Type: application/json' \
-d '{
"reason": "Lost phone; identity checked on a call"
}'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "two_step_reset",
"user_id": "e8c75368-c8e9-4e94-8555-da1323d48889"
}
}
GET /portal/admin/notifications
SMP staff, outside production: the emails queued for an address
Host https://app.socialmatterspay.com
Parameters
| Field | Notes |
|---|---|
to requiredquery · string | — |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Error — error shape and codes |
404 | Error — error shape and codes |
Fields of each item of data (200)
| Field | Notes | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
idstring | — | ||||||||||||||||||||
templateenum | one of portal.set_password, portal.team_invite, portal.password_reset, portal.new_login, partner.set_password, partner.password_reset, partner.new_login | ||||||||||||||||||||
to_emailstring | — | ||||||||||||||||||||
dataobject | The values the email is rendered from. Which keys are present depends on the template10 child fields
| ||||||||||||||||||||
statusenum | one of queued, sent, failed | ||||||||||||||||||||
created_atstring (date-time) | — |
Example
What the dashboard has emailed one address (SMP staff)
curl https://app.socialmatterspay.com/portal/admin/notifications?to=avery%40northwind.example \
-H 'Authorization: Bearer smp_staff_token'
HTTP 200
Cache-Control: no-store
{
"data": [
{
"id": "4085623c-af1d-4f70-a53e-64641796d9ef",
"template": "portal.team_invite",
"to_email": "avery@northwind.example",
"data": {
"business_name": "Northwind Supply",
"role": "finance",
"invited_by": "owner@northwind.example",
"link": "https://portal.smp.test/set-password?token=one_time_link_token",
"expires_at": "2026-10-31T00:23:02.156Z"
},
"status": "sent",
"created_at": "2026-10-28T00:23:02.157Z"
}
]
}
GET /portal/api/team
The account’s logins with their roles, invitation and two-step status
Host https://app.socialmatterspay.com
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Error — error shape and codes |
Fields of each item of data (200)
| Field | Notes |
|---|---|
objectstring = "team_member" | — |
idstring | — |
emailstring | — |
roleenum | one of owner, admin, finance, developer, viewer |
statusenum | invited: no password chosen yet; removed: the login was disabled. one of active, invited, removed |
two_stepboolean | Whether the login has enrolled an authenticator app |
last_login_atstring | null (date-time) | — |
created_atstring (date-time) | — |
Example
Teammates and their roles
curl https://app.socialmatterspay.com/portal/api/team \
-H 'Authorization: Bearer portal_session_token'
HTTP 200
Cache-Control: no-store
{
"data": [
{
"object": "team_member",
"id": "cb3f1ea5-3fbd-4960-9bfe-8b40da3f296e",
"email": "owner@northwind.example",
"role": "owner",
"status": "active",
"two_step": true,
"last_login_at": "2026-10-28T00:22:37.903Z",
"created_at": "2026-10-28T00:22:37.851Z"
},
{
"object": "team_member",
"id": "e8c75368-c8e9-4e94-8555-da1323d48889",
"email": "avery@northwind.example",
"role": "finance",
"status": "active",
"two_step": true,
"last_login_at": "2026-10-28T00:23:02.251Z",
"created_at": "2026-10-28T00:23:02.155Z"
},
{
"object": "team_member",
"id": "254d72fb-a59f-4275-8de8-8168d147f92f",
"email": "quinn@northwind.example",
"role": "viewer",
"status": "invited",
"two_step": false,
"last_login_at": null,
"created_at": "2026-10-28T00:23:02.198Z"
}
],
"roles": [
"admin",
"finance",
… 2 more
]
}
POST /portal/api/team/invites
Invite a teammate by email with a role (admin, finance, developer, viewer); they get a 72-hour set-password link
Host https://app.socialmatterspay.com
Request body
| Field | Notes |
|---|---|
email requiredstring (email) | — |
role requiredenum | one of admin, finance, developer, viewer |
Responses
| Status | Meaning |
|---|---|
201 | OK |
400 | Error — error shape and codes |
401 | Error — error shape and codes |
403 | Error — error shape and codes |
409 | Error — error shape and codes |
Fields of data (201)
The same fields as under portalTeam.
Example
Invite a teammate as finance
curl -X POST https://app.socialmatterspay.com/portal/api/team/invites \
-H 'Authorization: Bearer portal_session_token' \
-H 'Content-Type: application/json' \
-d '{
"email": "avery@northwind.example",
"role": "finance"
}'
HTTP 201
Cache-Control: no-store
{
"data": {
"object": "team_member",
"id": "e8c75368-c8e9-4e94-8555-da1323d48889",
"email": "avery@northwind.example",
"role": "finance",
"status": "invited",
"two_step": false,
"last_login_at": null,
"created_at": "2026-10-28T00:23:02.155Z"
}
}
POST /portal/api/team/{id}/role
Change a teammate’s role (never the owner’s, never your own)
Host https://app.socialmatterspay.com
Parameters
| Field | Notes |
|---|---|
id requiredpath · string | — |
Request body
| Field | Notes |
|---|---|
role requiredenum | one of admin, finance, developer, viewer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Error — error shape and codes |
401 | Error — error shape and codes |
403 | Error — error shape and codes |
404 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "team_member" | — |
idstring | — |
roleenum | one of admin, finance, developer, viewer |
Example
Change a teammate's role
curl -X POST https://app.socialmatterspay.com/portal/api/team/e8c75368-c8e9-4e94-8555-da1323d48889/role \
-H 'Authorization: Bearer portal_session_token' \
-H 'Content-Type: application/json' \
-d '{
"role": "viewer"
}'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "team_member",
"id": "e8c75368-c8e9-4e94-8555-da1323d48889",
"role": "viewer"
}
}
POST /portal/api/team/{id}/remove
Remove a teammate; their sessions and links end
Host https://app.socialmatterspay.com
Parameters
| Field | Notes |
|---|---|
id requiredpath · string | — |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Error — error shape and codes |
403 | Error — error shape and codes |
404 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "team_member" | — |
idstring | — |
statusstring = "removed" | — |
Example
Remove a teammate
curl -X POST https://app.socialmatterspay.com/portal/api/team/e8c75368-c8e9-4e94-8555-da1323d48889/remove \
-H 'Authorization: Bearer portal_session_token'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "team_member",
"id": "e8c75368-c8e9-4e94-8555-da1323d48889",
"status": "removed"
}
}
POST /portal/api/team/{id}/resend
Send an invited teammate a fresh set-password link
Host https://app.socialmatterspay.com
Parameters
| Field | Notes |
|---|---|
id requiredpath · string | — |
Responses
| Status | Meaning |
|---|---|
201 | OK |
401 | Error — error shape and codes |
403 | Error — error shape and codes |
404 | Error — error shape and codes |
409 | Error — error shape and codes |
Fields of data (201)
| Field | Notes |
|---|---|
objectstring = "team_invite" | — |
idstring | The teammate's id |
expires_atstring (date-time) | When the new link stops working (72 hours); the previous link stops now |
Example
Send an invitation again
curl -X POST https://app.socialmatterspay.com/portal/api/team/254d72fb-a59f-4275-8de8-8168d147f92f/resend \
-H 'Authorization: Bearer portal_session_token'
HTTP 201
Cache-Control: no-store
{
"data": {
"object": "team_invite",
"id": "254d72fb-a59f-4275-8de8-8168d147f92f",
"expires_at": "2026-10-31T00:23:02.204Z"
}
}
GET /portal/api/security
My two-step sign-in: enrolled, and how many backup codes are left
Host https://app.socialmatterspay.com
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Error — error shape and codes |
Fields of data (200)
| Field | Notes |
|---|---|
objectstring = "sign_in_security" | — |
two_stepboolean | — |
enrolled_atstring | null (date-time) | When the authenticator was enrolled; null if it has not been |
backup_codes_leftinteger | How many of the ten backup codes are unused |
Example
This login: its authenticator, backup codes left, recent sign-ins
curl https://app.socialmatterspay.com/portal/api/security \
-H 'Authorization: Bearer portal_session_token'
HTTP 200
Cache-Control: no-store
{
"data": {
"object": "sign_in_security",
"two_step": true,
"enrolled_at": "2026-10-28T00:22:37.903Z",
"backup_codes_left": 10
}
}
POST /portal/api/security/backup-codes
Ten new backup codes for a current authenticator code; shown once
Host https://app.socialmatterspay.com
Request body
| Field | Notes |
|---|---|
code requiredstring | — |
Responses
| Status | Meaning |
|---|---|
201 | OK |
401 | Error — error shape and codes |
403 | Error — error shape and codes |
409 | Error — error shape and codes |
Fields of data (201)
| Field | Notes |
|---|---|
objectstring = "backup_codes" | — |
backup_codesarray of string | Ten new one-time backup codes, shown only here. The previous codes stop working |
Example
Ten new backup codes, for a current code; shown once
curl -X POST https://app.socialmatterspay.com/portal/api/security/backup-codes \
-H 'Authorization: Bearer portal_session_token' \
-H 'Content-Type: application/json' \
-d '{
"code": "123456"
}'
HTTP 201
Cache-Control: no-store
{
"data": {
"object": "backup_codes",
"backup_codes": [
"backup_code_41",
"backup_code_42",
… 8 more
]
}
}
