openapi: 3.0.3
info:
  title: Guardian FDS Public API v1
  version: 1.0
  description: Credential-scoped asynchronous transaction ingestion and outcome API. This document contains only customer public /v1 contracts; internal and post-MVP interfaces are excluded.
servers:
  - url: https://api.fdsguard.co.kr
paths:
  /v1/transactions:
    post:
      summary: Submit one canonical transaction event asynchronously
      operationId: submitTransaction
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TransactionEvent' }
      responses:
        '202':
          description: Accepted into the durable ingestion ledger
          content: { application/json: { schema: { $ref: '#/components/schemas/AcceptedIngestion' } } }
        '200':
          description: Sandbox simulation accepted without persistence
          content: { application/json: { schema: { $ref: '#/components/schemas/SandboxSimulation' } } }
        '400': { $ref: '#/components/responses/ContractError' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/IngestionUnavailable' }
  /v1/ingestions/{ingestionId}:
    get:
      summary: Read ingestion and evaluation state in the authenticated Site scope
      operationId: getIngestion
      parameters:
        - name: ingestionId
          in: path
          required: true
          schema: { type: string, pattern: '^ing_[0-9]+$' }
      responses:
        '200':
          description: Ingestion status
          content: { application/json: { schema: { $ref: '#/components/schemas/IngestionStatus' } } }
        '400': { $ref: '#/components/responses/InvalidIngestionId' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/IngestionNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/transactions/{sourceEventId}/risk:
    get:
      summary: Read safe asynchronous evaluation result in the authenticated Site scope
      operationId: getTransactionRisk
      parameters:
        - name: sourceEventId
          in: path
          required: true
          schema: { type: string, minLength: 1, maxLength: 120 }
      responses:
        '200':
          description: Transaction ingestion and safe evaluation state
          content: { application/json: { schema: { $ref: '#/components/schemas/TransactionRiskStatus' } } }
        '400': { $ref: '#/components/responses/InvalidSourceEventId' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/TransactionNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/transactions/{sourceEventId}/outcomes:
    post:
      summary: Append one confirmed outcome label to an authenticated Site transaction
      operationId: appendTransactionOutcome
      parameters:
        - $ref: '#/components/parameters/SourceEventId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/OutcomeLabelRequest' } } }
      responses:
        '202':
          description: Outcome label accepted; a duplicate label is idempotently accepted.
          content: { application/json: { schema: { $ref: '#/components/schemas/OutcomeLabelAccepted' } } }
        '400': { $ref: '#/components/responses/OutcomeValidationError' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
        '404': { $ref: '#/components/responses/TransactionNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/usage:
    get:
      summary: Read the authenticated credential's current monthly API usage and quota
      operationId: getApiUsage
      responses:
        '200':
          description: Credential-scoped usage summary. Sandbox responses explicitly report no persistence.
          content: { application/json: { schema: { $ref: '#/components/schemas/ApiUsageSummary' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/IngestionUnavailable' }
components:
  securitySchemes:
    SiteCredential: { type: apiKey, in: header, name: x-api-key }
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 8, maxLength: 128, pattern: '^[A-Za-z0-9._:-]+$' }
      description: Retry key. Reusing it with a different payload returns 409.
    SourceEventId:
      name: sourceEventId
      in: path
      required: true
      schema: { type: string, minLength: 1, maxLength: 120 }
      description: Immutable customer event ID in the credential Site scope. When transaction.tid was supplied on ingestion, use that TID as this value.
  schemas:
    TransactionEvent:
      type: object
      additionalProperties: false
      required: [schema_version, source_event_id, occurred_at, sent_at, source_system, event_type, transaction]
      properties:
        schema_version: { type: string, enum: ['1.0'] }
        source_event_id: { type: string, minLength: 1, maxLength: 120, description: Immutable caller event ID. When transaction.tid is supplied, the accepted ledger and lookup response normalize this value to the TID. }
        occurred_at: { type: string, format: date-time, description: ISO 8601 time with offset }
        sent_at: { type: string, format: date-time, description: ISO 8601 time with offset }
        source_system: { type: string, minLength: 1 }
        event_type: { type: string, enum: [PROFILE_UPSERT, PAYMENT_AUTH, PAYMENT_CAPTURE, PAYMENT_CANCEL, PAYMENT_REFUND, PAYMENT_CHARGEBACK] }
        merchant_external_id:
          type: string
          minLength: 1
          maxLength: 100
          description: Required when the authenticated customer uses MULTI_MERCHANT. Optional for SINGLE_COMPANY, where Guardian assigns an opaque internal default subject.
        correlation_id: { type: string }
        profile_version: { type: integer, minimum: 0 }
        transaction: { $ref: '#/components/schemas/CanonicalTransaction' }
        payment_method: { $ref: '#/components/schemas/PaymentMethodContext' }
        order: { type: object, additionalProperties: true }
        customer: { type: object, additionalProperties: true }
        device: { type: object, additionalProperties: true }
        session: { type: object, additionalProperties: true }
        network: { type: object, additionalProperties: true }
        velocity: { type: object, additionalProperties: true }
    CanonicalTransaction:
      type: object
      additionalProperties: false
      required: [source_event_id, type, status, occurred_at, amount_minor, currency, channel]
      properties:
        source_event_id: { type: string, minLength: 1, maxLength: 120, description: Must equal the envelope source_event_id. }
        original_event_id: { type: string, description: Required for CANCEL, REFUND, and CHARGEBACK }
        type: { type: string, enum: [AUTH, CAPTURE, PAYMENT, CANCEL, REFUND, CHARGEBACK] }
        status: { type: string, enum: [REQUESTED, APPROVED, DECLINED, PENDING, CANCELED, REVERSED] }
        occurred_at: { type: string, format: date-time }
        amount_minor: { type: integer, minimum: 0, description: Amount in a currency minor unit }
        currency: { type: string, pattern: '^[A-Z]{3}$', example: KRW }
        channel: { type: string, enum: [ECOMMERCE, APP, POS, MANUAL, SMS] }
        installment_months: { type: integer, minimum: 0, maximum: 60, description: Number of card installments; 0 means lump-sum and is required for PAYMENT_AUTH events. }
        tid: { type: string, maxLength: 120, description: Payment TID. When supplied, it is the durable source_event_id used for trace, risk lookup, and outcomes. }
        auth_no_token: { type: string, description: Tokenized or limited ID; never raw card data }
        response_code: { type: string }
        reason_code: { type: string }
        is_cross_border: { type: boolean }
        requested_at: { type: string, format: date-time }
        business_type: { type: string }
    PaymentMethodContext:
      type: object
      additionalProperties: true
      required: [type, token]
      properties:
        type: { type: string }
        token: { type: string, minLength: 1, description: Stable token; raw PAN, CVC, CVV, account number forbidden }
    AcceptedIngestion:
      type: object
      required: [request_id, ingestion_id, status, received_at, schema_version, source_event_id, processing_state]
      properties:
        request_id: { type: string }
        ingestion_id: { type: string, example: ing_123 }
        status: { type: string, enum: [ACCEPTED] }
        received_at: { type: string, format: date-time }
        schema_version: { type: string, enum: ['1.0'] }
        source_event_id: { type: string }
        processing_state: { type: string, enum: [QUEUED, VALIDATED] }
    SandboxSimulation:
      type: object
      required: [request_id, environment, status, accepted, persistence, source_event_id, risk_score, decision]
      properties:
        request_id: { type: string }
        environment: { type: string, enum: [sandbox] }
        status: { type: string, enum: [SIMULATED] }
        accepted: { type: boolean, enum: [true] }
        persistence: { type: string, enum: [NONE] }
        source_event_id: { type: string }
        risk_score: { type: integer, minimum: 0, maximum: 100 }
        decision: { type: string, enum: [ALLOW, REVIEW, BLOCK] }
        reasons: { type: array, items: { type: string } }
    IngestionStatus:
      type: object
      required: [request_id, ingestion_id, source_event_id, evaluation_status, risk, received_at]
      properties:
        request_id: { type: string, description: Safe correlation ID; matches x-request-id response header. }
        ingestion_id: { type: string }
        source_event_id: { type: string }
        evaluation_status: { type: string, enum: [PENDING, PROCESSING, RETRY_SCHEDULED, SUCCEEDED, DLQ] }
        received_at: { type: string, format: date-time }
        risk:
          type: object
          nullable: true
          description: Present after evaluation; raw payment, customer and request payload fields are never returned.
          required: [score, level, decision, reasons, evaluator_version, evaluated_at]
          properties:
            score: { type: integer, minimum: 0, maximum: 100 }
            level: { type: string, enum: [low, medium, high, critical] }
            decision: { type: string, enum: [ALLOW, REVIEW, HOLD_RECOMMENDED, BLOCK_RECOMMENDED] }
            reasons: { type: array, items: { type: string } }
            rule_scores: { type: object, additionalProperties: true }
            evaluator_version: { type: string }
            evaluated_at: { type: string, format: date-time }
    TransactionRiskStatus:
      type: object
      additionalProperties: false
      required: [request_id, ingestion_id, source_event_id, evaluation_status, risk]
      properties:
        request_id: { type: string, description: Safe correlation ID; matches x-request-id response header. }
        ingestion_id: { type: string, pattern: '^ing_[0-9]+$' }
        source_event_id: { type: string, minLength: 1, maxLength: 120 }
        evaluation_status: { type: string, enum: [PENDING, PROCESSING, RETRY_SCHEDULED, SUCCEEDED, DLQ] }
        risk: { $ref: '#/components/schemas/IngestionStatus/properties/risk' }
    OutcomeLabelRequest:
      type: object
      additionalProperties: false
      required: [outcome_label]
      properties:
        outcome_label: { type: string, enum: [FRAUD_CONFIRMED, FALSE_POSITIVE, CHARGEBACK_CONFIRMED] }
    OutcomeLabelAccepted:
      type: object
      additionalProperties: false
      required: [request_id, ingestion_id, source_event_id, outcome_label, status]
      properties:
        request_id: { type: string, description: Safe correlation ID; matches x-request-id response header. }
        ingestion_id: { type: string, pattern: '^ing_[0-9]+$' }
        source_event_id: { type: string, minLength: 1, maxLength: 120 }
        outcome_label: { type: string, enum: [FRAUD_CONFIRMED, FALSE_POSITIVE, CHARGEBACK_CONFIRMED] }
        status: { type: string, enum: [ACCEPTED] }
    ApiUsageSummary:
      oneOf:
        - type: object
          additionalProperties: false
          required: [environment, persistence, quota]
          properties:
            environment: { type: string, enum: [sandbox] }
            persistence: { type: string, enum: [NONE] }
            quota: { type: object, nullable: true, description: Sandbox never persists or consumes quota. }
        - type: object
          additionalProperties: false
          required: [environment, quota]
          properties:
            environment: { type: string, enum: [production] }
            quota:
              type: object
              additionalProperties: false
              required: [period, limit, accepted, remaining]
              properties:
                period: { type: string, pattern: '^\\d{4}-\\d{2}$', example: '2026-08' }
                limit: { type: integer, minimum: 0 }
                accepted: { type: integer, minimum: 0 }
                remaining: { type: integer, minimum: 0 }
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [request_id, error]
      properties:
        request_id: { type: string, description: Safe correlation ID; matches x-request-id response header. }
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: array, items: { type: string } }
  responses:
    ContractError:
      description: Required, type, enum, or forbidden-field validation failed
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    Unauthorized:
      description: Credential is missing, invalid, inactive, or outside its Site scope
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    IdempotencyConflict:
      description: Idempotency-Key or source_event_id was reused with a different payload
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    RateLimited:
      description: Rate limit exceeded
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    IngestionUnavailable:
      description: Durable ingestion unavailable; retry with the same Idempotency-Key
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    InvalidIngestionId:
      description: Ingestion ID format invalid
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    IngestionNotFound:
      description: No ingestion exists in authenticated Site scope
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    InvalidSourceEventId:
      description: source_event_id is empty or exceeds the public 120-character limit
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    TransactionNotFound:
      description: No transaction exists in authenticated Site scope
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    OutcomeValidationError:
      description: Outcome request has an undeclared field or an unsupported label
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
security:
  - SiteCredential: []
