npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@radmas/mtx-external-api

v0.1.1

Published

TypeScript SDK for the MTX ticketing platform API

Downloads

12

Readme

@mtx/api

TypeScript SDK for the MTX ticketing platform API

npm version license


Table of Contents


Installation

npm install @mtx/api

The package ships as ESM with full TypeScript type declarations.


Quick Start

import { createMTXClient } from '@mtx/api';

// 1. Create the client
const mtx = createMTXClient({
  baseUrl: 'https://api.example-city.mtx.com',
  clientId: 'my-app-client-id',
});

// 2. Authenticate
const auth = await mtx.auth.login({
  username: '[email protected]',
  password: 's3cret',
});
console.log('Logged in, token expires in', auth.expiresIn, 'seconds');

// 3. List services for a jurisdiction
const services = await mtx.services.list({
  jurisdictionCodes: ['city_example'],
});
console.log(`Found ${services.length} services`);

// 4. Create a request (issue report)
const created = await mtx.requests.create({
  serviceId: services[0].id!,
  jurisdictionCode: 'city_example',
  description: 'Pothole on Main Street',
  lat: 40.4168,
  lng: -3.7038,
  addressString: 'Main Street 42',
});
console.log('Request created:', created.serviceRequestId);

Usage Example

End-to-end example: register a user, discover services, create a request with both the direct API and the fluent builder, then retrieve the created requests.

import { createMTXClient } from '@mtx/api';

const JURISDICTION = 'your_jurisdiction_code';

// 1. Create the client
const mtx = createMTXClient({
  baseUrl: 'https://api.your-city.mtx.com',
  clientId: 'your-client-id',
});

// 2. Register a new user
await mtx.auth.register({
  username: '[email protected]',
  password: 'SecurePass123!',
  firstName: 'Jane',
  lastName: 'Doe',
  legalTermsAccepted: true,
  jurisdictionCode: JURISDICTION,
});

// 3. Get jurisdiction detail for origin device and jurisdiction element
const jurisdiction = await mtx.jurisdictions.detail({
  jurisdictionCode: JURISDICTION,
});

const originDeviceId = jurisdiction.originDevices?.[0]?.id;
const jurisdictionElementId = jurisdiction.jurisdictionElements?.[0]?.id;

// 4. Find a service (e.g. street lighting)
const services = await mtx.services.list({
  jurisdictionCodes: [JURISDICTION],
});

const service = services.find(
  (s) => s.visibleName?.toLowerCase().includes('luminarias'),
);

// 5. Get service detail to discover required additional data questions
const serviceDetail = await mtx.services.detail({
  serviceId: service.id,
  jurisdictionCode: JURISDICTION,
});

// 6. Build additional data answers from the service's configurable questions
const additionalData = [];

for (const cq of serviceDetail.additionalData?.configurableQuestions ?? []) {
  const q = cq.question;
  if (!q?.id || q.type === 'label' || cq.omitInForm || !cq.required) continue;

  if (q.possibleAnswers?.length > 0) {
    additionalData.push({ question: q.id, value: q.possibleAnswers[0].value });
  } else {
    additionalData.push({ question: q.id, value: 'Your answer here' });
  }
}

// 7a. Create a request using the params object
const created = await mtx.requests.create({
  serviceId: service.id,
  jurisdictionCode: JURISDICTION,
  jurisdictionElementId,
  originDeviceId,
  description: 'Street light is off, has been broken for days.',
  addressString: 'Main Street, 64',
  lat: 40.4168,
  lng: -3.7038,
  firstName: 'Jane',
  lastName: 'Doe',
  phone: '+34600000000',
  public: true,
  additionalData,
});

console.log('Created:', created.serviceRequestId);

// 7b. Create a second request using the fluent builder
const builder = mtx.requests
  .build()
  .service(service.id)
  .jurisdiction(JURISDICTION)
  .jurisdictionElement(jurisdictionElementId)
  .originDeviceId(originDeviceId)
  .withServiceContext(serviceDetail)
  .description('Flickering street light near the school.')
  .address('Main Street, 80')
  .location(40.4170, -3.7035)
  .firstName('Jane')
  .lastName('Doe')
  .phone('+34600000000')
  .public(true);

// Add additional data answers to the builder
for (const ad of additionalData) {
  builder.additionalData(ad.question, ad.value);
}

// Validate before submitting
const validation = builder.isValid();
if (!validation.valid) {
  console.error('Validation errors:', validation.errors);
  process.exit(1);
}

const created2 = await builder.submit();
console.log('Created via builder:', created2.serviceRequestId);

// 8. Retrieve both requests
const request1 = await mtx.requests.detailByServiceRequestId({
  jurisdictionCode: JURISDICTION,
  serviceRequestId: created.serviceRequestId,
});
const request2 = await mtx.requests.detailByServiceRequestId({
  jurisdictionCode: JURISDICTION,
  serviceRequestId: created2.serviceRequestId,
});

for (const req of [request1, request2]) {
  console.log(`${req.serviceRequestId} — ${req.statusNode?.visibleName}`);
  console.log(`  ${req.description}`);
  console.log(`  ${req.addressString} (${req.lat}, ${req.lng})`);
}

Configuration

Create a client with createMTXClient(config?). Every option can be supplied directly or via an environment variable fallback.

| Option | Type | Default | Env Variable | Description | |---|---|---|---|---| | baseUrl | string | -- (required) | MTX_BASE_URL | Base URL of the MTX API instance | | clientId | string | -- (required) | MTX_CLIENT_ID | OAuth client identifier for your application | | jwtSecret | string | undefined | MTX_JWT_SECRET | Base64-encoded HMAC secret for JWT / SSO login | | geocoderSuffix | string | undefined | -- | String appended to addresses in location.resolve() (e.g. ", Spain") | | logger | MtxLogger | defaultLogger (console) | -- | Custom logger implementation | | onLegalTermsRequired | () => void \| Promise<void> | undefined | -- | Callback invoked when the API returns HTTP 451 (legal terms not accepted) |

import { createMTXClient } from '@mtx/api';

const mtx = createMTXClient({
  baseUrl: 'https://api.city.mtx.com',
  clientId: 'my-client',
  jwtSecret: 'bXktc2VjcmV0',          // only needed for loginJwt
  geocoderSuffix: ', Barcelona, Spain', // appended to addresses
  onLegalTermsRequired: () => {
    // redirect to terms acceptance screen
  },
});

Environment Variable Fallback

When an option is not provided in the config object, the SDK reads from process.env:

export MTX_BASE_URL=https://api.city.mtx.com
export MTX_CLIENT_ID=my-client
export MTX_JWT_SECRET=bXktc2VjcmV0
// No config needed if env vars are set
const mtx = createMTXClient();

Authentication

All authentication methods return an Authentication object and automatically store the tokens internally. Subsequent API calls use the stored access token.

