@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
Table of Contents
- Installation
- Quick Start
- Usage Example
- Configuration
- Authentication
- Resources
- Request Builder
- Async Iterator (Pagination)
- Validation
- Error Handling
- File Uploads
- Logging
- Schemas
- Environment Variables
- License
Installation
npm install @mtx/apiThe 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:
- The
AuthManagerchecks if the current access token will expire within the next 30 seconds. - If so, it transparently calls the refresh endpoint using the stored refresh token.
- Concurrent requests share the same refresh promise (no duplicate refreshes).
- 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); // truebase.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 policyuser.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:
- Starts at
page(default 1) and fetcheslimitresults per call. - Yields each
Requestobject one by one. - Fetches the next page when the current page is exhausted.
- 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:
- Mandatory informant fields -- fields required by
service.mandatoryInformantConfig(e.g.,firstName,lastName,email,phone). - Typology requirements -- whether the service's typology requires a
descriptionorlocation. - Required additional data questions -- required answers defined in
service.additionalData.configurableQuestions. - 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:
AdditionalDataAnswerSchemais a response/read schema -- itsvaluefield isz.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 theAdditionalDataInputtype, which constrainsvalueto the allowed union ofstring | 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).
