openapi: 3.1.0 info: title: HelpYourNeighbour API version: 0.2.0 description: API contract for authentication, requests, offers, contacts, profile, addresses, and reviews. servers: - url: http://localhost:3000 description: Local development security: - bearerAuth: [] paths: /health: get: tags: [system] summary: Health check security: [] responses: '200': description: Service healthy content: application/json: schema: $ref: '#/components/schemas/HealthResponse' /auth/register: post: tags: [auth] summary: Register a new user security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RegisterRequest' responses: '201': description: User created content: application/json: schema: $ref: '#/components/schemas/AuthTokenResponse' '400': $ref: '#/components/responses/BadRequest' '409': description: Email already exists content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': description: Registration failed content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /auth/login: post: tags: [auth] summary: Login with email/password security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LoginRequest' responses: '200': description: Login success content: application/json: schema: $ref: '#/components/schemas/AuthTokenResponse' '400': $ref: '#/components/responses/BadRequest' '401': description: Invalid credentials content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /requests: get: tags: [requests] summary: List help requests security: [] responses: '200': description: Request list content: application/json: schema: type: array items: $ref: '#/components/schemas/HelpRequest' post: tags: [requests] summary: Create help request requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateHelpRequest' responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/IdResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /offers/{requestId}: post: tags: [offers] summary: Create offer for help request parameters: - $ref: '#/components/parameters/RequestId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateOfferRequest' responses: '201': description: Offer created content: application/json: schema: $ref: '#/components/schemas/IdResponse' '400': $ref: '#/components/responses/BadRequestSimple' '401': $ref: '#/components/responses/Unauthorized' /offers/negotiation/{offerId}: post: tags: [offers] summary: Create counter-offer/negotiation message parameters: - $ref: '#/components/parameters/OfferId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateOfferRequest' responses: '201': description: Negotiation entry created content: application/json: schema: $ref: '#/components/schemas/IdResponse' '400': $ref: '#/components/responses/BadRequestSimple' '401': $ref: '#/components/responses/Unauthorized' /offers/accept/{offerId}: post: tags: [offers] summary: Accept offer and create deal parameters: - $ref: '#/components/parameters/OfferId' responses: '201': description: Deal created content: application/json: schema: $ref: '#/components/schemas/DealCreatedResponse' '400': description: Invalid offer id content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': $ref: '#/components/responses/Unauthorized' '404': description: Offer not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /contacts/request: post: tags: [contacts] summary: Request contact exchange for a deal requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContactRequestCreate' responses: '201': description: Request created content: application/json: schema: $ref: '#/components/schemas/IdResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: Deal not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '409': description: Request already exists content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /contacts/respond: post: tags: [contacts] summary: Accept or reject contact exchange request requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContactRequestRespond' responses: '200': description: Response processed content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: Request not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /contacts/deal/{dealId}: get: tags: [contacts] summary: List accepted contact exchanges for deal parameters: - $ref: '#/components/parameters/DealId' responses: '200': description: Accepted contact exchange rows content: application/json: schema: type: array items: $ref: '#/components/schemas/ContactExchangeRow' '400': description: Invalid dealId content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: Deal not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /addresses/change-request: post: tags: [addresses] summary: Request address change with postal verification code requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AddressChangeRequest' responses: '201': description: Change request created content: application/json: schema: $ref: '#/components/schemas/AddressChangeCreatedResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /addresses/verify: post: tags: [addresses] summary: Verify address change with 6-digit code requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AddressVerifyRequest' responses: '200': description: Address verified content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid payload or code content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: Request not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '409': description: Request not pending content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /profile/phone: post: tags: [profile] summary: Update encrypted phone number requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PhoneUpdateRequest' responses: '200': description: Phone updated content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /reviews/{dealId}: post: tags: [reviews] summary: Create review for deal parameters: - $ref: '#/components/parameters/DealId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateReviewRequest' responses: '201': description: Review created content: application/json: schema: $ref: '#/components/schemas/IdResponse' '400': $ref: '#/components/responses/BadRequestSimple' '401': $ref: '#/components/responses/Unauthorized' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT parameters: RequestId: name: requestId in: path required: true schema: type: integer minimum: 1 OfferId: name: offerId in: path required: true schema: type: integer minimum: 1 DealId: name: dealId in: path required: true schema: type: integer minimum: 1 responses: BadRequest: description: Validation failed content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' BadRequestSimple: description: Invalid payload content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unauthorized: description: Missing or invalid token content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Forbidden: description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' schemas: HealthResponse: type: object additionalProperties: false properties: status: type: string example: ok required: [status] ErrorResponse: type: object properties: error: oneOf: - type: string - type: object StatusResponse: type: object additionalProperties: false properties: status: type: string required: [status] IdResponse: type: object additionalProperties: false properties: id: type: integer minimum: 1 required: [id] DealCreatedResponse: type: object additionalProperties: false properties: dealId: type: integer minimum: 1 required: [dealId] AuthTokenResponse: type: object additionalProperties: false properties: token: type: string required: [token] RegisterRequest: type: object additionalProperties: false properties: email: type: string format: email password: type: string minLength: 8 displayName: type: string minLength: 2 maxLength: 120 required: [email, password, displayName] LoginRequest: type: object additionalProperties: false properties: email: type: string format: email password: type: string minLength: 1 required: [email, password] HelpRequest: type: object properties: id: type: integer title: type: string description: type: string value_chf: type: number status: type: string created_at: type: string format: date-time requester_name: type: string CreateHelpRequest: type: object additionalProperties: false properties: title: type: string minLength: 3 maxLength: 180 description: type: string minLength: 5 valueChf: type: number exclusiveMinimum: 0 required: [title, description, valueChf] CreateOfferRequest: type: object additionalProperties: false properties: amountChf: type: number exclusiveMinimum: 0 message: type: string maxLength: 2000 required: [amountChf] ContactRequestCreate: type: object additionalProperties: false properties: dealId: type: integer minimum: 1 targetUserId: type: integer minimum: 1 required: [dealId, targetUserId] ContactRequestRespond: type: object additionalProperties: false properties: requestId: type: integer minimum: 1 accept: type: boolean required: [requestId, accept] ContactExchangeRow: type: object properties: id: type: integer requester_id: type: integer target_id: type: integer accepted: type: boolean requester_phone_encrypted: type: string target_phone_encrypted: type: string AddressChangeRequest: type: object additionalProperties: false properties: newAddress: type: string minLength: 10 required: [newAddress] AddressChangeCreatedResponse: type: object additionalProperties: false properties: requestId: type: integer minimum: 1 postalDispatch: type: string example: pending_letter note: type: string verificationCode: type: string pattern: '^\\d{6}$' required: [requestId, postalDispatch, note, verificationCode] AddressVerifyRequest: type: object additionalProperties: false properties: requestId: type: integer minimum: 1 code: type: string pattern: '^\\d{6}$' required: [requestId, code] PhoneUpdateRequest: type: object additionalProperties: false properties: phone: type: string minLength: 6 maxLength: 40 required: [phone] CreateReviewRequest: type: object additionalProperties: false properties: revieweeId: type: integer minimum: 1 rating: type: integer minimum: 1 maximum: 5 comment: type: string maxLength: 2000 required: [revieweeId, rating]