interface Authentication {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
  tokenType: string;
}

Login with Username / Password

const auth = await mtx.auth.login({
  username: '[email protected]',
  password: 's3cret',
});

| Parameter | Type | Required | Description | |---|---|---|---| | username | string | Yes | User's email or username | | password | string | Yes | User's password |

Anonymous Login

const auth = await mtx.auth.loginAnonymous({
  deviceId: 'device-uuid-1234',
});

| Parameter | Type | Required | Description | |---|---|---|---| | deviceId | string | Yes | Unique device identifier |

JWT Login (SSO)

Requires jwtSecret in the client config. The SDK signs a JWT internally using HS256.

const auth = await mtx.auth.loginJwt({
  email: '[email protected]',
  firstName: 'Jane',
  lastName: 'Doe',
  register: true,
  legalTermsAccepted: true,
});

| Parameter | Type | Required | Description | |---|---|---|---| | email | string | Yes | User's email (validated against email regex) | | firstName | string | No | First name to set on the account | | lastName | string | No | Last name to set on the account | | register | boolean | No | If true, creates the account if it does not exist | | legalTermsAccepted | boolean | No | If true, marks legal terms as accepted |

Token Refresh

Manually refresh a session using a refresh token:

const auth = await mtx.auth.loginRefresh({
  refreshToken: 'existing-refresh-token',
});

| Parameter | Type | Required | Description | |---|---|---|---| | refreshToken | string | Yes | A previously obtained refresh token |

Registration

Register a new user and authenticate in one step:

const auth = await mtx.auth.register({
  username: '[email protected]',
  password: 'str0ngPa$$',
  firstName: 'John',
  lastName: 'Smith',
  legalTermsAccepted: true,
  jurisdictionCode: 'city_example', // optional
});

| Parameter | Type | Required | Description | |---|---|---|---| | username | string | Yes | Email or username for the new account | | password | string | Yes | Password for the new account | | firstName | string | Yes | First name | | lastName | string | Yes | Last name | | legalTermsAccepted | boolean | Yes | Whether legal terms are accepted | | jurisdictionCode | string | No | Default jurisdiction for the user |

Auto-Refresh

The SDK automatically refreshes expired tokens. When any API call is made:

  1. The AuthManager checks if the current access token will expire within the next 30 seconds.
  2. If so, it transparently calls the refresh endpoint using the stored refresh token.
  3. Concurrent requests share the same refresh promise (no duplicate refreshes).
  4. If the refresh fails, tokens are cleared and the request proceeds without authentication.

No manual intervention is needed after the initial login.

Logout

Close the server-side session and clear all local tokens:

await mtx.auth.logout();

| Parameter | Type | Required | Description | |---|---|---|---| | appKey | number | No | Application identifier (defaults to 2) | | deviceId | string | No | Device identifier for browser-specific sessions |

After logout, subsequent authenticated calls will fail until a new login is performed.

Password Reset

Request a password reset email for a user account. Public endpoint — no authentication required.

await mtx.auth.resetPassword({ username: '[email protected]' });

| Parameter | Type | Required | Description | |---|---|---|---| | username | string | Yes | Email or username | | jurisdictionCode | string | No | Jurisdiction ID | | appKey | string | No | Application key |

Password Recovery

Initiate password recovery. Public endpoint — no authentication required.

await mtx.auth.recoverPassword({ username: '[email protected]' });

| Parameter | Type | Required | Description | |---|---|---|---| | username | string | Yes | Email or username | | jurisdictionCode | string | No | Jurisdiction ID | | appKey | string | No | Application key |

Token Override Per-Request

Every resource method accepts an optional RequestOptions object. Pass accessToken to override the stored token for a single call:

const profile = await mtx.user.profile({
  accessToken: 'another-users-token',
});

All resource methods also accept an AbortSignal:

const controller = new AbortController();
const services = await mtx.services.list(
  { jurisdictionCodes: ['city_example'] },
  { signal: controller.signal },
);
interface RequestOptions {
  accessToken?: string;
  signal?: AbortSignal;
}

Resources

Base

Health check and OpenAPI spec retrieval.

base.status(options?)

Checks the operational status of the API.

| Parameter | Type | Required | Description | |---|---|---|---| | options | RequestOptions | No | Optional access token / abort signal |

Returns: Promise<boolean> -- true if the platform is reachable.

const isUp = await mtx.base.status();
console.log('API is up:', isUp); // true

base.openapi(options?)

Retrieves the OpenAPI specification.

| Parameter | Type | Required | Description | |---|---|---|---| | options | RequestOptions | No | Optional access token / abort signal |

Returns: Promise<string> -- The raw OpenAPI spec as a string.

const spec = await mtx.base.openapi();

Auth

See Authentication above for full details on all auth methods.

| Method | Parameters | Returns | |---|---|---| | auth.login({ username, password }) | { username: string; password: string } | Promise<Authentication> | | auth.loginAnonymous({ deviceId }) | { deviceId: string } | Promise<Authentication> | | auth.loginJwt({ email, firstName?, lastName?, register?, legalTermsAccepted? }) | See table above | Promise<Authentication> | | auth.loginRefresh({ refreshToken }) | { refreshToken: string } | Promise<Authentication> | | auth.register({ username, password, firstName, lastName, legalTermsAccepted, jurisdictionCode? }) | See table above | Promise<Authentication> | | auth.logout({ appKey?, deviceId? }) | { appKey?: number; deviceId?: string } | Promise<void> |


User

Manage the current user's profile and legal terms.

user.profile(options?)

Retrieves the authenticated user's profile.

Returns: Promise<Profile>

interface Profile {
  id?: string | null;
  email?: string | null;
  username?: string | null;
  firstName?: string | null;
  lastName?: string | null;
  twitterNickname?: string | null;
  anonymous?: boolean | null;
  phone?: string | null;
  gender?: 'male' | 'female' | 'other' | 'not_specified' | null;
  birthday?: string | null;
}
const profile = await mtx.user.profile();
console.log(`Hello, ${profile.firstName}`);

user.updateProfile(params, options?)

Updates the authenticated user's profile. Only provided fields are changed.

| Parameter | Type | Required | Description | |---|---|---|---| | email | string | No | New email address | | username | string | No | New username | | firstName | string | No | New first name | | lastName | string | No | New last name | | twitterNickname | string | No | Twitter handle | | phone | string | No | Phone number | | gender | 'male' \| 'female' \| 'other' \| 'not_specified' | No | Gender | | birthday | string | No | Birthday (date string) |

Returns: Promise<Profile>

const updated = await mtx.user.updateProfile({
  firstName: 'Jane',
  phone: '+34600000000',
});

user.acceptTerms(options?)

Accepts the legal terms for the current user. Requires authentication.

Returns: Promise<boolean> -- true on success.

