FairePlace Stripe API
Stripe-facing endpoints (webhooks and checkout plumbing).
Stripe-facing endpoints (webhooks and checkout plumbing).
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/faireplace-com:faireplace-com-stripe-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: FairePlace Property Management Stripe API
description: 'Comprehensive API for managing properties, leases, tenants, and all aspects of real estate management.
Built with Rust and Actix-web following hexagonal architecture principles.
## Authentication
This API uses API keys for authentication with tenant-based isolation.
## Base URL
- Production: `https://api.faireplace.com/api`'
version: 1.0.0
contact:
name: FairePlace API Support
email: api-support@faireplace.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.faireplace.com/api
description: Production server
security:
- BearerAuth: []
tags:
- name: Stripe
description: Stripe-facing endpoints (webhooks and checkout plumbing).
paths:
/webhooks/stripe:
post:
operationId: stripeWebhook
summary: Stripe Webhook - Event reception
description: 'Endpoint to receive Stripe webhooks and process payment events. ## ⚠️ IMPORTANT: Security
This endpoint **DOES NOT USE** standard Bearer API key authentication. Instead, it uses **Stripe signature verification** to ensure
that requests genuinely come from Stripe. ### Signature verification
Each webhook is verified with HMAC SHA256 according to Stripe documentation:
1. Extract the timestamp and v1 signature from the `Stripe-Signature` header
2. Build the signed payload: `timestamp + "." + payload_body`
3. Compute HMAC SHA256 with the webhook secret (`STRIPE_WEBHOOK_SECRET`)
4. Constant-time comparison with the received signature
5. Validate the timestamp (reject webhooks > 5 minutes old)
**Stripe-Signature header format**:
```
t=1234567890,v1=abc123def456... ```
### Replay attack protection
Webhooks with a timestamp too old (> 5 minutes) are rejected
to prevent replay attacks. ## Handled events
### `checkout.session.completed`
**Triggered**: When a user completes a payment via Stripe Checkout Session. **Actions**:
1. Extract metadata (`tenant_id`, `user_id`, `package_id`)
2. Idempotency check (avoid double processing)
3. Retrieve the package from DB via `stripe_price_id`
4. Credit the user''s account
5. Create a `purchase` type transaction
**Idempotency**:
- Check by `payment_intent_id` in existing transactions
- Check by `session_id` in transaction reasons
- If already processed, returns 200 OK without action
**Payload example**:
```json
{
"id": "evt_test_123",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_123",
"payment_intent": "pi_test_123",
"amount_total": 2990,
"metadata": {
"tenant_id": "tenant_123",
"user_id": "user_456",
"package_id": "550e8400-e29b-41d4-a716-446655440000"
},
"line_items": {
"data": [{
"price": {
"id": "price_test_123"
}
}]
}
}
}
}
```
### `payment_intent.succeeded`
**Triggered**: When a Stripe PaymentIntent is confirmed successfully. **Actions**:
- Logging for individual payment traceability
- No action on credits (handled by `checkout.session.completed`)
**Usage**: Primarily for individual payments (non-credits). ### `payment_intent.payment_failed`
**Triggered**: When a payment fails. **Actions**:
- Logging for traceability
- No action on credits (no deduction)
## Stripe Dashboard Configuration
For this endpoint to work, you must configure a webhook in Stripe Dashboard:
1. Go to **Developers** → **Webhooks**
2. Click **Add endpoint**
3. URL: `https://api.faireplace.com/api/webhooks/stripe` (or dev URL for testing)
4. Select the events:
- ✅ `checkout.session.completed`
- ✅ `payment_intent.succeeded`
- ✅ `payment_intent.payment_failed`
5. Copy the **Signing secret** (`whsec_...`) and configure it in `STRIPE_WEBHOOK_SECRET`
## Responses
The endpoint always returns **200 OK** for handled or ignored events,
per Stripe recommendations. This prevents Stripe from retrying
webhooks indefinitely. **Exceptions**:
- **401 Unauthorized**: Invalid or missing signature
- **400 Bad Request**: Invalid payload or missing metadata
- **500 Internal Server Error**: Processing error
## Request example
```bash
curl -X POST https://api.faireplace.com/v1/webhooks/stripe \
-H "Stripe-Signature: t=1234567890,v1=abc123def456..." \
-H "Content-Type: application/json" \
-d ''{
"id": "evt_test_123",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_123",
"payment_intent": "pi_test_123",
"amount_total": 2990,
"metadata": {
"tenant_id": "tenant_123",
"user_id": "user_456",
"package_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
}
}''
```
**Response**:
```json
{
"received": true
}
```
## Logs and traceability
All webhooks are logged with:
- Event type
- Stripe Event ID
- Session ID or Payment Intent ID
- Tenant ID and User ID (from metadata)
- Processing result (success, idempotency, error)
## Error handling
In case of error during webhook processing:
1. The error is logged with all details
2. A 500 response is returned to Stripe
3.'
tags:
- Stripe
security: []
requestBody:
required: true
description: "Stripe webhook payload in JSON format.\n\nThe exact format depends on the event type, but all Stripe webhooks\nfollow the standard structure:\n\n```json\n{\n \"id\": \"evt_xxx\",\n \"type\": \"checkout.session.completed\",\n \"data\": {\n \"object\": { ... }\n }\n}\n```\n"
content:
application/json:
schema:
type: object
description: 'Stripe Webhook Event.
The exact structure depends on the event type.
See the Stripe documentation for full details.
'
required:
- id
- type
- data
properties:
id:
type: string
description: 'Unique identifier of the Stripe event (starts with `evt_`).
Used for idempotency and traceability.
'
example: evt_test_123
type:
type: string
description: 'Stripe event type.
Supported types:
- `checkout.session.completed`: Payment via Checkout Session completed
- `payment_intent.succeeded`: PaymentIntent confirmed successfully
- `payment_intent.payment_failed`: Payment failed
'
example: checkout.session.completed
data:
type: object
description: 'Event data.
Contains the relevant Stripe object (Checkout Session, PaymentIntent, etc.)
in the `object` property.
'
required:
- object
properties:
object:
type: object
description: 'Stripe object related to the event.
For `checkout.session.completed`, contains the Checkout Session.
For `payment_intent.succeeded`, contains the PaymentIntent.
'
examples:
checkout_session_completed:
summary: Checkout Session completed
value:
id: evt_test_123
type: checkout.session.completed
data:
object:
id: cs_test_123
payment_intent: pi_test_123
amount_total: 2990
metadata:
tenant_id: tenant_123
user_id: user_456
package_id: 550e8400-e29b-41d4-a716-446655440000
line_items:
data:
- price:
id: price_test_123
payment_intent_succeeded:
summary: PaymentIntent succeeded
value:
id: evt_test_456
type: payment_intent.succeeded
data:
object:
id: pi_test_123
status: succeeded
amount: 2990
currency: eur
parameters:
- name: Stripe-Signature
in: header
required: true
description: 'Stripe signature to verify the authenticity of the webhook.
Format: `t=<timestamp>,v1=<signature>`
This signature is computed by Stripe using HMAC SHA256 and the webhook secret.
It ensures that the webhook genuinely comes from Stripe.
'
schema:
type: string
example: t=1234567890,v1=abc123def456...
responses:
'200':
description: 'Webhook received and processed successfully.
Always returns 200 OK even for unhandled events,
per Stripe recommendations.
'
content:
application/json:
schema:
type: object
required:
- received
properties:
received:
type: boolean
description: 'Indicates that the webhook has been received.
Always `true` on success.
'
example: true
examples:
success:
summary: Webhook processed successfully
value:
received: true
'400':
description: 'Invalid request.
**Possible causes**:
- Invalid JSON payload
- Missing required fields (`id`, `type`, `data.object`)
- Missing metadata in Checkout Session (`tenant_id`, `user_id`)
- Unsupported event format
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
invalid_payload:
summary: Invalid payload
value:
error:
code: PARSE_ERROR
message: 'Failed to parse webhook: invalid json'
status: 400
missing_metadata:
summary: Missing metadata
value:
error:
code: VALIDATION_ERROR
message: Missing tenant_id in metadata
status: 400
'401':
description: 'Invalid or missing signature.
**Possible causes**:
- Missing `Stripe-Signature` header
- Invalid signature format
- Signature does not match the payload
- Timestamp too old (> 5 minutes)
⚠️ **Security**: Webhooks without a valid signature are always rejected.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
missing_signature:
summary: Missing signature
value:
error:
code: UNAUTHORIZED
message: Missing Stripe-Signature header
status: 401
invalid_signature:
summary: Invalid signature
value:
error:
code: UNAUTHORIZED
message: Invalid Stripe signature
status: 401
timestamp_too_old:
summary: Timestamp too old
value:
error:
code: UNAUTHORIZED
message: 'Timestamp too old or too far in future: 600 seconds difference'
status: 401
'500':
description: 'Internal error during webhook processing.
**Possible causes**:
- Error retrieving the package
- Error adding credits
- Database error
⚠️ **Note**: Stripe will automatically retry webhooks that return 500
(up to 3 attempts over 3 days).
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
description: "The error envelope returned by every authenticated endpoint.\n\nTwo things are worth knowing before you write your error handling:\n\n- **`code` is a symbolic string, not a number.** The numeric HTTP status is\n in `status`. Branch on `code`: it is stable, and show `message`, which\n is written for humans and may be reworded without notice.\n- The public guest-signature endpoints use a **different, flatter shape**\n (`{ \"error_code\": …, \"message\": … }`, no `error` wrapper), because they\n are consumed by a page that has no session. Their responses document it\n inline.\n\nThe `X-Request-Id` response header carries the request identifier: quote it\nwhen reporting a problem. It is a header, not a body field.\n"
required:
- error
properties:
error:
type: object
required:
- code
- message
- status
properties:
code:
type: string
description: Stable symbolic code. Branch on this.
enum:
- NOT_FOUND
- UNAUTHORIZED
- VALIDATION_ERROR
- DATABASE_ERROR
- BUSINESS_ERROR
- STORAGE_ERROR
- CONFIG_ERROR
- FORBIDDEN
- CONFLICT
- PARSE_ERROR
- SERIALIZATION_ERROR
- AUTHENTICATION_ERROR
- AUTHORIZATION_ERROR
- EXTERNAL_SERVICE_ERROR
- RATE_LIMIT_EXCEEDED
- PAYMENT_REQUIRED
- PLAN_LIMIT_REACHED
- GONE
- BAD_REQUEST
- INTERNAL_ERROR
- INTERNAL_SERVER_ERROR
example: NOT_FOUND
message:
type: string
description: 'Human-readable explanation, most often in French. Safe to display,
not safe to parse.
'
example: Lease not found
status:
type: integer
description: The HTTP status code, repeated in the body.
example: 404
details:
type: object
additionalProperties: true
description: 'Extra machine-readable context. Only present on some errors: on
`RATE_LIMIT_EXCEEDED` it carries `retry_after_seconds`.
'
example:
retry_after_seconds: 12
securitySchemes:
BearerAuth:
type: http
scheme: bearer
description: API key with fp_* prefix, passed as Bearer token
WorkerApiKey:
type: apiKey
in: header
name: X-API-Key
WorkerBearer:
type: http
scheme: bearer
bearerFormat: Worker