{
  "openapi": "3.1.0",
  "info": {
    "title": "Vyloca Partner Pilot Gateway",
    "version": "1.3.0",
    "description": "Provider-neutral sandbox contract with strict schemas, correlated responses, idempotent execution and signed callbacks. Vyloca does not hold funds or receive raw card data."
  },
  "servers": [
    {
      "url": "https://vyloca.com/api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "security": [],
        "summary": "Read gateway activation status",
        "responses": {
          "200": {
            "description": "Gateway status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/operations/readiness": {
      "get": {
        "summary": "Read operational resilience and provider certification controls",
        "description": "Returns non-sensitive control capabilities and mandatory provider certification scenarios. It does not expose operational events, credentials, customer data or financial ledger entries.",
        "responses": {
          "200": {
            "description": "Operational readiness contract",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "requestId"],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": ["operationalModel", "controls", "alertCodes", "certificationScenarios"],
                      "properties": {
                        "operationalModel": { "const": "reference_only_non_financial_orchestration" },
                        "controls": { "type": "object", "additionalProperties": { "type": "boolean" } },
                        "alertCodes": { "type": "array", "items": { "type": "string" } },
                        "certificationScenarios": { "type": "array", "minItems": 18, "items": { "type": "string" } }
                      }
                    },
                    "requestId": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/accounts/resolve": {
      "post": {
        "summary": "Resolve a Nigerian recipient account",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResolveAccountRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Resolved account reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResolutionResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/quotes": {
      "post": {
        "summary": "Create a time-bound FX quote",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateQuoteRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Quote created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/payments": {
      "post": {
        "summary": "Execute an idempotent sandbox payment",
        "description": "One quote can create at most one payment. Reusing an idempotency key with different payment details is rejected.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Canonical payment state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/payments/{paymentId}": {
      "get": {
        "summary": "Read payment and audit events",
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentId"
          }
        ],
        "responses": {
          "200": {
            "description": "Payment state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentWithEventsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/payments/{paymentId}/reversals": {
      "post": {
        "summary": "Request partner review and reversal of a delayed payment",
        "description": "Records an idempotent reversal request. Only a signed partner callback can confirm the final reversed or merchant-credited state.",
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReversalRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Reversal review accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/webhooks/partner": {
      "post": {
        "security": [],
        "summary": "Accept a signed partner status webhook",
        "parameters": [
          {
            "name": "X-Vyloca-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "X-Vyloca-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhook"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "parameters": {
      "PaymentId": {
        "name": "paymentId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 100
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 100,
          "pattern": "^[A-Za-z0-9._:-]{8,100}$"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Authentication failed",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Conflict": {
        "description": "State or idempotency conflict",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "TooLarge": {
        "description": "Request body exceeds 16 KiB",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "Content-Type must be application/json",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "A required dependency is unavailable",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "GatewayTimeout": {
        "description": "A required dependency exceeded its deadline",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "schemas": {
      "ResolveAccountRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "bankCode",
          "accountNumber"
        ],
        "properties": {
          "bankCode": {
            "type": "string",
            "pattern": "^[0-9]{3}$"
          },
          "accountNumber": {
            "type": "string",
            "pattern": "^[0-9]{10}$",
            "writeOnly": true
          }
        }
      },
      "CreateQuoteRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "amountNgn",
          "destinationResolutionReference",
          "fundingMode"
        ],
        "properties": {
          "amountNgn": {
            "type": "integer",
            "minimum": 100,
            "maximum": 50000000
          },
          "destinationResolutionReference": {
            "type": "string",
            "minLength": 8,
            "maxLength": 80
          },
          "fundingMode": {
            "type": "string",
            "enum": [
              "just_in_time",
              "partner_balance"
            ]
          }
        }
      },
      "CreatePaymentRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "quoteId"
        ],
        "properties": {
          "quoteId": {
            "type": "string",
            "minLength": 8,
            "maxLength": 80
          },
          "scenario": {
            "type": "string",
            "enum": [
              "success",
              "delayed",
              "declined",
              "transfer_failed"
            ],
            "default": "success"
          }
        }
      },
      "ReversalRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "reason"
        ],
        "properties": {
          "reason": {
            "type": "string",
            "minLength": 8,
            "maxLength": 240
          }
        }
      },
      "PartnerWebhook": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "eventId",
          "paymentId",
          "providerId",
          "partnerReference",
          "eventType",
          "occurredAt"
        ],
        "properties": {
          "eventId": {
            "type": "string",
            "minLength": 8,
            "maxLength": 100
          },
          "paymentId": {
            "type": "string",
            "minLength": 8,
            "maxLength": 100
          },
          "providerId": { "type": "string", "minLength": 3, "maxLength": 80 },
          "partnerReference": { "type": "string", "minLength": 3, "maxLength": 100 },
          "eventType": { "$ref": "#/components/schemas/ProviderEventType" },
          "occurredAt": { "type": "string", "format": "date-time" },
          "payoutReference": { "type": "string", "minLength": 3, "maxLength": 100 },
          "evidenceReference": { "type": "string", "minLength": 8, "maxLength": 200 }
        }
      },
      "ProviderEventType": {
        "type": "string",
        "enum": [
          "payment.accepted", "funding.authorized", "payout.submitted", "merchant.credit.confirmed",
          "payment.failed", "reversal.pending", "reversal.completed", "refund.pending",
          "refund.completed", "dispute.opened", "manual_review.required"
        ]
      },
      "PartnerFinalStatus": {
        "deprecated": true,
        "description": "Deprecated. Provider events are recorded separately from Vyloca orchestration state.",
        "type": "string",
        "enum": [
          "merchant_credited",
          "delayed",
          "declined",
          "reversed"
        ]
      },
      "CanonicalPaymentStatus": {
        "type": "string",
        "enum": [
          "submitted", "provider_processing", "funding_confirmed", "merchant_credit_pending",
          "merchant_credited", "failed", "reversal_pending", "reversed", "refund_pending",
          "refunded", "disputed", "manual_review"
        ]
      },
      "Resolution": {
        "type": "object",
        "required": [
          "resolutionReference",
          "bankCode",
          "bankName",
          "accountName",
          "accountLast4",
          "createdAt"
        ],
        "properties": {
          "resolutionReference": { "type": "string" },
          "bankCode": {
            "type": "string"
          },
          "bankName": {
            "type": "string"
          },
          "accountName": {
            "type": "string"
          },
          "accountLast4": {
            "type": "string",
            "pattern": "^[0-9]{4}$"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Quote": {
        "type": "object",
        "required": [
          "quoteId",
          "amountNgn",
          "sourceCurrency",
          "exchangeRate",
          "feeEur",
          "totalEur",
          "expiresAt",
          "destinationResolutionReference",
          "fundingMode",
          "createdAt"
        ],
        "properties": {
          "quoteId": {
            "type": "string"
          },
          "amountNgn": {
            "type": "integer"
          },
          "sourceCurrency": {
            "const": "EUR"
          },
          "exchangeRate": {
            "type": "number"
          },
          "feeEur": {
            "type": "number"
          },
          "totalEur": {
            "type": "number"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "destinationResolutionReference": {
            "type": "string"
          },
          "fundingMode": {
            "type": "string",
            "enum": [
              "just_in_time",
              "partner_balance"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Payment": {
        "type": "object",
        "required": [
          "paymentId",
          "partnerReference",
          "quoteId",
          "status",
          "message",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "paymentId": {
            "type": "string"
          },
          "partnerReference": {
            "type": "string"
          },
          "quoteId": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/CanonicalPaymentStatus"
          },
          "message": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AuditEvent": {
        "type": "object",
        "required": [
          "eventId",
          "eventType",
          "actor",
          "detail",
          "createdAt"
        ],
        "properties": {
          "eventId": {
            "type": "string"
          },
          "paymentId": {
            "type": "string"
          },
          "eventType": {
            "type": "string"
          },
          "actor": {
            "type": "string",
            "enum": [
              "vyloca",
              "partner",
              "bank"
            ]
          },
          "detail": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "fields": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error",
          "requestId"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/Error"
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "ResolutionResponse": {
        "type": "object",
        "required": [
          "data",
          "requestId"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Resolution"
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "required": [
          "data",
          "requestId"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Quote"
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "PaymentResponse": {
        "type": "object",
        "required": [
          "data",
          "requestId"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Payment"
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "PaymentWithEventsResponse": {
        "type": "object",
        "required": [
          "data",
          "requestId"
        ],
        "properties": {
          "data": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Payment"
              },
              {
                "type": "object",
                "required": [
                  "events"
                ],
                "properties": {
                  "events": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/AuditEvent"
                    }
                  }
                }
              }
            ]
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "service",
          "version",
          "environment",
          "status",
          "controls",
          "regulatedBoundary",
          "requestId"
        ],
        "properties": {
          "service": {
            "const": "vyloca-partner-gateway"
          },
          "version": {
            "const": "v1"
          },
          "environment": {
            "const": "sandbox"
          },
          "status": {
            "type": "string",
            "enum": [
              "operational",
              "degraded",
              "awaiting_activation"
            ]
          },
          "controls": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            }
          },
          "regulatedBoundary": {
            "const": "partner"
          },
          "requestId": {
            "type": "string"
          }
        }
      }
    }
  }
}