await mtx.user.acceptTerms();

user.legalTerms(options?)

Retrieves the current legal terms and conditions.

Returns: Promise<LegalTerms>

interface LegalTerms {
  legalText?: LegalText | null;
}

interface LegalText {
  privacyPolicy?: string | null;
  termsOfUse?: string | null;
  cookiesPolicy?: string | null;
}
const terms = await mtx.user.legalTerms();
console.log(terms.legalText?.termsOfUse);

user.legalTermsByJurisdiction(params, options?)

Retrieves legal text URLs configured for a specific jurisdiction.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCode | string | Yes | Functional ID of the jurisdiction | | type | string | Yes | Application type (e.g., 'citizen') |

Returns: Promise<LegalTerms> — same shape as legalTerms(), but values are URLs.

const terms = await mtx.user.legalTermsByJurisdiction({
  jurisdictionCode: 'madrid',
  type: 'citizen',
});
console.log(terms.legalText?.privacyPolicy); // URL to privacy policy

user.legalText(params, options?)

Returns the raw HTML content of a legal document.

| Parameter | Type | Required | Description | |---|---|---|---| | legalTermsId | string | Yes | Legal terms document ID | | legalPolicyType | 'privacy_policy' \| 'terms_of_use' \| 'accessibility' \| 'cookies_policy' | Yes | Type of legal document |

Returns: Promise<string> — raw HTML content.

const html = await mtx.user.legalText({
  legalTermsId: '66ed366137c0c32c020b54ea',
  legalPolicyType: 'privacy_policy',
});

user.updateAvatar(params, options?)

Uploads an avatar image for the current user. Requires authentication. Accepts any FileParam (File, Blob, Buffer, file path, etc.).

| Parameter | Type | Required | Description | |---|---|---|---| | file | FileParam | Yes | Image file to upload |

Returns: Promise<Profile> — updated profile with new avatar URL.

const updated = await mtx.user.updateAvatar({
  file: new Blob([imageData], { type: 'image/png' }),
});

user.updateRegisteredAddress(options?)

Triggers sync of the user's registered address with jurisdiction webhooks. Requires authentication.

Returns: Promise<RegisteredAddress>

interface RegisteredAddress {
  registeredAddress?: {
    street?: string | null;
    number?: string | null;
    city?: string | null;
    postalCode?: string | null;
    country?: string | null;
  } | null;
  jurisdiction?: string | null;
  webhookId?: string | null;
  processedAt?: string | null;
}
const result = await mtx.user.updateRegisteredAddress();
console.log(result.registeredAddress?.city);

user.sendDeleteConfirmationMail(params, options?)

Sends a confirmation email with a deletion link. Public endpoint — no authentication required.

| Parameter | Type | Required | Description | |---|---|---|---| | email | string | Yes | User's email address | | jurisdictionCode | string | Yes | Jurisdiction ID |

Returns: Promise<boolean>true on success.

await mtx.user.sendDeleteConfirmationMail({
  email: '[email protected]',
  jurisdictionCode: 'madrid',
});

user.deleteUser(params, options?)

Deletes and anonymizes a user account using the confirmation token from the email. Public endpoint.

| Parameter | Type | Required | Description | |---|---|---|---| | token | string | Yes | Confirmation token from delete email |

Returns: Promise<boolean>true on success.

await mtx.user.deleteUser({ token: 'confirm-tok-from-email' });

Jurisdictions

jurisdictions.list(params?, options?)

Retrieves all jurisdictions, optionally filtered by geographic coordinates.

| Parameter | Type | Required | Description | |---|---|---|---| | lat | number | No | Latitude to filter by proximity | | lng | number | No | Longitude to filter by proximity |

Returns: Promise<Jurisdiction[]>

const all = await mtx.jurisdictions.list();

// Filter by coordinates
const nearby = await mtx.jurisdictions.list({
  lat: 41.3851,
  lng: 2.1734,
});

jurisdictions.detail(params, options?)

Retrieves detailed information for a specific jurisdiction.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCode | string | Yes | The unique jurisdiction code |

Returns: Promise<Jurisdiction>

const jurisdiction = await mtx.jurisdictions.detail({
  jurisdictionCode: 'city_barcelona',
});
console.log(jurisdiction);

Jurisdiction Elements

Jurisdiction elements represent subdivisions within a jurisdiction (districts, neighborhoods, etc.).

jurisdictionElements.list(params, options?)

Retrieves jurisdiction elements for a given jurisdiction.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCode | string | Yes | The jurisdiction code to list elements for |

Returns: Promise<JurisdictionElement[]>

const elements = await mtx.jurisdictionElements.list({
  jurisdictionCode: 'city_barcelona',
});
console.log(`${elements.length} districts found`);

Services

Services represent the types of issues citizens can report (e.g., potholes, broken streetlights).

services.list(params, options?)

Lists services available within one or more jurisdictions.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCodes | string[] | Yes | One or more jurisdiction codes | | lat | number | No | Latitude for location-based filtering | | lng | number | No | Longitude for location-based filtering | | typologyIds | string[] | No | Filter by typology IDs |

Returns: Promise<Service[]>

const services = await mtx.services.list({
  jurisdictionCodes: ['city_barcelona'],
  typologyIds: ['typ_infrastructure'],
});

services.detail(params, options?)

Retrieves detailed information for a specific service, including its additional data questions and mandatory informant configuration.

| Parameter | Type | Required | Description | |---|---|---|---| | serviceId | string | Yes | The service ID | | jurisdictionCode | string | Yes | The jurisdiction code |

Returns: Promise<Service>

const service = await mtx.services.detail({
  serviceId: 'svc_pothole',
  jurisdictionCode: 'city_barcelona',
});

// Inspect required fields
console.log('Has location:', service.typology?.hasLocation);
console.log('Needs description:', service.typology?.withDescription);
console.log('Additional questions:', service.additionalData?.configurableQuestions?.length);

services.validatePosition(params, options?)

Checks if a given lat/lng is valid for a specific service area. Useful before creating a request to ensure the location is within the service boundary. Requires authentication.

| Parameter | Type | Required | Description | |---|---|---|---| | serviceId | string | Yes | The service ID | | jurisdictionElementId | string | Yes | Jurisdiction element ID | | lat | number | Yes | Latitude | | lng | number | Yes | Longitude | | level | number | No | Jurisdiction element depth level | | projection | number | No | Spatial reference system SRID (default: 4326) |

Returns: Promise<ValidatePositionResult>

interface ValidatePositionResult {
  valid: boolean;
}
const result = await mtx.services.validatePosition({
  serviceId: 'svc_pothole',
  jurisdictionElementId: 'je_downtown',
  lat: 41.3851,
  lng: 2.1734,
});

if (result.valid) {
  // proceed to create request
}

Typologies

