Authentication

DEXPIRY uses JWT bearer tokens. There is no API-key header today. Choose the flow that matches the actor.

Flow Actor Endpoint
Email + password Merchant, admin POST /v1/auth/login
Claim link Customer POST /v1/customer/auth/claim
Magic link Customer POST /v1/customer/auth/magic-link
Request access Customer (email a new link) POST /v1/customer/auth/request-access

Every protected route:

Authorization: Bearer <jwt>

Missing or invalid tokens return 401 with error: "unauthorized". Wrong role (for example a customer token on a merchant route) returns 403 with error: "forbidden".

Merchant and admin login

curl -X POST https://api.dexpiry.com/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "yourPassword"}'

200 response (shape):

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "...",
    "name": "...",
    "email": "user@example.com",
    "role": "MERCHANT_USER",
    "merchantId": "...",
    "merchantName": "...",
    "merchantRole": "OWNER"
  }
}

role is one of ADMIN, MERCHANT_USER, CUSTOMER, SERVICE_PROVIDER. Merchant payloads include merchantId and merchantRole (OWNER | ADMIN | USER). Admin payloads include adminRole.

Errors:

HTTP error When
400 validation_error (or Zod details) Invalid email/password body
401 invalid_credentials Unknown email or wrong password
403 account_suspended User status is SUSPENDED
403 merchant_suspended Merchant organisation is not ACTIVE

Change password (authenticated):

curl -X PUT https://api.dexpiry.com/v1/auth/change-password \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"currentPassword": "old", "newPassword": "newPass1"}'

Customer claim link

Used by the landing page /u/[claimId] after a merchant issues a warranty and the customer opens the e-mail.

  1. Validate (read-only)GET /v1/customer/auth/validate-claim?claimId=<id>
  2. Consume and get JWTPOST /v1/customer/auth/claim
curl -X POST https://api.dexpiry.com/v1/customer/auth/claim \
  -H "Content-Type: application/json" \
  -d '{"claimId": "<claim-id>"}'

Optional focusWarrantyId (UUID) highlights a specific warranty in the returned wallet.

Expired links return 401 expired. Invalid links return 401 invalid_link. Resend:

curl -X POST https://api.dexpiry.com/v1/customer/auth/resend-link \
  -H "Content-Type: application/json" \
  -d '{"claimId": "<claim-id>"}'

Resend is rate-limited (429 too_many_requests).

Customer magic link

Used when the customer requests wallet access from the app (POST /v1/customer/auth/request-access with { "email": "..." }). The e-mail contains a one-time token.

Validate without consuming:

curl "https://api.dexpiry.com/v1/customer/auth/validate?token=<token>"

Consume and receive a JWT:

curl -X POST https://api.dexpiry.com/v1/customer/auth/magic-link \
  -H "Content-Type: application/json" \
  -d '{"token": "<token>"}'
HTTP error When
401 invalid_token Unknown or malformed
401 expired_token Past expiry
401 token_used Already consumed

Using the token

curl https://api.dexpiry.com/v1/merchant/stats \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
curl https://api.dexpiry.com/v1/customer/warranties \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Public routes (no JWT)