{
  "openapi": "3.1.0",
  "info": {
    "title": "Wallet Loyalty — Partner API",
    "version": "1.0.0",
    "description": "Two endpoints for a point-of-sale integration.\n\n**Who is this?** `POST /api/v1/customers/match` resolves whatever the customer showed — a pass QR, a card number, a phone, an email — to a customer id.\n\n**Give them the points.** `POST /api/v1/stamps` awards against that id.\n\nThe POS owns the ticket, the till and the customer-to-sale association. We never model open tickets and never correlate by time: if you cannot tell us who the customer is, nothing happens.\n\nSend your own immutable sale reference as `externalRef` and retries are safe — a unique index on (merchant, reference) means the same sale awards once, and that survives a key rotation.\n\nFull reference, including the integration flow, error handling and worked examples: https://walletloyaltycard.com/developers"
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A per-merchant API key, `wlk_` followed by 32 base64url-encoded random bytes. The merchant OWNER mints it in their dashboard under More → API keys; managers and staff cannot. Send it as `Authorization: Bearer wlk_...`. Keys are hashed at rest and shown exactly once, at creation. Revoking is immediate. There is no expiry: a key is valid until revoked."
      }
    },
    "schemas": {}
  },
  "paths": {
    "/api/v1/customers/match": {
      "post": {
        "tags": [
          "partner"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serial": {
                    "maxLength": 128,
                    "type": "string"
                  },
                  "memberId": {
                    "maxLength": 64,
                    "type": "string"
                  },
                  "phone": {
                    "maxLength": 32,
                    "type": "string"
                  },
                  "email": {
                    "maxLength": 254,
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resolved, or a truthful miss. A miss is 200 rather than 404 — the caller asked a reasonable question and got an answer.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "required": [
                        "ok",
                        "matched",
                        "matchedOn",
                        "customer"
                      ],
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "matched": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "matchedOn": {
                          "description": "Which identifier resolved the customer.",
                          "anyOf": [
                            {
                              "type": "string",
                              "enum": [
                                "serial"
                              ]
                            },
                            {
                              "type": "string",
                              "enum": [
                                "memberId"
                              ]
                            },
                            {
                              "type": "string",
                              "enum": [
                                "phone"
                              ]
                            },
                            {
                              "type": "string",
                              "enum": [
                                "email"
                              ]
                            }
                          ]
                        },
                        "customer": {
                          "type": "object",
                          "required": [
                            "id",
                            "name",
                            "progress"
                          ],
                          "properties": {
                            "id": {
                              "description": "Pass this as `customerId` to POST /api/v1/stamps.",
                              "type": "string"
                            },
                            "name": {
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "progress": {
                              "type": "object",
                              "required": [
                                "stampsSinceLastRedemption",
                                "totalStamps",
                                "totalRedemptions"
                              ],
                              "properties": {
                                "stampsSinceLastRedemption": {
                                  "description": "Stamps accumulated since the customer last redeemed a reward.",
                                  "type": "integer"
                                },
                                "totalStamps": {
                                  "description": "Lifetime stamps, across all redemptions.",
                                  "type": "integer"
                                },
                                "totalRedemptions": {
                                  "description": "Lifetime rewards redeemed.",
                                  "type": "integer"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "ok",
                        "matched"
                      ],
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "matched": {
                          "type": "boolean",
                          "enum": [
                            false
                          ]
                        },
                        "reason": {
                          "description": "Present only when more than one customer matched. We refuse to guess between candidates: prompt for the pass QR or the printed card number, which are unique. Absent means a plain miss — nobody matched.",
                          "type": "string",
                          "enum": [
                            "ambiguous"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Zero identifiers supplied, or more than one. Blank and whitespace-only fields do not count as supplied.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "invalid_request"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "not_authenticated"
                          ]
                        },
                        {
                          "type": "string",
                          "enum": [
                            "invalid_api_key"
                          ]
                        },
                        {
                          "type": "string",
                          "enum": [
                            "api_key_revoked"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry the `customers:read` scope.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "insufficient_scope"
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The key's merchant is missing or soft-deleted. Terminal — stop using the key.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "no_merchant"
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "rate_limited"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stamps": {
      "post": {
        "tags": [
          "partner"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "customerId"
                ],
                "properties": {
                  "customerId": {
                    "minLength": 8,
                    "type": "string"
                  },
                  "count": {
                    "minimum": 1,
                    "maximum": 10,
                    "type": "integer"
                  },
                  "amountCents": {
                    "minimum": 0,
                    "maximum": 100000000,
                    "type": "integer"
                  },
                  "note": {
                    "maxLength": 400,
                    "type": "string"
                  },
                  "externalRef": {
                    "maxLength": 120,
                    "minLength": 1,
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "This `externalRef` was already used for this customer — a retry of a sale we already have. The original award is returned and nothing new was issued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "stamp",
                    "progress",
                    "card"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "duplicate": {
                      "description": "Present only on a replayed `externalRef`. The original award is echoed back and nothing new was issued.",
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "stamp": {
                      "type": "object",
                      "required": [
                        "id",
                        "customerId",
                        "count",
                        "note",
                        "externalRef",
                        "createdAt"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "customerId": {
                          "type": "string"
                        },
                        "count": {
                          "description": "Stamps awarded. On a points card this is computed from `amountCents` and is not bounded by 10; it can legitimately be 0 for a sale smaller than one whole earning block.",
                          "type": "integer"
                        },
                        "note": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "externalRef": {
                          "description": "Your own reference, echoed back exactly as you sent it.",
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "createdAt": {
                          "format": "date-time",
                          "type": "string"
                        }
                      }
                    },
                    "progress": {
                      "type": "object",
                      "required": [
                        "stampsSinceRedemption",
                        "stampsRequired",
                        "rewardReady"
                      ],
                      "properties": {
                        "stampsSinceRedemption": {
                          "type": "integer"
                        },
                        "stampsRequired": {
                          "type": "integer"
                        },
                        "rewardReady": {
                          "description": "True when the customer has earned a reward. Redemption happens in the merchant's own app or at the counter — there is no partner redemption endpoint.",
                          "type": "boolean"
                        }
                      }
                    },
                    "card": {
                      "type": "object",
                      "required": [
                        "id",
                        "stampsRequired",
                        "rewardDescription"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "stampsRequired": {
                          "type": "integer"
                        },
                        "rewardDescription": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Awarded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "stamp",
                    "progress",
                    "card"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "duplicate": {
                      "description": "Present only on a replayed `externalRef`. The original award is echoed back and nothing new was issued.",
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "stamp": {
                      "type": "object",
                      "required": [
                        "id",
                        "customerId",
                        "count",
                        "note",
                        "externalRef",
                        "createdAt"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "customerId": {
                          "type": "string"
                        },
                        "count": {
                          "description": "Stamps awarded. On a points card this is computed from `amountCents` and is not bounded by 10; it can legitimately be 0 for a sale smaller than one whole earning block.",
                          "type": "integer"
                        },
                        "note": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "externalRef": {
                          "description": "Your own reference, echoed back exactly as you sent it.",
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "createdAt": {
                          "format": "date-time",
                          "type": "string"
                        }
                      }
                    },
                    "progress": {
                      "type": "object",
                      "required": [
                        "stampsSinceRedemption",
                        "stampsRequired",
                        "rewardReady"
                      ],
                      "properties": {
                        "stampsSinceRedemption": {
                          "type": "integer"
                        },
                        "stampsRequired": {
                          "type": "integer"
                        },
                        "rewardReady": {
                          "description": "True when the customer has earned a reward. Redemption happens in the merchant's own app or at the counter — there is no partner redemption endpoint.",
                          "type": "boolean"
                        }
                      }
                    },
                    "card": {
                      "type": "object",
                      "required": [
                        "id",
                        "stampsRequired",
                        "rewardDescription"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "stampsRequired": {
                          "type": "integer"
                        },
                        "rewardDescription": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Points card with no `amountCents`, or a field belonging to the other card type. `count` is stamps-cards-only and `amountCents` is points-cards-only; sending the wrong one is rejected rather than silently discarded.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "amount_required"
                          ]
                        },
                        {
                          "type": "string",
                          "enum": [
                            "amount_not_accepted"
                          ]
                        },
                        {
                          "type": "string",
                          "enum": [
                            "count_not_accepted"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "not_authenticated"
                          ]
                        },
                        {
                          "type": "string",
                          "enum": [
                            "invalid_api_key"
                          ]
                        },
                        {
                          "type": "string",
                          "enum": [
                            "api_key_revoked"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "The merchant's plan is cancelled. Do not retry; the merchant must reactivate.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "upgrade_required"
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry the `stamps:write` scope.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "insufficient_scope"
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such customer under this merchant, or the merchant itself is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "customer_not_found"
                          ]
                        },
                        {
                          "type": "string",
                          "enum": [
                            "no_merchant"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Either the merchant has no active stamp card, or this `externalRef` was already used for a DIFFERENT customer.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "no_active_card"
                          ]
                        },
                        {
                          "type": "string",
                          "enum": [
                            "external_ref_conflict"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "rate_limited"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "https://api.walletloyaltycard.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "partner",
      "description": "The public partner surface. Everything else on this host is private."
    }
  ]
}