Typologies categorize services into groups (e.g., infrastructure, environment, mobility).

typologies.list(params, options?)

Retrieves typologies for one or more jurisdictions.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCodes | string[] | Yes | One or more jurisdiction codes | | jurisdictionElementId | string | No | Filter by jurisdiction element | | typologyIds | string[] | No | Filter by specific typology IDs | | lat | number | No | Latitude for location-based filtering | | lng | number | No | Longitude for location-based filtering | | page | number | No | Page number for pagination | | limit | number | No | Number of results per page |

Returns: Promise<Typology[]>

const typologies = await mtx.typologies.list({
  jurisdictionCodes: ['city_barcelona'],
  lat: 41.3851,
  lng: 2.1734,
});

for (const t of typologies) {
  console.log(t.name, t.id);
}

Location

Resolve location-specific additional data for a jurisdiction element (geocoding integration).

location.resolve(params, options?)

Retrieves location additional data. If geocoderSuffix was set in the client config and formattedAddress is provided, the suffix is appended automatically.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionElementId | string | Yes | The jurisdiction element ID | | formattedAddress | string | No | A formatted street address | | lat | number | No | Latitude | | lng | number | No | Longitude |

Returns: Promise<LocationAdditionalData[]>

const locationData = await mtx.location.resolve({
  jurisdictionElementId: 'je_eixample',
  formattedAddress: 'Carrer de Mallorca 401',
  lat: 41.3995,
  lng: 2.1748,
});

Requests

Create, list, detail, and annotate citizen issue reports.

requests.create(params, options?)

Creates a new request (issue report) within a jurisdiction.

| Parameter | Type | Required | Description | |---|---|---|---| | serviceId | string | Yes | The service ID for this request | | jurisdictionCode | string | Yes | The jurisdiction code | | jurisdictionElementId | string | No | Jurisdiction element (district/neighborhood) | | originDeviceId | string | No | Device type identifier | | description | string | No | Description of the issue | | public | boolean | No | Whether the request is publicly visible | | lat | number | No | Latitude of the issue location | | lng | number | No | Longitude of the issue location | | addressString | string | No | Street address of the issue | | email | string | No | Reporter's email | | firstName | string | No | Reporter's first name | | lastName | string | No | Reporter's last name | | phone | string | No | Reporter's phone number | | twitterNickname | string | No | Reporter's Twitter handle | | additionalData | AdditionalDataInput[] | No | Answers to the service's additional data questions |

Returns: Promise<CreatedRequest>

const created = await mtx.requests.create({
  serviceId: 'svc_pothole',
  jurisdictionCode: 'city_barcelona',
  description: 'Large pothole near bus stop',
  lat: 41.3851,
  lng: 2.1734,
  addressString: 'Carrer de Mallorca 401',
  email: '[email protected]',
  firstName: 'Maria',
  lastName: 'Garcia',
  additionalData: [
    { question: 'q_severity', value: 'high' },
  ],
});
console.log('Created request:', created.serviceRequestId);

requests.list(params?, options?)

Lists requests with filters and pagination.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCodes | string[] | No | Filter by jurisdiction codes | | serviceRequestIds | string[] | No | Filter by specific request IDs | | serviceIds | string[] | No | Filter by service IDs | | startDate | string | No | Start date filter | | endDate | string | No | End date filter | | own | boolean | No | Only show the authenticated user's requests | | lat | number | No | Latitude for proximity search | | lng | number | No | Longitude for proximity search | | page | number | No | Page number (default: 1) | | limit | number | No | Results per page (default: 20) | | addressAndServiceRequestId | string | No | Free-text search on address and request ID | | status | string[] | No | Filter by status codes | | typologyIds | string[] | No | Filter by typology IDs | | distance | number | No | Max distance in meters from lat/lng | | following | boolean | No | Only show followed requests | | order | string | No | Sort order | | complaints | boolean | No | Filter by complaints | | reiterations | boolean | No | Filter by reiterations | | userReiterated | boolean | No | Filter requests the user reiterated | | userComplaint | boolean | No | Filter requests the user complained about | | jurisdictionElementIds | string[] | No | Filter by jurisdiction element IDs | | level | number | No | Filter by level | | polygon | number[] | No | Polygon coordinates for geographic filtering | | finalOk | boolean | No | Filter by final OK status | | finalNotOk | boolean | No | Filter by final Not OK status | | finalStatus | boolean | No | Filter by final status | | interested | boolean | No | Filter by interested flag | | timezone | string | No | Timezone for date filters |

Returns: Promise<Request[]>

const requests = await mtx.requests.list({
  jurisdictionCodes: ['city_barcelona'],
  own: true,
  page: 1,
  limit: 20,
  status: ['open', 'in_progress'],
});

requests.detail(params, options?)

Retrieves detailed information about a specific request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID |

Returns: Promise<Request>

const request = await mtx.requests.detail({
  requestId: 'req_abc123',
});
console.log(request.status, request.description);

requests.attachMedia(params, options?)

Attaches a media file (photo/video) to an existing request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | jurisdictionCode | string | Yes | The jurisdiction code | | file | [string, Blob] | Yes | Tuple of [filename, blob] |

Returns: Promise<RequestMedia>

const media = await mtx.requests.attachMedia({
  requestId: 'req_abc123',
  jurisdictionCode: 'city_barcelona',
  file: ['photo.jpg', imageBlob],
});

requests.attachComment(params, options?)

Attaches a comment (with optional media files) to an existing request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | comment | string | No | Comment text | | files | [string, Blob][] | No | Array of [filename, blob] tuples |

Returns: Promise<RequestComment>

const comment = await mtx.requests.attachComment({
  requestId: 'req_abc123',
  comment: 'The pothole has gotten larger after the rain',
  files: [
    ['update1.jpg', photoBlob1],
    ['update2.jpg', photoBlob2],
  ],
});

requests.listComments(params, options?)

Lists all comments attached to a specific request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | jurisdictionCode | string | No | Filter by jurisdiction code |

Returns: Promise<RequestComment[]>

const comments = await mtx.requests.listComments({
  requestId: 'req_abc123',
});
for (const c of comments) {
  console.log(c.description, c.createdDatetime);
}

requests.listFiles(params, options?)

Lists all files attached to a specific request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) |

Returns: Promise<RequestFile[]>

const files = await mtx.requests.listFiles({
  requestId: 'req_abc123',
});
for (const f of files) {
  console.log(f.url, f.type);
}

requests.uploadFile(params, options?)

Uploads a file to an existing request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | file | FileParam | Yes | The file to upload | | public | boolean | No | Whether the file is publicly visible |

Returns: Promise<RequestFile>

const uploaded = await mtx.requests.uploadFile({
  requestId: 'req_abc123',
  file: new Blob([data], { type: 'application/pdf' }),
  public: true,
});
console.log('Uploaded:', uploaded.url);

