{"openapi":"3.0.3","info":{"title":"ez.fyi API","version":"1.0.0","description":"ez.fyi is OCPL's internal URL shortener and transparent file proxy. This API lets approved integrations create, revoke, and tag links programmatically. See https://ez.fyi/docs for narrative documentation."},"servers":[{"url":"https://ez.fyi","description":"Current environment"}],"paths":{"/api/v1/openapi.json":{"get":{"summary":"Return this OpenAPI 3.0.3 specification","description":"Public — no authentication. Cached for 5 minutes at the edge.","responses":{"200":{"description":"OpenAPI 3.0.3 document","content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"405":{"description":"Method not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/links":{"post":{"summary":"Create a link","description":"Creates a new shortened link. Type A static file proxy in v1; Type B dynamic content lands in Plan 5.","security":[{"bearerAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional. When present, identical bodies within 24h replay the original response; a different body with the same key returns 409 idempotency_conflict."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLinkBody"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkCreated"}}}},"400":{"description":"Validation error (body shape, idempotency conflict, invalid_tag, too_many_tags)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid Bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Forbidden — host not on allowlist (Type B, Plan 5)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Idempotency-Key reused with a different body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"missing_field / invalid_field / unknown_field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/links/{slug}":{"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{10}$"},"description":"10-character Base64 URL-safe slug"}],"delete":{"summary":"Revoke a link","description":"Sets is_active=0 on the link. The S3 file is never deleted (revocation is reversible only via admin UI). Fires the configured webhook fire-and-forget.","security":[{"bearerAuth":[]}],"responses":{"204":{"description":"Revoked"},"401":{"description":"Missing or invalid Bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Link not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"405":{"description":"Method not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/links/{slug}/tags":{"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{10}$"},"description":"10-character Base64 URL-safe slug"}],"patch":{"summary":"Add tags to a link (additive)","description":"Adds tags to an existing link. Existing tags on the link are preserved — this endpoint never removes. Idempotent against (slug, tag) — adding a tag already present is a no-op.","security":[{"bearerAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional. When present, identical bodies within 24h replay the original response; a different body with the same key returns 409 idempotency_conflict."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddTagsBody"}}}},"responses":{"200":{"description":"Updated tag list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTagsList"}}}},"400":{"description":"Validation error (body shape, idempotency conflict, invalid_tag, too_many_tags)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid Bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Link not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"405":{"description":"Method not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"missing_field / invalid_field / unknown_field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/links/{slug}/tags/{tag}":{"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{10}$"},"description":"10-character Base64 URL-safe slug"},{"name":"tag","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9:_-]+$","maxLength":64},"description":"Tag string (URL-encoded if it contains a colon)"}],"delete":{"summary":"Remove a tag from a link","description":"Removes one tag from one link. Idempotent — removing a tag that isn't present is a no-op.","security":[{"bearerAuth":[]}],"responses":{"204":{"description":"Removed (or not present — idempotent)"},"401":{"description":"Missing or invalid Bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Link not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"405":{"description":"Method not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}},"components":{"schemas":{"CreateLinkBody":{"type":"object","properties":{"file_ref":{"type":"string","minLength":1,"maxLength":1024},"content_type":{"type":"string","pattern":"^[\\w.+-]+\\/[\\w.+-]+$"},"label":{"type":"string","minLength":1,"maxLength":255},"webhook_url":{"type":"string","maxLength":2048,"pattern":"^https:\\/\\/"},"tags":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z0-9:_-]+$","maxLength":64},"maxItems":20}},"required":["file_ref","content_type"],"additionalProperties":false},"AddTagsBody":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z0-9:_-]+$","maxLength":64},"maxItems":20}},"required":["tags"],"additionalProperties":false},"LinkCreated":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9_-]{10}$"},"url":{"type":"string","description":"Recipient-facing short URL (env.PUBLIC_HOST/{slug})"},"created_at":{"type":"string","description":"RFC 3339 timestamp"},"tags":{"type":"array","items":{"type":"string","pattern":"^[a-z0-9:_-]+$"}}},"required":["slug","url","created_at","tags"],"additionalProperties":false},"LinkTagsList":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9_-]{10}$"},"tags":{"type":"array","items":{"type":"string","pattern":"^[a-z0-9:_-]+$"}}},"required":["slug","tags"],"additionalProperties":false},"ErrorEnvelope":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable message"},"code":{"type":"string","enum":["forbidden","idempotency_conflict","internal_error","invalid_body","invalid_field","invalid_key","invalid_tag","method_not_allowed","missing_field","not_found","too_many_tags","unknown_field"],"description":"Stable machine code — switch on this in client code, never on `error` prose."},"field":{"type":"string","description":"Offending field name (validation errors only)"}},"required":["error","code"],"additionalProperties":false}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Issue: ezfy_<32 hex chars>. Issued by OCPL via the admin console. Send as `Authorization: Bearer ezfy_...`."}}}}