{
  "openapi": "3.1.0",
  "info": {
    "title": "Perkadoo API",
    "version": "1",
    "summary": "Count member actions, read streaks and subscribe to streak events.",
    "description": "The public HTTP API behind Perkadoo Streaks. Count an action from your server or automation, read a member's streak for display, link a signed-in member to their browser, and use the customer API that powers the Perkadoo Zapier app. Guides: https://perkadoo.com/docs/api/overview",
    "contact": {
      "name": "Perkadoo support",
      "email": "hello@perkadoo.com",
      "url": "https://perkadoo.com/docs"
    }
  },
  "externalDocs": {
    "description": "Perkadoo API docs",
    "url": "https://perkadoo.com/docs/api/overview"
  },
  "servers": [
    {
      "url": "https://api.perkadoo.com"
    }
  ],
  "tags": [
    {
      "name": "Events",
      "description": "Count actions and read streaks with your project key.",
      "externalDocs": {
        "url": "https://perkadoo.com/docs/api/events"
      }
    },
    {
      "name": "Identity",
      "description": "Link an anonymous visitor to a signed-in member.",
      "externalDocs": {
        "url": "https://perkadoo.com/docs/guides/connecting-users"
      }
    },
    {
      "name": "Customer API",
      "description": "Bearer-token API for Zapier, Make, n8n and your own backend.",
      "externalDocs": {
        "url": "https://perkadoo.com/docs/api/customer-api"
      }
    }
  ],
  "paths": {
    "/v1/webhook/{siteKey}/e": {
      "post": {
        "tags": ["Events"],
        "operationId": "countAction",
        "summary": "Count an action",
        "description": "Counts one member action toward their streak. Call it from your server or automation after the action succeeds. Actions are never counted from the browser.",
        "security": [
          {
            "webhookSecret": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteKeyPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "event": "lesson_complete",
                "userHash": "member_123",
                "idempotencyKey": "lesson_42:member_123"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Perkadoo has the action.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": ["ok"]
                }
              }
            }
          },
          "400": {
            "description": "INVALID_BODY, INVALID_WIDGET_KIND, INVALID_SITE_KEY or SITE_KEY_MISMATCH.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "WEBHOOK_SECRET_REQUIRED: the secret is missing or wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_KEY_IN_PROGRESS: the same completion is still processing. Retry after 1 second.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "PAYLOAD_TOO_LARGE: meta is over about 4KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/state": {
      "get": {
        "tags": ["Events"],
        "operationId": "getState",
        "summary": "Read a member's streak",
        "description": "Returns a member's streak, this week's check-ins, milestones and widget settings. Browser requests must come from the website address saved in Perkadoo.",
        "parameters": [
          {
            "name": "siteKey",
            "in": "query",
            "required": true,
            "description": "Your project key.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userHash",
            "in": "query",
            "required": true,
            "description": "Your stable member ID.",
            "schema": {
              "type": "string",
              "maxLength": 256
            }
          },
          {
            "name": "event",
            "in": "query",
            "required": true,
            "description": "The action the streak counts, such as lesson_complete.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The member's streak.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/State"
                }
              }
            }
          },
          "403": {
            "description": "FORBIDDEN_ORIGIN: a browser on another site made the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/alias": {
      "post": {
        "tags": ["Identity"],
        "operationId": "aliasVisitor",
        "summary": "Link a visitor to a member",
        "description": "Links the browser's anonymous visitor to a signed-in member with a short-lived token your server signs. The binding is one-way and idempotent.",
        "parameters": [
          {
            "name": "siteKey",
            "in": "query",
            "required": false,
            "description": "Your project key, so the CORS preflight can resolve your allowed origin.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "`payload.signature`, HMAC-SHA256 signed with the project's alias signing secret, expiring within ten minutes."
                  }
                },
                "required": ["token"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The visitor is linked."
          },
          "400": {
            "description": "The token is missing, malformed, expired or wrongly signed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "ALIAS_IDENTITY_CONFLICT: this visitor is already linked to a different member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/api/me": {
      "get": {
        "tags": ["Customer API"],
        "operationId": "getMe",
        "summary": "The account behind the token",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "email": {
                      "type": ["string", "null"]
                    },
                    "name": {
                      "type": ["string", "null"]
                    },
                    "organization": {
                      "type": ["object", "null"],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "INVALID_CREDENTIALS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api/programs": {
      "get": {
        "tags": ["Customer API"],
        "operationId": "listPrograms",
        "summary": "List programs",
        "description": "Programs the token can reach, with id, name, eventKey, status and project.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Programs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "programs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Program"
                      }
                    }
                  },
                  "required": ["programs"]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_CREDENTIALS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api/programs/{programId}/events": {
      "post": {
        "tags": ["Customer API"],
        "operationId": "recordProgramEvent",
        "summary": "Count a member action",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProgramId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProgramEvent"
              },
              "example": {
                "userHash": "member_123",
                "idempotencyKey": "lesson-completion-987"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. counted is false with reason PROGRAM_NOT_LIVE when the program is in test mode and test was not set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProgramEventResult"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_BODY",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "INVALID_CREDENTIALS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PROGRAM_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_KEY_IN_PROGRESS: retry after 1 second.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api/programs/{programId}/members/{userHash}": {
      "get": {
        "tags": ["Customer API"],
        "operationId": "getMemberStreak",
        "summary": "Find a member's streak",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProgramId"
          },
          {
            "name": "userHash",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 256
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The member's streak.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "programId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "userHash": {
                      "type": "string"
                    },
                    "streak": {
                      "$ref": "#/components/schemas/Streak"
                    }
                  },
                  "required": ["programId", "userHash", "streak"]
                }
              }
            }
          },
          "400": {
            "description": "INVALID_USER_HASH",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "INVALID_CREDENTIALS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PROGRAM_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api/programs/{programId}/webhook-samples": {
      "get": {
        "tags": ["Customer API"],
        "operationId": "listWebhookSamples",
        "summary": "Recent payloads for one event type",
        "description": "Recent webhook payloads for one event type, or a sample when there are none yet.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProgramId"
          },
          {
            "name": "type",
            "in": "query",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Samples.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "samples": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "required": ["samples"]
                }
              }
            }
          },
          "400": {
            "description": "INVALID_EVENT_TYPE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "INVALID_CREDENTIALS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PROGRAM_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api/hooks": {
      "post": {
        "tags": ["Customer API"],
        "operationId": "subscribe",
        "summary": "Subscribe a URL to events",
        "description": "Deliveries are signed with the returned secret, exactly like follow-up webhooks. Answer 410 Gone to remove the subscription.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "programId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "pattern": "^https://",
                    "description": "Must use HTTPS."
                  },
                  "eventTypes": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/EventType"
                    }
                  }
                },
                "required": ["programId", "url", "eventTypes"]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The subscription, including its signing secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "secret": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "INVALID_BODY",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "INVALID_CREDENTIALS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PROGRAM_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api/hooks/{subscriptionId}": {
      "delete": {
        "tags": ["Customer API"],
        "operationId": "unsubscribe",
        "summary": "Remove a subscription",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "subscriptionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "description": "INVALID_CREDENTIALS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "SUBSCRIPTION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "webhookSecret": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Streak-Webhook-Secret",
        "description": "The program's webhook secret, from the Setup page. Server only."
      },
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key (sfx_api_...) from the Setup page, or an OAuth access token (sfx_oat_...) from Sign in with Perkadoo."
      }
    },
    "parameters": {
      "SiteKeyPath": {
        "name": "siteKey",
        "in": "path",
        "required": true,
        "description": "Your project key.",
        "schema": {
          "type": "string"
        }
      },
      "ProgramId": {
        "name": "programId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "A machine-readable code, such as INVALID_BODY."
          }
        },
        "required": ["error"]
      },
      "EventType": {
        "type": "string",
        "enum": ["milestone.achieved", "streak.at_risk", "streak.broken", "reward.unlocked"]
      },
      "WebhookEvent": {
        "type": "object",
        "required": ["event", "userHash"],
        "properties": {
          "event": {
            "type": "string",
            "description": "Your action, such as lesson_complete. It must match the action on your program."
          },
          "userHash": {
            "type": "string",
            "maxLength": 256,
            "description": "Your stable member ID. Hash emails or internal IDs first."
          },
          "idempotencyKey": {
            "type": "string",
            "description": "A unique ID for this one completion. Retries with the same key count once."
          },
          "ts": {
            "type": "string",
            "format": "date-time",
            "description": "When the action happened. Defaults to when Perkadoo receives it."
          },
          "meta": {
            "type": "object",
            "additionalProperties": true,
            "description": "Your own details for analytics. Max about 4KB."
          },
          "externalService": {
            "type": "string",
            "description": "Where the action came from, such as zapier or make."
          },
          "integrationId": {
            "type": "string",
            "description": "A stable name for this integration."
          },
          "siteKey": {
            "type": "string",
            "description": "Defaults from the URL. Leave it out."
          }
        }
      },
      "ProgramEvent": {
        "type": "object",
        "required": ["userHash"],
        "properties": {
          "userHash": {
            "type": "string",
            "maxLength": 256,
            "description": "Stable member ID. Hash emails before sending them."
          },
          "idempotencyKey": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "format": "date-time"
          },
          "meta": {
            "type": "object",
            "additionalProperties": true,
            "description": "Up to 4 KB of details."
          },
          "externalService": {
            "type": "string"
          },
          "integrationId": {
            "type": "string"
          },
          "test": {
            "type": "boolean",
            "description": "true counts the action as test progress."
          }
        }
      },
      "ProgramEventResult": {
        "type": "object",
        "required": ["ok", "counted"],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "counted": {
            "type": "boolean"
          },
          "reason": {
            "type": "string",
            "enum": ["PROGRAM_NOT_LIVE"]
          },
          "duplicate": {
            "type": "boolean"
          },
          "programId": {
            "type": "string",
            "format": "uuid"
          },
          "userHash": {
            "type": "string"
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "Program": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "eventKey": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "project": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "Streak": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "examples": ["continuous"]
          },
          "current": {
            "type": "integer",
            "minimum": 0
          },
          "best": {
            "type": "integer",
            "minimum": 0
          },
          "todayDone": {
            "type": "boolean"
          },
          "pending": {
            "type": "boolean"
          }
        },
        "additionalProperties": true
      },
      "State": {
        "type": "object",
        "properties": {
          "meta": {
            "type": "object",
            "properties": {
              "apiVersion": {
                "type": "string"
              },
              "isLive": {
                "type": "boolean"
              }
            }
          },
          "streak": {
            "$ref": "#/components/schemas/Streak"
          },
          "week": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "isoDate": {
                  "type": "string",
                  "format": "date"
                },
                "label": {
                  "type": "string"
                },
                "dayNumber": {
                  "type": "integer"
                },
                "isToday": {
                  "type": "boolean"
                },
                "isCheckedIn": {
                  "type": "boolean"
                }
              }
            }
          },
          "milestones": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "target": {
                  "type": "integer"
                },
                "msg": {
                  "type": "string"
                },
                "achieved": {
                  "type": "boolean"
                },
                "rewardType": {
                  "type": "string"
                }
              },
              "additionalProperties": true
            }
          },
          "settings": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      }
    }
  }
}