requests.editFile(params, options?)

Edits a file's visibility on an existing request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | fileId | string | Yes | The file ID to edit | | public | boolean | Yes | New visibility setting |

Returns: Promise<RequestFile>

const updated = await mtx.requests.editFile({
  requestId: 'req_abc123',
  fileId: 'file_xyz',
  public: false,
});

requests.deleteFile(params, options?)

Deletes a file from an existing request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | fileId | string | Yes | The file ID to delete |

Returns: Promise<boolean> -- true on success.

await mtx.requests.deleteFile({
  requestId: 'req_abc123',
  fileId: 'file_xyz',
});

requests.listByCoordinates(params, options?)

Lists requests near given geographic coordinates.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCodes | string[] | No | Filter by jurisdiction codes | | lat | number | Yes | Latitude | | lng | number | Yes | Longitude | | limit | number | No | Maximum number of results | | own | boolean | No | Only show the authenticated user's requests |

Returns: Promise<RequestCoordinate[]>

const coords = await mtx.requests.listByCoordinates({
  jurisdictionCodes: ['city_barcelona'],
  lat: 41.3851,
  lng: 2.1734,
  limit: 20,
});
for (const c of coords) {
  console.log(c.token, c.lat, c.lng, c.addressString);
}

requests.detailByServiceRequestId(params, options?)

Retrieves a request using its jurisdiction code and service request ID (human-readable ID).

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCode | string | Yes | The jurisdiction code | | serviceRequestId | string | Yes | The service request ID |

Returns: Promise<Request>

const request = await mtx.requests.detailByServiceRequestId({
  jurisdictionCode: 'city_barcelona',
  serviceRequestId: 'SR-2026-001234',
});
console.log(request.description);

requests.detectDuplicate(params, options?)

Checks if a potential duplicate request exists near the given coordinates for a service. Returns null if no duplicate is found.

| Parameter | Type | Required | Description | |---|---|---|---| | lat | number | Yes | Latitude | | lng | number | Yes | Longitude | | serviceId | string | Yes | The service ID to check |

Returns: Promise<Request | null>

const duplicate = await mtx.requests.detectDuplicate({
  lat: 41.3851,
  lng: 2.1734,
  serviceId: 'svc_pothole',
});
if (duplicate) {
  console.log('Possible duplicate:', duplicate.serviceRequestId);
}

requests.listRelated(params, options?)

Lists related requests in the same family (parent/child relationships).

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) |

Returns: Promise<Request[]>

const related = await mtx.requests.listRelated({
  requestId: 'req_abc123',
});
for (const r of related) {
  console.log(r.serviceRequestId, r.description);
}

requests.validate(params, service)

Validates request parameters against a service's configuration. See Validation for details.

requests.listAll(params?, options?)

Async generator for automatic pagination. See Async Iterator.

Request Interactions

Methods for following, supporting, complaining about, evaluating, and reiterating requests.

Follow / Unfollow

requests.follow(params, options?)

Follows a request to receive notifications about updates.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCode | string | Yes | The jurisdiction code | | serviceRequestId | string | Yes | The service request ID | | emailNotification | boolean | No | Receive email notifications | | pushNotification | boolean | No | Receive push notifications | | smsNotification | boolean | No | Receive SMS notifications |

Returns: Promise<Request>

const request = await mtx.requests.follow({
  jurisdictionCode: 'city_barcelona',
  serviceRequestId: 'SR-2026-001234',
  emailNotification: true,
  pushNotification: true,
});
requests.listFollowed(params?, options?)

Lists all requests the authenticated user is following.

| Parameter | Type | Required | Description | |---|---|---|---| | appKey | number | No | Application key filter |

Returns: Promise<Request[]>

const followed = await mtx.requests.listFollowed();
for (const r of followed) {
  console.log(r.serviceRequestId, r.description);
}
requests.unfollow(params, options?)

Unfollows a request to stop receiving notifications.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCode | string | Yes | The jurisdiction code | | serviceRequestId | string | Yes | The service request ID |

Returns: Promise<Request>

await mtx.requests.unfollow({
  jurisdictionCode: 'city_barcelona',
  serviceRequestId: 'SR-2026-001234',
});

Support

requests.support(params, options?)

Supports (upvotes) a request to signal agreement.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCode | string | Yes | The jurisdiction code | | serviceRequestId | string | Yes | The service request ID |

Returns: Promise<Request>

const request = await mtx.requests.support({
  jurisdictionCode: 'city_barcelona',
  serviceRequestId: 'SR-2026-001234',
});
console.log('Supporting count:', request.supportingCount);

Complaints

requests.complain(params, options?)

Files a complaint on a request, with optional file attachments.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | description | string | No | Complaint description | | email | string | No | Complainant's email | | firstName | string | No | Complainant's first name | | lastName | string | No | Complainant's last name | | followRequest | boolean | No | Follow the request after complaining | | emailNotification | boolean | No | Receive email notifications | | pushNotification | boolean | No | Receive push notifications | | smsNotification | boolean | No | Receive SMS notifications | | files | FileParam[] | No | File attachments |

Returns: Promise<Complaint>

const complaint = await mtx.requests.complain({
  requestId: 'req_abc123',
  description: 'The issue has not been resolved',
  followRequest: true,
  emailNotification: true,
  files: [new Blob([photoData], { type: 'image/jpeg' })],
});
console.log('Complaint filed:', complaint.id);
requests.listComplaints(params, options?)

Lists complaints filed on a specific request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) |

Returns: Promise<ComplaintSummary[]>

const complaints = await mtx.requests.listComplaints({
  requestId: 'req_abc123',
});
for (const c of complaints) {
  console.log(c.id, c.datetime, c.originCreate);
}

Evaluations

requests.evaluate(params, options?)

Submits an evaluation (rating) for a request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | evaluation | number | Yes | Rating value (1-5) |

Returns: Promise<Evaluation>

const evaluation = await mtx.requests.evaluate({
  requestId: 'req_abc123',
  evaluation: 5,
});
console.log('Evaluation submitted:', evaluation.id);
requests.listEvaluations(params, options?)

Lists evaluations for a specific request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) |

Returns: Promise<Evaluation[]>

const evaluations = await mtx.requests.listEvaluations({
  requestId: 'req_abc123',
});
for (const e of evaluations) {
  console.log('Rating:', e.evaluation, 'by', e.user?.nickname);
}

Reiterations

requests.reiterate(params, options?)

Reiterates (re-reports) a request to escalate it.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) | | description | string | No | Additional description | | emailNotification | boolean | No | Receive email notifications | | pushNotification | boolean | No | Receive push notifications | | smsNotification | boolean | No | Receive SMS notifications | | followRequest | boolean | No | Follow the request after reiterating | | source | string | No | Source identifier |

Returns: Promise<Reiteration>

