{
  "openapi": "3.1.0",
  "info": {
    "title": "Markifact API",
    "version": "1.0.0",
    "description": "API for managing Markifact workspace connections."
  },
  "servers": [
    {
      "url": "https://api.markifact.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "Connection": {
        "type": "object",
        "properties": {
          "id": { "type": ["string", "null"], "description": "Public Markifact connection ID." },
          "type": { "type": "string", "description": "Connection type." },
          "display_name": { "type": ["string", "null"] },
          "external_id": { "type": ["string", "null"] },
          "is_private": { "type": "boolean" },
          "created_at": { "type": ["integer", "null"], "format": "int64" },
          "updated_at": { "type": ["integer", "null"], "format": "int64" }
        },
        "required": ["id", "type", "is_private"]
      },
      "ConnectionType": {
        "type": "object",
        "properties": {
          "type": { "type": "string" },
          "name": { "type": "string" },
          "auth_methods": { "type": "array", "items": { "type": "string", "enum": ["oauth", "manual"] }, "description": "How this type can be authenticated. An \"oauth\" type accepts a shared auth link; a \"manual\" type is created by posting credentials to POST /v1/connections. A type listing both can be connected either way, and using your own app there is a white-label connection." },
          "credential_fields": { "type": ["array", "null"], "items": { "type": "string" }, "description": "Credential field names a manual connection of this type expects. Null for types that take no credentials: OAuth-only types, and the ones connected only in the Markifact app." },
          "docs_url": { "type": ["string", "null"], "format": "uri", "description": "Plain-markdown page explaining where each credential comes from, suitable for an agent to read directly. Null for types that have no page yet.", "example": "https://docs.markifact.com/nodes/meta_ads/meta_ads_custom_app.md" }
        },
        "required": ["type", "name", "auth_methods"]
      },
      "CreateAuthLinkRequest": {
        "type": "object",
        "properties": {
          "connection_type": { "type": "string", "example": "ga4" },
          "redirect_url": { "type": "string", "format": "uri", "example": "https://example.com/oauth/callback" },
          "external_id": { "type": "string", "maxLength": 255, "example": "customer_123" },
          "is_private": { "type": "boolean", "default": false, "description": "Whether the created connection should be private to the API-key user. Defaults to false, meaning shared with the workspace so other team members can see and use it." }
        },
        "required": ["connection_type"]
      },
      "CreateConnectionRequest": {
        "type": "object",
        "properties": {
          "connection_type": { "type": "string", "example": "gads", "description": "A connection type whose auth_methods include \"manual\". See GET /v1/connections/types." },
          "name": { "type": "string", "maxLength": 100, "example": "Acme Agency Google Ads", "description": "Your label for the connection. For white-label types, posting the same type and name again replaces the stored credentials in place. Slack and Shopify are keyed on the workspace and store instead, and API-key types (Ahrefs, SEMrush, Adjust, DataForSEO, OpenAI Ads) always create a new connection." },
          "credentials": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Exactly the credential_fields the type lists. A missing or unrecognised field is rejected before any call to the provider.", "example": { "client_id": "1234567890-abc123.apps.googleusercontent.com", "client_secret": "GOCSPX-...", "refresh_token": "1//0g..." } },
          "is_private": { "type": "boolean", "default": false, "description": "Whether the connection should be private to the API-key user. Defaults to false, meaning shared with the workspace so other team members can see and use it. Omit it when replacing credentials on an existing connection and the current visibility is kept." }
        },
        "required": ["connection_type", "name", "credentials"]
      },
      "AuthLink": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "expires_at": { "type": "integer", "format": "int64" }
        },
        "required": ["id", "url", "expires_at"]
      },
      "AuthLinkStatus": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "connected", "expired"] },
          "connection_type": { "type": "string" },
          "connection_id": { "type": ["string", "null"] },
          "external_id": { "type": ["string", "null"] },
          "expires_at": { "type": "integer", "format": "int64" },
          "consumed_at": { "type": ["integer", "null"], "format": "int64" },
          "created_at": { "type": "integer", "format": "int64" },
          "updated_at": { "type": "integer", "format": "int64" },
          "connections": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Connection" }
          }
        },
        "required": ["id", "status", "connection_type", "expires_at", "created_at", "updated_at", "connections"]
      },
      "Error": {
        "type": "object",
        "properties": {
          "detail": {}
        }
      },
      "LogSource": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "enum": ["mcp", "agent", "workflow"], "description": "What ran the operation." },
          "id": { "type": ["string", "null"], "description": "Parent object ID: the MCP server, agent conversation, or workflow." },
          "run_id": { "type": ["string", "null"], "description": "Single execution ID. Set on workflow logs, null otherwise." },
          "name": { "type": ["string", "null"], "description": "Current parent name, falling back to the name captured at log time if the parent was deleted." }
        },
        "required": ["type"]
      },
      "LogUser": {
        "type": "object",
        "properties": {
          "id": { "type": ["string", "null"], "description": "Markifact user ID." },
          "email": { "type": ["string", "null"], "description": "Email of the team member who ran the operation." }
        }
      },
      "Log": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Log ID." },
          "operation_id": { "type": "string", "example": "gads_get_campaigns", "description": "Markifact operation that ran." },
          "operation_version": { "type": ["integer", "null"] },
          "status": { "type": "string", "enum": ["success", "error", "skipped", "stop", "start"] },
          "source": { "$ref": "#/components/schemas/LogSource" },
          "user": { "$ref": "#/components/schemas/LogUser" },
          "started_at": { "type": "integer", "format": "int64", "example": 1784874174, "description": "Start time in Unix seconds." },
          "completed_at": { "type": ["integer", "null"], "format": "int64", "description": "Completion time in Unix seconds. Null while the operation is still running." },
          "credits_used": { "type": "integer" },
          "cache_hit": { "type": "boolean", "description": "Whether the result came from cache instead of the provider." },
          "input": { "description": "Input the operation ran with. Shape depends on the operation." },
          "output": { "description": "Result the operation returned, or the error payload when status is error. Shape depends on the operation." },
          "metadata": { "type": "object", "additionalProperties": true, "description": "Extra context. Keys depend on the source: node_id, node_label, and retry_attempts for workflows, tool_name for MCP." }
        },
        "required": ["id", "operation_id", "status", "source", "user", "started_at", "credits_used", "cache_hit", "metadata"]
      },
      "LogPage": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Log" } },
          "has_more": { "type": "boolean", "description": "Whether more logs match beyond this page." },
          "next_cursor": { "type": ["string", "null"], "description": "Pass back as `cursor` to fetch the next page. Null on the last page." }
        },
        "required": ["data", "has_more"]
      },
      "CreditBalance": {
        "type": "object",
        "properties": {
          "credits_limit": { "type": "integer", "description": "Credits included in the plan for the current period." },
          "credits_used": { "type": "integer", "description": "Credits consumed so far this period." },
          "credits_remaining": { "type": "integer", "description": "credits_limit minus credits_used. Can be negative: a run that overshoots is not rolled back." },
          "billing_period_end": { "type": ["integer", "null"], "format": "int64", "description": "When the period ends and usage resets, in Unix seconds." },
          "tier": { "type": "string", "description": "Current plan." }
        },
        "required": ["credits_limit", "credits_used", "credits_remaining", "tier"]
      },
      "CreditService": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "enum": ["workflow", "agent", "copilot", "mcp"], "description": "What was charged." },
          "id": { "type": ["string", "null"], "description": "The workflow, conversation, or MCP server charged. Matches source.id on operation logs." },
          "name": { "type": ["string", "null"], "description": "Name at the time of the charge, not the parent's current name." }
        },
        "required": ["type"]
      },
      "CreditLog": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "description": "Ledger entry ID." },
          "credits_used": { "type": "integer", "description": "Credits this charge consumed." },
          "credits_remaining": { "type": "integer", "description": "Balance immediately after this charge. A snapshot, not a running total: renewals reset the balance without writing an entry." },
          "service": { "$ref": "#/components/schemas/CreditService" },
          "user": { "$ref": "#/components/schemas/LogUser" },
          "created_at": { "type": "integer", "format": "int64", "example": 1784874205, "description": "Charge time in Unix seconds." }
        },
        "required": ["id", "credits_used", "credits_remaining", "service", "user", "created_at"]
      },
      "CreditLogPage": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/CreditLog" } },
          "has_more": { "type": "boolean", "description": "Whether more entries match beyond this page." },
          "next_cursor": { "type": ["string", "null"], "description": "Pass back as `cursor` to fetch the next page. Null on the last page." }
        },
        "required": ["data", "has_more"]
      }
    }
  },
  "paths": {
    "/v1/connections": {
      "get": {
        "operationId": "listConnections",
        "summary": "List connections",
        "description": "List API-visible connections in your Markifact team workspace.",
        "tags": ["Connections"],
        "parameters": [
          {
            "name": "external_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Filter connections by the external ID you provided when creating the auth link."
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "example": "ga4" },
            "description": "Filter connections by connection type."
          }
        ],
        "responses": {
          "200": {
            "description": "Connections returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Connection" }
                }
              }
            }
          },
          "401": { "description": "Invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "post": {
        "operationId": "createConnection",
        "summary": "Create connection",
        "description": "Create a connection from credentials you already hold, for any connection type whose auth_methods include \"manual\". The credentials are verified with the provider before anything is stored, so a successful response means the connection works. OAuth-only types cannot be created this way; use POST /v1/connections/auth-link instead.",
        "tags": ["Connections"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateConnectionRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Connection created and verified.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Connection" }
              }
            }
          },
          "400": { "description": "The type cannot be created from credentials, a credential field is missing or unrecognised, or the provider rejected the credentials.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "White-label connections require the Team plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/connections/types": {
      "get": {
        "operationId": "listConnectionTypes",
        "summary": "List connection types",
        "description": "List every connection type Markifact supports, with how each one can be authenticated and, for manual types, the credential fields it expects.",
        "tags": ["Connections"],
        "responses": {
          "200": {
            "description": "Connection types returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/ConnectionType" }
                }
              }
            }
          }
        }
      }
    },
    "/v1/connections/{connection_id}": {
      "get": {
        "operationId": "getConnection",
        "summary": "Get connection",
        "description": "Find connections by Markifact connection ID or your external ID.",
        "tags": ["Connections"],
        "parameters": [
          {
            "name": "connection_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Markifact connection ID by default, or your external ID when lookup_by is external_id."
          },
          {
            "name": "lookup_by",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["id", "external_id"], "default": "id" },
            "description": "Use id for Markifact connection ID lookup, or external_id for your external ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Matching connections returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Connection" }
                }
              }
            }
          },
          "404": { "description": "Connection not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "operationId": "deleteConnection",
        "summary": "Delete connection",
        "description": "Delete an API-visible connection from your Markifact team workspace.",
        "tags": ["Connections"],
        "parameters": [
          {
            "name": "connection_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Public Markifact connection ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted connection returned.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Connection" }
              }
            }
          },
          "404": { "description": "Connection not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/connections/auth-link": {
      "post": {
        "operationId": "createAuthLink",
        "summary": "Create auth link",
        "description": "Create a shareable OAuth link that connects an external account to your Markifact team.",
        "tags": ["Connections"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateAuthLinkRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Auth link created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthLink" }
              }
            }
          },
          "400": { "description": "Invalid request.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/connections/auth-links/{auth_link_id}/status": {
      "get": {
        "operationId": "getAuthLinkStatus",
        "summary": "Get auth link status",
        "description": "Check whether an auth link is pending, expired, or connected.",
        "tags": ["Connections"],
        "parameters": [
          {
            "name": "auth_link_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Auth link ID returned by Create auth link."
          }
        ],
        "responses": {
          "200": {
            "description": "Auth link status returned.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthLinkStatus" }
              }
            }
          },
          "404": { "description": "Auth link not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/credits": {
      "get": {
        "operationId": "getCreditBalance",
        "summary": "Get credit balance",
        "description": "Current credit balance for the team.",
        "tags": ["Credits"],
        "responses": {
          "200": {
            "description": "Balance returned.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreditBalance" }
              }
            }
          },
          "401": { "description": "Invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Team not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/credits/logs": {
      "get": {
        "operationId": "listCreditLogs",
        "summary": "List credit logs",
        "description": "List the team's credit charges, newest first. Not truncated by plan.",
        "tags": ["Credits"],
        "parameters": [
          {
            "name": "service_type",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["workflow", "agent", "copilot", "mcp"] },
            "description": "Filter by what was charged."
          },
          {
            "name": "service_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Charges from one workflow, conversation, or MCP server."
          },
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Charges triggered by one team member."
          },
          {
            "name": "created_after",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "format": "int64", "minimum": 0 },
            "description": "Only charges at or after this Unix second."
          },
          {
            "name": "created_before",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "format": "int64", "minimum": 0 },
            "description": "Only charges at or before this Unix second."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 25 },
            "description": "Entries per page. The ceiling is higher than on other resources because ledger entries carry no payloads."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "The next_cursor from the previous page. Keep every other filter unchanged while paging."
          }
        ],
        "responses": {
          "200": {
            "description": "Credit logs returned.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreditLogPage" }
              }
            }
          },
          "400": { "description": "Invalid cursor.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/logs": {
      "get": {
        "operationId": "listLogs",
        "summary": "List logs",
        "description": "List your team's operation logs, newest first.",
        "tags": ["Logs"],
        "parameters": [
          {
            "name": "source_type",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["mcp", "agent", "workflow"] },
            "description": "Only return logs from this source. Omit for all sources."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["success", "error", "skipped", "stop", "start"] },
            "description": "Only return logs with this status."
          },
          {
            "name": "operation_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "example": "gads_get_campaigns" },
            "description": "Exact operation ID to filter by."
          },
          {
            "name": "source_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Only return logs from one MCP server, agent conversation, or workflow."
          },
          {
            "name": "source_run_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Only return logs from a single workflow run."
          },
          {
            "name": "started_after",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "format": "int64", "minimum": 0 },
            "description": "Only return logs started at or after this Unix second. Cannot reach past your plan's history window."
          },
          {
            "name": "started_before",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "format": "int64", "minimum": 0 },
            "description": "Only return logs started at or before this Unix second."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 },
            "description": "Logs per page."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "The next_cursor from the previous page. Keep every other filter unchanged while paging."
          }
        ],
        "responses": {
          "200": {
            "description": "Logs returned.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LogPage" }
              }
            }
          },
          "400": { "description": "Invalid cursor.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/logs/{log_id}": {
      "get": {
        "operationId": "getLog",
        "summary": "Get log",
        "description": "Fetch a single log by ID.",
        "tags": ["Logs"],
        "parameters": [
          {
            "name": "log_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "The log ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Log returned.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Log" }
              }
            }
          },
          "401": { "description": "Invalid API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Log not found, or older than your plan's history window.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  }
}
