{
  "openapi": "3.1.0",
  "info": {
    "title": "CodigosAfiliados public feeds",
    "description": "Read-only, unauthenticated JSON feeds for AI/LLM citation and agent consumption. Community-verified referral codes for Spain. Documented for humans and models in /llms.txt; discoverable via /.well-known/api-catalog (RFC 9727). Markdown representations of content pages exist at /marca/{slug}.md and /blog/{slug}.md. Rate limit: 120 requests per minute per client on the dynamic endpoints, announced live via RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers (IETF RateLimit-Fields draft); exceeding it returns a 429 with Retry-After. /api/codes-index.json is a static file and is not rate-limited.\n\n## Versioning\n\nThe `/api/v1/*` paths are versioned aliases of the unversioned ones and return byte-identical bodies: a single handler serves both (`src/lib/feeds/`), so they cannot drift. `/api/v1/codes-index.json` is static like its twin and is not rate-limited; `/api/v1/brand-summary/{slug}.json` is, and shares one counter with the unversioned path because the limit is keyed on client address, not on path. Deprecation policy: https://www.codigosafiliados.com/developers#deprecation",
    "version": "1.2.0",
    "contact": {
      "url": "https://www.codigosafiliados.com/contacto"
    }
  },
  "servers": [
    {
      "url": "https://www.codigosafiliados.com"
    }
  ],
  "paths": {
    "/api/codes-index.json": {
      "get": {
        "operationId": "getCodesIndex",
        "summary": "Index of brands with active referral codes",
        "description": "Top brands with at least one community-verified active code, each with its canonical hub URL. Regenerated continuously; cache-friendly. Deprecated: superseded by the /api/v1 path, which returns the identical payload. See https://www.codigosafiliados.com/developers for the deprecation policy.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The index.",
            "headers": {
              "ETag": {
                "description": "Version of the feed; send it back as If-None-Match.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodesIndex"
                },
                "example": {
                  "source": "CodigosAfiliados.com",
                  "description": "Códigos de referido y afiliado verificados para España — 2026",
                  "generated_at": "2026-09-30T11:37:40.686Z",
                  "total_brands": 1000,
                  "total_active_codes": 915,
                  "brands": [
                    {
                      "name": "MyInvestor",
                      "slug": "myinvestor",
                      "url": "https://www.codigosafiliados.com/marca/myinvestor",
                      "codigo_afiliado_url": "https://www.codigosafiliados.com/marca/myinvestor",
                      "referido_url": "https://www.codigosafiliados.com/marca/myinvestor/como-funciona",
                      "website": "https://myinvestor.es",
                      "codes": [
                        {
                          "code": "RWBI9",
                          "title": "25€ en MyInvestor 💸",
                          "reward_user": "25€",
                          "reward_referrer": "25€",
                          "expires_at": null,
                          "upvotes": 2,
                          "url": "https://www.codigosafiliados.com/marca/myinvestor/25-85718ace"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the copy you hold (by ETag) is current. No body."
          },
          "503": {
            "description": "The database could not be reached. Retry after the Retry-After header; this response is never cached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "deprecated": true
      }
    },
    "/api/brand-summary/{slug}.json": {
      "get": {
        "operationId": "getBrandSummary",
        "summary": "One brand's referral-programme summary",
        "description": "The facts an agent needs to answer «does {brand} have a referral code and what does it pay»: top code, rewards for both parties, terms, verification count and the canonical hub URL. Deprecated: superseded by the /api/v1 path, which returns the identical payload. See https://www.codigosafiliados.com/developers for the deprecation policy.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            },
            "description": "Brand slug, e.g. n26. The full slug list is in /api/codes-index.json."
          }
        ],
        "responses": {
          "200": {
            "description": "The brand summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandSummary"
                },
                "example": {
                  "brand": "N26",
                  "slug": "n26",
                  "url": "https://www.codigosafiliados.com/marca/n26",
                  "website": "https://n26.com",
                  "category": "Banca y Finanzas",
                  "category_slug": "banca-y-finanzas",
                  "active_code_count": 16,
                  "top_code": null,
                  "top_code_title": null,
                  "reward_user": null,
                  "reward_referrer": null,
                  "terms": null,
                  "expires_at": null,
                  "verified_by_users": 0,
                  "last_verified": null,
                  "all_codes": [],
                  "source": "CodigosAfiliados.com"
                }
              }
            }
          },
          "404": {
            "description": "No brand with that slug has an active referral programme here. Body is the standard error object with code brand_not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The database could not be reached. Retry after the Retry-After header; this response is never cached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "deprecated": true
      }
    },
    "/api/{unknown}": {
      "get": {
        "operationId": "unknownEndpoint",
        "summary": "Any /api/* path that is not documented above",
        "description": "Answers with the standard JSON error object (code endpoint_not_found) instead of the site's HTML 404 page, so an agent that guesses a URL gets something parseable. The documented surface is the two feeds above. This covers versioned paths too: any `/api/v1/...` that does not exist answers the same way.",
        "parameters": [
          {
            "name": "unknown",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Any undocumented path segment."
          }
        ],
        "responses": {
          "404": {
            "description": "Standard error object, code endpoint_not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/codes-index.json": {
      "get": {
        "operationId": "getCodesIndexV1",
        "summary": "Index of brands with active referral codes (v1)",
        "description": "Versioned v1 endpoint. Top brands with at least one community-verified active code, each with its canonical hub URL. Regenerated continuously; cache-friendly.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The index.",
            "headers": {
              "ETag": {
                "description": "Version of the feed; send it back as If-None-Match.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodesIndex"
                },
                "example": {
                  "source": "CodigosAfiliados.com",
                  "description": "Códigos de referido y afiliado verificados para España — 2026",
                  "generated_at": "2026-09-30T11:37:40.686Z",
                  "total_brands": 1000,
                  "total_active_codes": 915,
                  "brands": [
                    {
                      "name": "MyInvestor",
                      "slug": "myinvestor",
                      "url": "https://www.codigosafiliados.com/marca/myinvestor",
                      "codigo_afiliado_url": "https://www.codigosafiliados.com/marca/myinvestor",
                      "referido_url": "https://www.codigosafiliados.com/marca/myinvestor/como-funciona",
                      "website": "https://myinvestor.es",
                      "codes": [
                        {
                          "code": "RWBI9",
                          "title": "25€ en MyInvestor 💸",
                          "reward_user": "25€",
                          "reward_referrer": "25€",
                          "expires_at": null,
                          "upvotes": 2,
                          "url": "https://www.codigosafiliados.com/marca/myinvestor/25-85718ace"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the copy you hold (by ETag) is current. No body."
          },
          "503": {
            "description": "The database could not be reached. Retry after the Retry-After header; this response is never cached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/brand-summary/{slug}.json": {
      "get": {
        "operationId": "getBrandSummaryV1",
        "summary": "Top referral codes and verified rewards for a brand (v1)",
        "description": "Versioned v1 endpoint. Highest-voted community referral codes, user and referrer rewards, and verification status for one brand. Dynamic, rate-limited.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            },
            "description": "Brand slug, e.g. n26. The full slug list is in /api/codes-index.json."
          }
        ],
        "responses": {
          "200": {
            "description": "The brand summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandSummary"
                },
                "example": {
                  "brand": "N26",
                  "slug": "n26",
                  "url": "https://www.codigosafiliados.com/marca/n26",
                  "website": "https://n26.com",
                  "category": "Banca y Finanzas",
                  "category_slug": "banca-y-finanzas",
                  "active_code_count": 16,
                  "top_code": null,
                  "top_code_title": null,
                  "reward_user": null,
                  "reward_referrer": null,
                  "terms": null,
                  "expires_at": null,
                  "verified_by_users": 0,
                  "last_verified": null,
                  "all_codes": [],
                  "source": "CodigosAfiliados.com"
                }
              }
            }
          },
          "404": {
            "description": "No brand with that slug has an active referral programme here. Body is the standard error object with code brand_not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The database could not be reached. Retry after the Retry-After header; this response is never cached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "The ETag from a previous response. When the feed has not changed, the server answers 304 with no body, so a client can poll cheaply.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Rate limit exceeded: more than 120 requests in the current 60-second window. Body is the standard error object with code rate_limited. Wait Retry-After seconds; the RateLimit-* headers are also present on non-429 responses from rate-limited endpoints, so a client can self-throttle before ever hitting this.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the window resets. Always equals RateLimit-Reset.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Limit": {
            "description": "Requests allowed per 60-second window (120).",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the window resets.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code",
          "message"
        ],
        "description": "Every error from this API is this shape: a short human label, a stable machine code, a Spanish sentence explaining what happened, and where to look next. HTML is never returned for an /api/* error.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Short label, e.g. Not found."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable code. Safe to branch on; the message text is not.",
            "examples": [
              "endpoint_not_found",
              "brand_not_found",
              "rate_limited"
            ]
          },
          "message": {
            "type": "string",
            "description": "What happened, in Spanish."
          },
          "hint": {
            "type": "string",
            "description": "Where to look to resolve it. Optional."
          }
        }
      },
      "CodesIndex": {
        "type": "object",
        "title": "CodesIndex",
        "description": "Index of every brand in the directory with its canonical URLs and the codes the community has published for it.",
        "properties": {
          "source": {
            "type": "string",
            "format": "uri",
            "description": "Canonical site the data comes from."
          },
          "description": {
            "type": "string",
            "description": "One line stating what the feed is, in Spanish."
          },
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this payload was built."
          },
          "total_brands": {
            "type": "integer",
            "description": "Brands included in this response."
          },
          "total_active_codes": {
            "type": "integer",
            "description": "Accepted codes across those brands."
          },
          "brands": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "slug": {
                  "type": "string",
                  "pattern": "^[a-z0-9-]+$"
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Brand hub."
                },
                "codigo_afiliado_url": {
                  "type": "string",
                  "format": "uri"
                },
                "referido_url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The how-it-works page for the brand referral programme."
                },
                "website": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uri",
                  "description": "The brand own site."
                },
                "codes": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "One community-published code for the brand.",
                    "properties": {
                      "code": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The code itself. Null when the offer is a link-only referral with no code to type."
                      },
                      "title": {
                        "type": "string",
                        "description": "How the person who published it described the offer."
                      },
                      "reward_user": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "What the new user gets. The publisher's words, unless the brand's own terms contradict them: then the brand's figure, or null with reward_note."
                      },
                      "reward_referrer": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "What the person who refers gets. Empty string when they declared nothing."
                      },
                      "reward_note": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Set when the brand's own published terms settle what the new user gets, for example «La marca solo paga a quien invita». When set, reward_user is null or the brand's figure (TASK-516)."
                      },
                      "expires_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "Declared expiry, or null when none was declared."
                      },
                      "upvotes": {
                        "type": "integer",
                        "description": "Net community votes. The directory ranks by this."
                      },
                      "url": {
                        "type": "string",
                        "format": "uri",
                        "description": "Canonical page for this code."
                      }
                    },
                    "additionalProperties": true
                  }
                }
              },
              "additionalProperties": true
            }
          }
        },
        "additionalProperties": true
      },
      "BrandSummary": {
        "type": "object",
        "title": "BrandSummary",
        "description": "One brand: canonical URLs, category, active-code count and the most upvoted code with its rewards and terms. Reward fields are free text in the publisher words, never normalised to a number.",
        "properties": {
          "brand": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9-]+$"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "category_slug": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[a-z0-9-]+$"
          },
          "active_code_count": {
            "type": "integer"
          },
          "top_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "Most upvoted code, or null when the brand has none accepted."
          },
          "top_code_title": {
            "type": [
              "string",
              "null"
            ]
          },
          "reward_user": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the new user gets. The publisher's words, unless the brand's own terms contradict them: then the brand's figure, or null with reward_note."
          },
          "reward_referrer": {
            "type": [
              "string",
              "null"
            ]
          },
          "reward_note": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when the brand's own published terms settle what the new user gets, for example «La marca solo paga a quien invita». When set, reward_user is null or the brand's figure (TASK-516)."
          },
          "official_reward_user": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the brand itself publishes for a new user, read on its site (TASK-516)."
          },
          "official_reward_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "When official_reward_user was read on the brand's site."
          },
          "terms": {
            "type": [
              "string",
              "null"
            ],
            "description": "Conditions as declared by whoever published the code."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "verified_by_users": {
            "type": "integer",
            "description": "How many people voted the top code still works."
          },
          "last_verified": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "all_codes": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One community-published code for the brand.",
              "properties": {
                "code": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The code itself. Null when the offer is a link-only referral with no code to type."
                },
                "title": {
                  "type": "string",
                  "description": "How the person who published it described the offer."
                },
                "reward_user": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "What the new user gets. The publisher's words, unless the brand's own terms contradict them: then the brand's figure, or null with reward_note."
                },
                "reward_referrer": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "What the person who refers gets. Empty string when they declared nothing."
                },
                "reward_note": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Set when the brand's own published terms settle what the new user gets, for example «La marca solo paga a quien invita». When set, reward_user is null or the brand's figure (TASK-516)."
                },
                "expires_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "description": "Declared expiry, or null when none was declared."
                },
                "upvotes": {
                  "type": "integer",
                  "description": "Net community votes. The directory ranks by this."
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Canonical page for this code."
                }
              },
              "additionalProperties": true
            }
          },
          "source": {
            "type": "string"
          }
        },
        "additionalProperties": true
      }
    }
  },
  "security": [],
  "x-api-versioning": {
    "version": "v1",
    "strategy": "url-path",
    "supported_versions": [
      "v1"
    ],
    "deprecation_policy": "https://www.codigosafiliados.com/developers#deprecation"
  }
}