const reiteration = await mtx.requests.reiterate({
  requestId: 'req_abc123',
  description: 'This issue is still unresolved after 2 weeks',
  followRequest: true,
});
console.log('Reiteration submitted:', reiteration.id);
requests.listReiterations(params, options?)

Lists reiterations for a specific request.

| Parameter | Type | Required | Description | |---|---|---|---| | requestId | string | Yes | The request ID (token) |

Returns: Promise<ReiterationSummary[]>

const reiterations = await mtx.requests.listReiterations({
  requestId: 'req_abc123',
});
for (const r of reiterations) {
  console.log(r.id, r.datetime);
}

Additional Data Queries

Query additional data questions, answers, and external question lists.

additionalData.listAnswers(params, options?)

Retrieves a question with its possible answers and answer count.

| Parameter | Type | Required | Description | |---|---|---|---| | questionId | string | Yes | The question ID | | limit | number | No | Results per page | | page | number | No | Page number |

Returns: Promise<QuestionWithAnswers>

const question = await mtx.additionalData.listAnswers({
  questionId: 'q-123',
  limit: 20,
  page: 1,
});
console.log(question.question, question.answersCount);
for (const answer of question.possibleAnswers ?? []) {
  console.log(answer.value);
}

additionalData.searchAnswers(params, options?)

Searches answers for a question by text query.

| Parameter | Type | Required | Description | |---|---|---|---| | questionId | string | Yes | The question ID | | query | string | Yes | Search text | | limit | number | No | Results per page | | page | number | No | Page number |

Returns: Promise<Answer[]>

const answers = await mtx.additionalData.searchAnswers({
  questionId: 'q-123',
  query: 'pothole',
});
for (const a of answers) {
  console.log(a.value, a.valueTranslations);
}

additionalData.externalQuestions(params, options?)

Retrieves external questions for a question list.

| Parameter | Type | Required | Description | |---|---|---|---| | questionListId | string | Yes | The external question list ID |

Returns: Promise<ExternalQuestionList>

const list = await mtx.additionalData.externalQuestions({
  questionListId: 'ql-456',
});
for (const item of list.data ?? []) {
  console.log(item.question, item.required, item.responseAttribute);
}

additionalData.externalAnswers(params, options?)

Retrieves answers for an external data question, with optional search.

| Parameter | Type | Required | Description | |---|---|---|---| | questionId | string | Yes | The external question ID | | limit | number | No | Results per page | | page | number | No | Page number | | query | string | No | Optional search text |

Returns: Promise<Answer[]>

const answers = await mtx.additionalData.externalAnswers({
  questionId: 'eq-789',
  query: 'street',
  limit: 10,
});
for (const a of answers) {
  console.log(a.value);
}

additionalData.questionsByUseCase(params, options?)

Retrieves the additional data question list for a given jurisdiction and use case type.

| Parameter | Type | Required | Description | |---|---|---|---| | jurisdictionCode | string | Yes | The jurisdiction code | | useCaseType | string | Yes | The use case type identifier | | config | Record<string, unknown> | No | Optional configuration object |

Returns: Promise<AdditionalData>

const additionalData = await mtx.additionalData.questionsByUseCase({
  jurisdictionCode: 'city_barcelona',
  useCaseType: 'incident',
});
for (const cq of additionalData.configurableQuestions ?? []) {
  console.log(cq.question?.question, cq.required);
}

Request Builder

The RequestBuilder provides a fluent, chainable API for constructing requests step by step. It is especially useful in form-based UIs where fields are filled incrementally.

import { RequestBuilder } from '@mtx/api';

// Get the service detail first (needed for validation)
const service = await mtx.services.detail({
  serviceId: 'svc_pothole',
  jurisdictionCode: 'city_barcelona',
});

// Build the request using chained setters
const builder = mtx.requests.build()
  .service('svc_pothole')
  .jurisdiction('city_barcelona')
  .withServiceContext(service)
  .description('Pothole on Carrer de Mallorca')
  .location(41.3995, 2.1748)
  .address('Carrer de Mallorca 401')
  .email('[email protected]')
  .firstName('Maria')
  .lastName('Garcia')
  .public(true)
  .additionalData('q_severity', 'high');

// Validate before submitting
const validation = builder.isValid();
if (!validation.valid) {
  console.error('Validation errors:', validation.errors);
} else {
  const created = await builder.submit();
  console.log('Request submitted:', created.serviceRequestId);
}

Builder Methods

| Method | Parameter | Returns | Description | |---|---|---|---| | service(serviceId) | string | this | Sets the service ID | | jurisdiction(code) | string | this | Sets the jurisdiction code | | jurisdictionElement(id) | string | this | Sets the jurisdiction element ID | | description(text) | string | this | Sets the issue description | | location(lat, lng) | number, number | this | Sets the geographic coordinates | | address(addr) | string | this | Sets the street address | | email(e) | string | this | Sets the reporter's email | | firstName(n) | string | this | Sets the reporter's first name | | lastName(n) | string | this | Sets the reporter's last name | | phone(p) | string | this | Sets the reporter's phone number | | twitterNickname(n) | string | this | Sets the reporter's Twitter handle | | public(b) | boolean | this | Sets public visibility | | originDeviceId(id) | string | this | Sets the origin device identifier | | additionalData(code, value) | string, AdditionalDataInput['value'] | this | Adds a single additional data answer | | attach(file) | FileParam | this | Adds a file attachment | | withServiceContext(service) | Service | this | Sets the service context (required for isValid()); also sets serviceId if not already set | | isValid() | -- | ValidationResult | Validates against the service config | | submit(options?) | RequestOptions | Promise<CreatedRequest> | Submits the request to the API | | toParams() | -- | CreateRequestParams | Returns a copy of the current parameters |


Async Iterator (Pagination)

The requests.listAll() method returns an AsyncGenerator that automatically paginates through all results. It fetches pages on demand as you iterate.

// Iterate through ALL open requests in a jurisdiction
for await (const request of mtx.requests.listAll({
  jurisdictionCodes: ['city_barcelona'],
  status: ['open'],
  limit: 20, // page size (default: 20)
})) {
  console.log(request.serviceRequestId, request.description);
}

The generator:

  1. Starts at page (default 1) and fetches limit results per call.
  2. Yields each Request object one by one.
  3. Fetches the next page when the current page is exhausted.
  4. Stops when a page returns fewer results than limit.

You can also collect results into an array:

const allRequests: Request[] = [];
for await (const request of mtx.requests.listAll({ own: true })) {
  allRequests.push(request);
}

Pass RequestOptions as the second argument to use a custom token or abort signal:

const controller = new AbortController();

for await (const request of mtx.requests.listAll(
  { jurisdictionCodes: ['city_barcelona'] },
  { signal: controller.signal },
)) {
  if (someCondition) {
    controller.abort();
    break;
  }
}

