{
  "openapi": "3.1.0",
  "info": {
    "title": "TechJobs.vn Jobs API",
    "version": "1.0.0",
    "description": "Read-only search over open tech jobs at companies hiring in Vietnam, refreshed hourly from the companies' own career pages.\n\nAI agents that support MCP should use the MCP server at https://techjobs.vn/mcp instead: same data, plus companies, job details and account tools. This REST API is the fallback for clients that only make HTTP calls.\n\n**Limits.** Without a key: 10 jobs per page, pages 1–3, about 120 calls a minute and 3000 a day per IP. With a personal API key: up to 50 jobs per page, about 120 calls a minute and 3000 a day per account, shared with MCP calls. Over the limit the API answers 429 with a Retry-After header. All limits: https://techjobs.vn/mcp/limits\n\n**Attribution.** Show each job's `url` (its techjobs.vn page) wherever you show the job, and name TechJobs.vn as the source. Do not resell the data or build a competing job database from it.\n\n**No SLA.** The API is free and best effort. `/v1` stays backward compatible: fields may be added, never removed or renamed. Questions: support@techjobs.vn",
    "termsOfService": "https://techjobs.vn/terms",
    "contact": {
      "name": "TechJobs.vn",
      "email": "support@techjobs.vn",
      "url": "https://techjobs.vn/developers"
    }
  },
  "externalDocs": {
    "description": "Developer guide",
    "url": "https://techjobs.vn/developers"
  },
  "servers": [
    {
      "url": "https://techjobs.vn"
    }
  ],
  "paths": {
    "/api/v1/jobs": {
      "get": {
        "operationId": "searchJobs",
        "summary": "Search open tech jobs in Vietnam",
        "description": "Jobs matching every filter given, newest and best title matches first. Closed jobs are never returned. Add filters before paging.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text words matched against job title, company and skills, e.g. \"react frontend\". Title matches rank first.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "skills",
            "in": "query",
            "required": false,
            "description": "Every listed skill must be required by the job, e.g. [\"react\", \"typescript\"]. Comma-separated, or repeat the parameter.",
            "schema": {
              "maxItems": 10,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "name": "seniority",
            "in": "query",
            "required": false,
            "description": "Seniority levels, any of: intern, junior, mid, senior, lead. Comma-separated, or repeat the parameter.",
            "schema": {
              "maxItems": 5,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "intern",
                  "junior",
                  "mid",
                  "senior",
                  "lead"
                ]
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "hcm = Ho Chi Minh City, hanoi = Hà Nội; remote covers remote-vn and remote-global.",
            "schema": {
              "type": "string",
              "enum": [
                "hcm",
                "hanoi",
                "remote",
                "da-nang",
                "hai-phong",
                "binh-duong",
                "bac-ninh",
                "remote-vn",
                "remote-global"
              ]
            }
          },
          {
            "name": "company",
            "in": "query",
            "required": false,
            "description": "Company slug or exact name, e.g. \"grab\" or \"OPSWAT\".",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "posted_within_days",
            "in": "query",
            "required": false,
            "description": "Only jobs first posted within this many days.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum jobs to return (default 10): up to 10 without sign-in, 50 when signed in.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page of `limit` results, starting at 1; up to page 3 without sign-in.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "response_format",
            "in": "query",
            "required": false,
            "description": "concise (default) returns title, company and link; detailed adds city, seniority and skills.",
            "schema": {
              "type": "string",
              "enum": [
                "concise",
                "detailed"
              ]
            }
          }
        ],
        "security": [
          {},
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Matching jobs. `total` counts every match; `notice` appears when some were left out because of the guest limits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobSearchResult"
                }
              }
            }
          },
          "400": {
            "description": "A parameter is invalid, or the API key was sent in the URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit reached. Wait `retry_after_seconds` before the next call.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after_seconds": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable (maintenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "JobSearchResult": {
        "type": "object",
        "properties": {
          "jobs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "title": {
                  "type": "string"
                },
                "company": {
                  "type": "string"
                },
                "company_slug": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "posted_at": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "referral_available": {
                  "type": "boolean"
                },
                "referrers_count": {
                  "type": "integer"
                },
                "location": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "city": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "seniority": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "skills": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": [
                "id",
                "title",
                "company",
                "company_slug",
                "url",
                "posted_at",
                "referral_available",
                "referrers_count"
              ],
              "additionalProperties": false
            }
          },
          "total": {
            "type": "integer"
          },
          "notice": {
            "type": "string"
          }
        },
        "required": [
          "jobs",
          "total"
        ],
        "additionalProperties": false
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong and how to fix it."
          }
        },
        "required": [
          "error"
        ]
      }
    },
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tj_live_…",
        "description": "Optional personal API key from https://techjobs.vn/account/api-keys, sent as `Authorization: Bearer $TECHJOBS_API_KEY`. Never put it in the URL."
      }
    }
  }
}