{
  "openapi": "3.1.0",
  "info": {
    "title": "Zeepler API",
    "version": "1",
    "summary": "Send label data as JSON, get Zebra ZPL back.",
    "description": "Zeepler turns a JSON description of a label into ZPL for Zebra printers at 203 dpi. One call, `POST /v1/zeeplify`, returns the ZPL as plain text, ready to send to the printer. `GET /v1/me` checks an API key without generating a label.\n\nFull documentation, with examples: https://zeepler.com/documentation",
    "termsOfService": "https://zeepler.com/terms",
    "contact": {
      "name": "Zeepler support",
      "url": "https://zeepler.com/support"
    }
  },
  "externalDocs": {
    "description": "Documentation",
    "url": "https://zeepler.com/documentation"
  },
  "servers": [
    {
      "url": "https://api.zeepler.com/v1",
      "description": "United States. For accounts created in the US region."
    },
    {
      "url": "https://api.ca.zeepler.com/v1",
      "description": "Canada. For accounts created in the Canada region. A key works only against its own region's host; anywhere else it is answered 401."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/zeeplify": {
      "post": {
        "operationId": "zeeplify",
        "summary": "Generate a label",
        "description": "Turns the request into ZPL. Every problem with the body is listed in one 400, each with the path of the field that caused it. A label is counted and billed only when ZPL is returned.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LabelRequest"
              },
              "example": {
                "config": {
                  "size": "4x2"
                },
                "data": {
                  "name": "Jane Doe",
                  "sku": "HB-M8-40-Z",
                  "url": "https://example.com/p/HB-M8-40-Z"
                },
                "templates": [
                  {
                    "type": "Text",
                    "coords": [
                      30,
                      30
                    ],
                    "font": "0,50",
                    "dataPath": "name"
                  },
                  {
                    "type": "BarCode",
                    "coords": [
                      30,
                      120
                    ],
                    "style": "code128",
                    "dataPath": "sku"
                  },
                  {
                    "type": "QRCode",
                    "coords": [
                      600,
                      30
                    ],
                    "magnification": 5,
                    "dataPath": "url"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The label, as ZPL.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "^XA^MCY^XZ\n    ^XA\n    ^SZ2\n    ^MMT\n    ^MNY\n    ^LL406\n    ^PW812\n    ^LH0,0\n^CF0,50^FO30,30^FDJane Doe^FS\n^BY2,2,59^FO30,120^BCN,,N,N,N,A^FDHB-M8-40-Z^FS\n^BY2,2,0^FO600,30^BQN,2,5^FDQA,https://example.com/p/HB-M8-40-Z^FS\n^XZ\n"
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON, or breaks a rule. Every problem is listed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "An account without a card has used its free labels for the month.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FreeCapReached"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests. Three sources: the per-key rate limit (60 a second, 600 a minute; wait the seconds in `Retry-After`), the account's monthly spending limit, or the gateway's route throttle (`{\"message\": \"Too Many Requests\"}`, no `Retry-After`).",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait. Sent with the per-key rate-limit answer only.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/SpendingLimitReached"
                    },
                    {
                      "$ref": "#/components/schemas/GatewayMessage"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/me": {
      "get": {
        "operationId": "me",
        "summary": "Check an API key",
        "description": "Answers the account's plan, its labels this month and its monthly cap. It never generates, counts or bills a label, but it does use the same rate limit as `zeeplify`.",
        "responses": {
          "200": {
            "description": "The key is valid.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "example": {
                  "plan": "free",
                  "labelsThisMonth": 3,
                  "cap": 40
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "Too many requests: the per-key rate limit (wait the seconds in `Retry-After`), or the gateway's route throttle (`{\"message\": \"Too Many Requests\"}`, no `Retry-After`).",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait. Sent with the per-key rate-limit answer only.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/GatewayMessage"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Your API key, from the dashboard. It starts with `zpl_`."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The API key is missing, malformed, unknown (or from the other region), or its account is disabled.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Unauthorized"
            }
          }
        }
      },
      "InternalError": {
        "description": "Our fault, not yours. Retry. A handler fault answers `{\"error\": \"Internal error\"}`; a request refused before it reached the handler (a Lambda throttle) answers the gateway's `{\"message\": \"Internal Server Error\"}`.",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/InternalError"
                },
                {
                  "$ref": "#/components/schemas/GatewayMessage"
                }
              ]
            }
          }
        }
      }
    },
    "schemas": {
      "LabelRequest": {
        "type": "object",
        "description": "Fields not listed here are ignored.",
        "required": [
          "config",
          "data",
          "templates"
        ],
        "properties": {
          "config": {
            "$ref": "#/components/schemas/LabelConfig"
          },
          "data": {
            "type": "object",
            "description": "The values to print, by name. Each element prints the value its `dataPath` names, which must be a string or a number. Fields no element names are ignored, whatever their type."
          },
          "templates": {
            "type": "array",
            "description": "The elements on the label, printed in order.",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Element"
            }
          }
        }
      },
      "LabelConfig": {
        "type": "object",
        "required": [
          "size"
        ],
        "properties": {
          "size": {
            "description": "Label stock, in inches. 3x1 is 609 x 203 dots and 4x2 is 812 x 406, at 203 dpi.",
            "enum": [
              "3x1",
              "4x2"
            ]
          },
          "lang": {
            "description": "The output language. ZPL is the only one.",
            "const": "zpl"
          },
          "printMode": {
            "description": "The ZPL `^MM` print mode. Default `T` (tear-off).",
            "enum": [
              "T",
              "P",
              "R",
              "A",
              "C",
              "D",
              "F",
              "L",
              "U",
              "K"
            ]
          },
          "mediaTracking": {
            "description": "The ZPL `^MN` media tracking. Default `Y` (gap or notch).",
            "enum": [
              "N",
              "Y",
              "W",
              "M",
              "A",
              "V"
            ]
          }
        }
      },
      "Element": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/TextElement"
          },
          {
            "$ref": "#/components/schemas/BarCodeElement"
          },
          {
            "$ref": "#/components/schemas/QRCodeElement"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "Text": "#/components/schemas/TextElement",
            "BarCode": "#/components/schemas/BarCodeElement",
            "QRCode": "#/components/schemas/QRCodeElement"
          }
        }
      },
      "Coords": {
        "type": "array",
        "description": "[x, y] in dots from the top-left corner. Each must fall inside the label: x below its width, y below its height.",
        "items": {
          "type": "integer",
          "minimum": 0
        },
        "minItems": 2,
        "maxItems": 2
      },
      "DataPath": {
        "type": "string",
        "minLength": 1,
        "description": "The name of a field in `data`, whose value is a string or a number."
      },
      "TextElement": {
        "type": "object",
        "required": [
          "type",
          "coords",
          "dataPath"
        ],
        "properties": {
          "type": {
            "const": "Text"
          },
          "coords": {
            "$ref": "#/components/schemas/Coords"
          },
          "dataPath": {
            "$ref": "#/components/schemas/DataPath"
          },
          "font": {
            "type": "string",
            "description": "A ZPL `^CF` font: a name A-Z or 0-9, optionally with height and width in dots, as `0,40`. A Text element without one keeps the previous element's font.",
            "pattern": "^[A-Z0-9](,\\d{1,4}(,\\d{1,4})?)?$"
          },
          "maxLength": {
            "type": "integer",
            "minimum": 1,
            "description": "Cut the value to this many characters. ZPL text does not wrap."
          }
        }
      },
      "BarCodeElement": {
        "type": "object",
        "required": [
          "type",
          "coords",
          "dataPath"
        ],
        "properties": {
          "type": {
            "const": "BarCode"
          },
          "coords": {
            "$ref": "#/components/schemas/Coords"
          },
          "dataPath": {
            "$ref": "#/components/schemas/DataPath"
          },
          "style": {
            "description": "Default `code128`. An `ean13` value must be 12 digits (the printer adds the check digit) or 13.",
            "enum": [
              "code128",
              "code93",
              "ean13"
            ]
          }
        }
      },
      "QRCodeElement": {
        "type": "object",
        "required": [
          "type",
          "coords",
          "dataPath",
          "magnification"
        ],
        "properties": {
          "type": {
            "const": "QRCode"
          },
          "coords": {
            "$ref": "#/components/schemas/Coords"
          },
          "dataPath": {
            "$ref": "#/components/schemas/DataPath"
          },
          "magnification": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "description": "Dots per QR module."
          }
        }
      },
      "ValidationErrors": {
        "type": "object",
        "required": [
          "errors"
        ],
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "description": "The path of the field, as `templates[2].dataPath`. Empty when the body itself is the problem."
                },
                "message": {
                  "type": "string",
                  "description": "The rule it breaks. It never quotes the value sent."
                }
              }
            }
          }
        },
        "example": {
          "errors": [
            {
              "field": "templates[0].coords",
              "message": "must fall inside the label, 812 x 406 dots"
            }
          ]
        }
      },
      "Unauthorized": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "enum": [
              "Missing API key",
              "Invalid API key format",
              "Invalid API key",
              "Account disabled"
            ]
          }
        }
      },
      "FreeCapReached": {
        "type": "object",
        "required": [
          "error",
          "message",
          "cap",
          "used",
          "dashboard"
        ],
        "properties": {
          "error": {
            "const": "Free monthly labels used"
          },
          "message": {
            "type": "string"
          },
          "cap": {
            "type": "integer",
            "description": "Labels a month an account without a card may generate."
          },
          "used": {
            "type": "integer"
          },
          "dashboard": {
            "type": "string",
            "format": "uri",
            "description": "Where to add a card."
          }
        }
      },
      "RateLimited": {
        "type": "object",
        "required": [
          "error",
          "retryAfter"
        ],
        "properties": {
          "error": {
            "const": "Rate limit exceeded"
          },
          "retryAfter": {
            "type": "integer",
            "minimum": 0,
            "description": "Seconds to wait, as in `Retry-After`."
          }
        }
      },
      "SpendingLimitReached": {
        "type": "object",
        "required": [
          "error",
          "currentSpend",
          "limit"
        ],
        "properties": {
          "error": {
            "const": "Monthly spending limit exceeded"
          },
          "currentSpend": {
            "type": "number",
            "description": "This month's spend, in CAD."
          },
          "limit": {
            "type": "number",
            "description": "The account's monthly limit, in CAD. Raise or clear it in the dashboard."
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "plan",
          "labelsThisMonth",
          "cap"
        ],
        "properties": {
          "plan": {
            "description": "`free`: no card, capped. `card`: a saved card with auto-billing. `invoice`: invoiced terms.",
            "enum": [
              "free",
              "card",
              "invoice"
            ]
          },
          "labelsThisMonth": {
            "type": "integer",
            "minimum": 0
          },
          "cap": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The monthly label cap, or null when there is none."
          }
        }
      },
      "InternalError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "const": "Internal error"
          }
        }
      },
      "GatewayMessage": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "description": "An answer from the API gateway in front of the handler, not from the handler itself."
      }
    }
  }
}