Validation

The SDK provides client-side validation against a service's configuration before submitting a request.

requests.validate(params, service)

Validates CreateRequestParams against a Service object.

| Parameter | Type | Description | |---|---|---| | params | CreateRequestParams | The request parameters to validate | | service | Service | The service detail object (from services.detail()) |

Returns: ValidationResult

interface ValidationResult {
  valid: boolean;
  errors: ValidationError[];
}

interface ValidationError {
  field: string;
  message: string;
  code: string;
  expectedType?: string;  // present on additional data type errors
  receivedType?: string;  // present on additional data type errors
}

The validator checks:

  1. Mandatory informant fields -- fields required by service.mandatoryInformantConfig (e.g., firstName, lastName, email, phone).
  2. Typology requirements -- whether the service's typology requires a description or location.
  3. Required additional data questions -- required answers defined in service.additionalData.configurableQuestions.
  4. Additional data type validation -- each answer's value is validated against the question's declared type.
const service = await mtx.services.detail({
  serviceId: 'svc_pothole',
  jurisdictionCode: 'city_barcelona',
});

const result = mtx.requests.validate(
  {
    serviceId: 'svc_pothole',
    jurisdictionCode: 'city_barcelona',
    // intentionally missing description and location
  },
  service,
);

if (!result.valid) {
  for (const err of result.errors) {
    console.error(`${err.field}: ${err.message} (${err.code})`);
    // e.g. "description: Description is required by this service (required)"
    // e.g. "lat: Location is required by this service (required)"
  }
}

Additional Data Type Validation

When a service defines additional data questions with a type, validate() checks that each answer's value matches the expected type and format:

