Sign in

.md

Exchange an email address and password for a token bound to this device, with or without naming the environment.

POST/v1/auth/token

Answers with one of three things: a token, a two-factor challenge, or a choice between environments. Branch on status, never on the HTTP code.

The token appears exactly once, here. There is no endpoint that hands it out again. Store it where the platform keeps secrets, the Keychain on iOS and the Keystore on Android, and never in ordinary storage.

Request

curl https://api.proppertrading.com/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "workspace": "acmefunded.com",
    "email": "[email protected]",
    "password": "the password they typed",
    "app": "mail",
    "scopes": ["me:read", "mail:write", "notifications:write", "devices:write"],
    "device_name": "Jamie iPhone",
    "platform": "ios",
    "os_version": "26.0",
    "app_version": "1.0.0"
  }'
FieldTypeDescription
workspacestringThe address of the environment. Optional
emailstringRequired
passwordstringRequired
appstringmail, admin or personal. Required
scopesarrayWhat this client wants. Required, at least one
device_namestringShown on the owner's security screen. Optional
platformstringios, android or web. Optional
os_versionstringOptional
app_versionstringOptional

Without an environment

Leave workspace out and the address and password decide. An address is unique per environment rather than across the platform, so it can belong to several accounts: the password is checked against each of them, and only the ones it is right for go on. One is an ordinary sign-in. More than one answers with a choice.

An environment is only ever named to somebody who just proved they have an account there. A wrong password and an unknown address answer invalid_login, in the same time, exactly as they do with an environment.

The device fields are decoration for a human reading a device list. They decide nothing, so there is no harm in them being wrong, and no point in making them clever.

Response, signed in

{
    "data": {
        "object": "access_token",
        "status": "signed_in",
        "token": "ptat_9f2c1a44_3b8e7d2f5c9a1b4e6d8f0a2c4e6b8d1f",
        "session": {
            "id": "9f2c1a44",
            "object": "session",
            "name": "Jamie iPhone",
            "app": "mail",
            "platform": "ios",
            "device_name": "Jamie iPhone",
            "os_version": "26.0",
            "app_version": "1.0.0",
            "current": true,
            "two_factor": false,
            "last_seen_at": null,
            "created_at": "2026-08-19T15:00:22+00:00",
            "expires_at": null
        },
        "workspace": {
            "object": "workspace",
            "id": "acmefunded",
            "name": "Acme Funded",
            "platform": false,
            "url": "https://acmefunded.com",
            "branding": {
                "logo_url": "https://acmefunded.com/storage/logo.webp",
                "logo_dark_url": null,
                "icon_url": null,
                "primary_color": "#3b82f6",
                "primary_color_dark": "#60a5fa",
                "primary_foreground": "#ffffff",
                "primary_foreground_dark": "#0b1530"
            },
            "maintenance": false
        }
    },
    "meta": { "request_id": "req_01kz4bkr1aj1gv63d8fv3133ey" }
}

Status is 201. workspace is the environment the account belongs to, in the same shape as resolving one, so a client that signed in without naming it can still look like it.

Response, second factor needed

{
    "data": {
        "object": "two_factor_challenge",
        "status": "two_factor_required",
        "challenge": "a14ea608e89261062f4e90792701ef2a7c5ad1188c8537108efd788d1390ce03",
        "expires_in": 300,
        "methods": ["passkey", "totp", "recovery_code"],
        "workspace": { "object": "workspace", "id": "acmefunded", "name": "Acme Funded", "platform": false, "url": "https://acmefunded.com", "branding": { "logo_url": null, "logo_dark_url": null, "icon_url": null, "primary_color": "#3b82f6", "primary_color_dark": null, "primary_foreground": "#ffffff", "primary_foreground_dark": null }, "maintenance": false }
    },
    "meta": { "request_id": "req_01kz4bkr1aj1gv63d8fv3133ey" }
}

Status is 200. Send the challenge on to the second step with one of the answers methods names.

methods lists only what this client can actually present, in the order worth offering. passkey appears when the account has a passkey registered through an app, which is not the same credential as a passkey on the firm's own website: a native app may only ask for domains built into it, so the two are separate. totp and recovery_code appear together or not at all.

Response, two-factor to set up

{
    "data": {
        "object": "two_factor_setup",
        "status": "two_factor_setup_required",
        "challenge": "c3a91f0e5b7d2c4e6a8f1b3d5c7e9a0b2d4f6a8c1e3b5d7f9a0c2e4b6d8f1a3c",
        "expires_in": 600,
        "methods": ["totp"],
        "workspace": { "object": "workspace", "id": "acmefunded", "name": "Acme Funded", "platform": false, "url": "https://acmefunded.com", "branding": null, "maintenance": false }
    },
    "meta": { "request_id": "req_01kz4bkr1aj1gv63d8fv3133ey" }
}

Status is 200, and only with "app": "admin". The account has to have two-factor authentication (an owner, our own staff, or a role that demands it) and has none yet, so it sets one up now, as the website makes it do after the password: ask for a secret, add it to an authenticator, and confirm it with a code, which finishes the sign-in. Every other app is refused with two_factor_setup_required instead, because the clients built before this step expect that.

Response, several environments

{
    "data": {
        "object": "workspace_choice",
        "status": "workspace_choice_required",
        "choice": "5d0c3f2e9a7b4c1d8e6f0a2b4c6d8e1f3a5b7c9d0e2f4a6b8c1d3e5f7a9b0c2d",
        "expires_in": 300,
        "workspaces": [
            { "object": "workspace", "id": "acmefunded", "name": "Acme Funded", "platform": false, "url": "https://acmefunded.com", "branding": { "logo_url": null, "logo_dark_url": null, "icon_url": null, "primary_color": "#3b82f6", "primary_color_dark": null, "primary_foreground": "#ffffff", "primary_foreground_dark": null }, "maintenance": false },
            { "object": "workspace", "id": "northfunding", "name": "North Funding", "platform": false, "url": "https://northfunding.com", "branding": null, "maintenance": false }
        ]
    },
    "meta": { "request_id": "req_01kz4bkr1aj1gv63d8fv3133ey" }
}

Status is 200, and only without workspace. Show the list and send the pick to the choice step. A switched-off account is left out of the list while another is on it.

Scopes you asked for and scopes you got

Read scopes back from your profile rather than assuming the list you sent. Anything a person's device may never hold is dropped silently, so an over-broad client keeps working and simply gets less.

Errors

StatusCodeMeaning
400workspace_not_foundNothing is served from that address
401invalid_loginWrong address, wrong password, or no such account here
401account_deactivatedThe account exists and has been switched off
403admin_access_requiredSigning in to the Propper app with an account that does not help run the environment
403two_factor_setup_requiredThe account has to have two-factor and has none. Set it up on the web first. With "app": "admin" this is a setup step instead
403two_factor_method_unavailableEvery second factor on the account is bound to a domain an app cannot ask for. Register a passkey through the app, or add an authenticator
403block_anonymizerThe firm blocks VPN, proxy and Tor connections
403block_countryThe firm blocks connections from this country
422validation_failedA field is missing or malformed
429rate_limit_exceededTen attempts per ten minutes per address, five per email
503tenant_in_maintenanceThe firm is closed for maintenance

A wrong password and an unknown address answer the same

Both give invalid_login, and both take the same time. This endpoint is not a way to find out who has an account with which firm.