| Question Type | Expected Value | Error Code | |---|---|---| | number | A finite number or numeric string | invalid_type | | boolean | true/false or "true"/"false" | invalid_type / invalid_format | | singleValueList | A string from possibleAnswers (not an array) | invalid_type / invalid_answer | | multiValueList | An array of strings, all from possibleAnswers | invalid_type / invalid_answer | | datetime | An ISO 8601 date string | invalid_type / invalid_format | | position | { lat: number, lng: number } with valid ranges | invalid_type / invalid_format | | url | A valid http or https URL string | invalid_type / invalid_format | | color | A hex color string (#RRGGBB) | invalid_type / invalid_format | | audioClip | { audio_clip: { es: string } } | invalid_type | | videoClip | { video_clip: { es: string } } | invalid_type |

Errors include structured fields for programmatic handling (useful for LLM agents):

const result = mtx.requests.validate(params, service);

for (const err of result.errors) {
  console.error(err.field);        // "additionalData.q1"
  console.error(err.code);         // "invalid_type"
  console.error(err.message);      // 'Expected a numeric value, got "tres"'
  console.error(err.expectedType); // "number"
  console.error(err.receivedType); // "string"
}

Unknown or new question types pass validation without error, ensuring forward-compatibility with future API changes.

Using Validation with the Request Builder

The RequestBuilder.isValid() method wraps requests.validate(), using the service set via withServiceContext():

const builder = mtx.requests.build()
  .service('svc_pothole')
  .jurisdiction('city_barcelona')
  .withServiceContext(service);

// Check validity as the user fills in fields
builder.description('Pothole');
console.log(builder.isValid()); // { valid: false, errors: [...] }

builder.location(41.39, 2.17);
console.log(builder.isValid()); // { valid: true, errors: [] }

If withServiceContext() has not been called, isValid() returns a single error with code missing_service.


Error Handling

Error Hierarchy

MtxError (base)
├── MtxApiError (HTTP errors from the API)
│   └── LegalTermsRequiredError (HTTP 451)
├── MtxDecodingError (response Zod validation failed)
├── MtxValidationError (client-side validation errors)
├── MtxConfigError (missing or invalid configuration)
└── MtxArgumentError (missing or invalid method arguments)
    └── InvalidEmailError (invalid email format)

Error Properties

| Error Class | Properties | Description | |---|---|---| | MtxError | message | Base error class for all SDK errors | | MtxApiError | statusCode, code, description | HTTP error returned by the API | | LegalTermsRequiredError | (inherits MtxApiError, statusCode=451) | User must accept legal terms | | MtxDecodingError | zodErrors (ZodIssue[]) | API response did not match the expected schema | | MtxValidationError | errors (ValidationError[]) | Client-side validation failure | | MtxConfigError | message | Missing baseUrl, clientId, or jwtSecret | | MtxArgumentError | argument | A required argument was missing or invalid | | InvalidEmailError | argument (= 'email') | Email format validation failed |

Examples

Catching API Errors

import { MtxApiError, LegalTermsRequiredError } from '@mtx/api';

try {
  await mtx.auth.login({ username: '[email protected]', password: 'wrong' });
} catch (error) {
  if (error instanceof LegalTermsRequiredError) {
    // User needs to accept legal terms first
    const terms = await mtx.user.legalTerms();
    console.log('Please accept:', terms.legalText?.termsOfUse);
    await mtx.user.acceptTerms();
  } else if (error instanceof MtxApiError) {
    console.error(`API error ${error.statusCode}: ${error.description}`);
    console.error('Error code:', error.code);
  }
}

Catching Decoding Errors

import { MtxDecodingError } from '@mtx/api';

try {
  const services = await mtx.services.list({ jurisdictionCodes: ['test'] });
} catch (error) {
  if (error instanceof MtxDecodingError) {
    console.error('Unexpected API response shape:', error.zodErrors);
  }
}

Catching Configuration Errors

import { MtxConfigError } from '@mtx/api';

try {
  const mtx = createMTXClient({ baseUrl: 'https://api.city.mtx.com' });
  // Throws: Missing clientId
} catch (error) {
  if (error instanceof MtxConfigError) {
    console.error('Config problem:', error.message);
  }
}

Catching Argument Errors

import { MtxArgumentError, InvalidEmailError } from '@mtx/api';

try {
  await mtx.auth.loginJwt({ email: 'not-an-email' });
} catch (error) {
  if (error instanceof InvalidEmailError) {
    console.error('Bad email format');
  } else if (error instanceof MtxArgumentError) {
    console.error(`Missing argument: ${error.argument}`);
  }
}

Using the onLegalTermsRequired Callback

const mtx = createMTXClient({
  baseUrl: 'https://api.city.mtx.com',
  clientId: 'my-client',
  onLegalTermsRequired: async () => {
    // This callback fires automatically on HTTP 451
    console.log('Legal terms required -- redirecting user');
    // Show a dialog, navigate to terms page, etc.
  },
});

File Uploads

The SDK supports file uploads through requests.attachMedia() and requests.attachComment(). Files are sent as multipart/form-data.

Browser (File from Input)

// <input type="file" id="photo" accept="image/*" />
const input = document.getElementById('photo') as HTMLInputElement;
const file = input.files![0];

// Attach a photo to an existing request
await mtx.requests.attachMedia({
  requestId: 'req_abc123',
  jurisdictionCode: 'city_barcelona',
  file: [file.name, file],  // [filename, Blob]
});

Browser (Blob from Canvas)

const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const blob = await new Promise<Blob>((resolve) =>
  canvas.toBlob((b) => resolve(b!), 'image/jpeg'),
);

await mtx.requests.attachMedia({
  requestId: 'req_abc123',
  jurisdictionCode: 'city_barcelona',
  file: ['snapshot.jpg', blob],
});

Node.js (Buffer from File System)

import { readFileSync } from 'node:fs';

const buffer = readFileSync('/path/to/photo.jpg');
const blob = new Blob([buffer], { type: 'image/jpeg' });

await mtx.requests.attachMedia({
  requestId: 'req_abc123',
  jurisdictionCode: 'city_barcelona',
  file: ['photo.jpg', blob],
});

Multiple Files in a Comment

const files: [string, Blob][] = [
  ['photo1.jpg', blob1],
  ['photo2.jpg', blob2],
  ['video.mp4', videoBlob],
];

await mtx.requests.attachComment({
  requestId: 'req_abc123',
  comment: 'Here are additional photos of the issue',
  files,
});

Logging

The SDK uses a logger for HTTP request/response tracing. By default it logs to the console.

Logger Interface

interface MtxLogger {
  debug(message: string): void;
  info(message: string): void;
  error(message: string): void;
}

Default Logger

The default logger forwards to console.debug, console.info, and console.error:

const mtx = createMTXClient({
  baseUrl: 'https://api.city.mtx.com',
  clientId: 'my-client',
  // logger defaults to console
});

Custom Logger

Pass any object implementing MtxLogger:

import { createMTXClient, type MtxLogger } from '@mtx/api';

const myLogger: MtxLogger = {
  debug: (msg) => myLoggingService.log('debug', msg),
  info: (msg) => myLoggingService.log('info', msg),
  error: (msg) => myLoggingService.log('error', msg),
};

const mtx = createMTXClient({
  baseUrl: 'https://api.city.mtx.com',
  clientId: 'my-client',
  logger: myLogger,
});

Silencing the Logger

To suppress all SDK log output, pass a no-op logger:

const silentLogger: MtxLogger = {
  debug: () => {},
  info: () => {},
  error: () => {},
};

const mtx = createMTXClient({
  baseUrl: 'https://api.city.mtx.com',
  clientId: 'my-client',
  logger: silentLogger,
});

Schemas

The SDK uses Zod schemas internally to validate and transform every API response. All schemas are exported so you can reuse them for your own form validation, data parsing, or type inference.

Importing Schemas

import {
  // Auth
  AuthenticationSchema,
  ProfileSchema,

  // Common
  TagSchema,
  UserSchema,
  LocationSchema,
  LegalTextSchema,
  LegalTermsSchema,

  // Jurisdictions
  OriginDeviceSchema,
  JurisdictionElementSchema,
  JurisdictionSchema,
  JurisdictionElementListSchema,
  JurisdictionListSchema,

  // Typologies
  TypologySchema,
  TypologyListSchema,

  // Additional Data
  QuestionSchema,
  ConfigurableQuestionSchema,
  AdditionalDataSchema,
  PossibleAnswerSchema,
  AdditionalDataAnswerSchema,
  AdditionalDataSingleValueSchema,
  AdditionalDataMultivalueSchema,
  AdditionalDataDatetimeSchema,
  AdditionalDataLocationSchema,
  AdditionalDataAudioClipSchema,
  AdditionalDataVideoClipSchema,
  LocationAdditionalDataSchema,
  LocationAdditionalDataListSchema,

  // Additional Data Queries
  AnswerSchema,
  AnswerListSchema,
  QuestionWithAnswersSchema,
  ExternalQuestionItemSchema,
  ExternalQuestionListSchema,

  // Services
  MandatoryInformantFieldSchema,
  StatusNodeSchema,
  ServiceSchema,
  ServiceListSchema,

  // Media
  MediaMetadataSchema,
  FileSchema,
  MediaSchema,

  // Requests
  FollowingSchema,
  CreatedRequestSchema,
  CreatedRequestListSchema,
  RequestSchema,
  RequestListSchema,

  // Request Media & Comments
  RequestMediaSchema,
  RequestCommentMediaSchema,
  RequestCommentSchema,
  RequestCommentListSchema,

  // Request Files & Coordinates
  RequestFileSchema,
  RequestFileListSchema,
  RequestCoordinateSchema,
  RequestCoordinateListSchema,

  // Request Interactions
  SourceSchema,
  InteractionUserSchema,
  ComplaintSchema,
  ComplaintSummarySchema,
  ComplaintSummaryListSchema,
  EvaluationSchema,
  EvaluationListSchema,
  ReiterationSchema,
  ReiterationSummarySchema,
  ReiterationSummaryListSchema,
} from '@mtx/api';

Note: AdditionalDataAnswerSchema is a response/read schema -- its value field is z.any(), so it accepts whatever the API returns. For form validation (write side), use the specific write schemas (AdditionalDataSingleValueSchema, AdditionalDataMultivalueSchema, AdditionalDataDatetimeSchema, AdditionalDataLocationSchema, AdditionalDataAudioClipSchema, AdditionalDataVideoClipSchema) or the AdditionalDataInput type, which constrains value to the allowed union of string | string[] | { lat, lng, srs? } | { audio_clip } | { video_clip }.

Example: Form Validation with Zod

import { z } from 'zod';
import { AdditionalDataSingleValueSchema } from '@mtx/api';

// Build a form schema reusing the SDK's write schema for strict validation
const MyRequestFormSchema = z.object({
  description: z.string().min(10, 'Description must be at least 10 characters'),
  lat: z.number(),
  lng: z.number(),
  answers: z.array(AdditionalDataSingleValueSchema),
});

// Validate user input
const result = MyRequestFormSchema.safeParse(formData);
if (!result.success) {
  console.error(result.error.issues);
}

Type Inference

All response types are inferred from their schemas. You can also infer types yourself:

import { z } from 'zod';
import { ServiceSchema } from '@mtx/api';

type MyService = z.infer<typeof ServiceSchema>;

Environment Variables

| Variable | Required | Description | |---|---|---| | MTX_BASE_URL | Yes (if not in config) | Base URL of the MTX API instance | | MTX_CLIENT_ID | Yes (if not in config) | OAuth client identifier | | MTX_JWT_SECRET | No | Base64-encoded HMAC secret for JWT login |

Environment variables are only read when the corresponding config option is not provided. The SDK checks process.env and gracefully handles environments where process is undefined (e.g., browsers).


License

MIT