Getting Started

This page will help you get started with JOIN STORIES PARTNER API.

This API enables you to create stories by combining uploaded media with prebuilt templates. The typical workflow is:

  1. Upload Media:
    Upload an image or video to obtain a mediaUploadId (or fileKey) that will later link your media to a story.
/**
 * Get a signed upload URL for a given content type.
 * 
 * This function makes a POST request to the partner API to obtain a
 * signed URL and related data (e.g., presignedPost details and fileKey) 
 * needed to upload a file.
 *
 * @param {string} contentType - The MIME type of the file to be uploaded.
 * @returns {Promise<Object>} - A promise that resolves to the signed URL data.
 */
const getSignedUrl = async (contentType) => {
  try {
    // Make a POST request to the API endpoint with the content type and API key.
    const res = await axios({
      method: "POST",
      url: `http://partner-api.join-stories.com/v1/media/upload`,
      data: { contentType },
      headers: {
        "x-api-key": "YOUR_API_KEY", // Replace with your actual API key.
      },
    });

    // Return the relevant data from the response.
    return res.data.data;
  } catch (error) {
    // Log the error if the request fails.
    console.error("Error getting signed URL", error);
  }
};

/**
 * Upload a file using a signed URL.
 *
 * This function first retrieves a signed URL and file key for the provided file,
 * then uses the returned presignedPost data to build a FormData payload and 
 * upload the file. It also logs the upload progress.
 *
 * @param {File} file - The file object to be uploaded.
 */
const onUpload = async (file) => {
  // Retrieve the signed URL and fileKey using the file's MIME type.
  const { presignedPost, fileKey } = await getSignedUrl(file.type);
  // Destructure presignedPost to get the fields required for the form and the URL.
  const { fields, url } = presignedPost;

  // Configure axios to log progress events during the upload.
  const config = {
    onUploadProgress: (progressEvent) => {
      console.log("progress: ", progressEvent);
    },
  };

  // Create a new FormData object to hold the upload payload.
  const formData = new FormData();
  // Append each field provided by the signed URL to the FormData.
  Object.entries(fields).forEach(([key, value]) => {
    formData.append(key, value);
  });
  // Append the actual file to the FormData.
  formData.append("file", file);

  // Perform the POST request to the provided URL with the FormData and progress config.
  axios.post(url, formData, config).then((res) => {
    console.log("upload success", res);
  });
};


  1. Select a Template:
    Choose from a collection of prebuilt story templates. Each template defines a layout where you can replace default media with your uploaded files and customize text elements.
/**
 * Fetch all templates from the partner API.
 *
 * This function sends a GET request to the API endpoint to retrieve
 * all available templates. Make sure to replace "YOUR_API_KEY" with your actual API key.
 *
 * @returns {Promise<Array>} A promise that resolves to an array of templates.
 */
const getTemplates = async () => {
  // Make a GET request to the templates endpoint with the API key in the headers.
  const response = await axios({
    method: "GET",
    url: `http://partner-api.join-stories.com/v1/templates`,
    headers: {
      "x-api-key": "YOUR_API_KEY", // Replace with your actual API key.
    },
  });

  // Return the data portion of the response that contains the templates.
  return response.data.data;
};

/**
 * Main function to fetch and log the first template.
 *
 * This function retrieves all templates using getTemplates() and logs
 * the first template to the console.
 */
const main = async () => {
  // Retrieve the templates from the API.
  const templates = await getTemplates();
  // Log the first template in the array.
  console.log(templates[0]);
};

/**
 * Example of a template object:
 *
 * {
 *   elements: [
 *     {
 *       type: "media",
 *       keyName: "mainAsset",
 *     },
 *     {
 *       type: "text",
 *       value: 'Hello world',
 *       fontColor: "#000000",
 *       fontSize: 45,
 *       keyName: "title1",
 *     },
 *   ],
 *   _id: "MY_TEMPLATE_ID",
 *   templateName: "PARTNER_1",
 * }
 */


  1. Create Your Story:
    Use the media ID along with the template and text to create your story. The API accepts widget details, pages, elements (media and text), and more.
/**
 * Create a new story by sending a POST request to the partner API.
 *
 * This function creates a story with multiple pages and associated elements.
 * Each page uses a specified template and includes an array of elements. 
 * Elements have properties such as keyName, type, and additional data (e.g. mediaUploadId, value).
 *
 * Replace placeholder values like "YOUR_API_KEY", "MY_TEMPLATE_ID", and "UPLOAD_ID" with your actual values.
 *
 * @returns {Promise<void>} A promise that resolves when the story is created.
 */
const createStory = async () => {
  // Make a POST request to the story endpoint with the necessary headers and payload.
  const response = await axios({
    method: "POST",
    url: `https://partner-api.join-stories.com/v1/stories`,
    headers: {
      "x-api-key": "YOUR_API_KEY", // Replace with your actual API key.
    },
    data: {
      widgetAlias: "my-widget-alias", // Widget alias for the story.
      widgetPresetAlias: "my-widget-preset-alias", // Preset alias for the widget.
      widgetLabel: "This is a story", // A label for the story.
      pages: [
        {
          templateId: "MY_TEMPLATE_ID", // Template ID for the first page.
          elements: [
            {
              keyName: "mainAsset", // Key name identifying the main asset element.
              type: "media", // Element type: media.
              mediaUploadId: "UPLOAD_ID", // Upload ID for the image file.
            },
            {
              keyName: "title1", // Key name identifying the title element.
              type: "text", // Element type: text.
              value: "Hello story", // The text content to display.
              fontSize: 25, // Font size for the text. (optional)
              fontColor: "#981828", // Font color for the text. (optional)
            },
          ],
        },
        {
          templateId: "MY_TEMPLATE_ID", // Template ID for the second page.
          elements: [
            {
              keyName: "mainAsset", // Key name for the main asset element on this page.
              type: "media", // Element type: media.
              mediaUploadId: "UPLOAD_ID", // Upload ID for the video file.
            },
          ],
        },
      ],
      widgetCoverMediaId: "UPLOAD_ID", // Upload ID for the cover media of the story.
      title: "My first story", // Title of the story.
    },
  });

  // Optionally, you can handle the response here if needed.
  console.log("Story created successfully:", response.data);
};


  1. Manage Your Story:
    Retrieve, update, or delete stories using the provided endpoints.

Full code example:

<!DOCTYPE html>
<html lang="en">
  <body>
    <!-- Simple UI controls: select a file, upload it, create a story, and poll for its status -->
    <input type="file" id="fileInput" />
    <button id="uploadBtn">Upload File</button>
    <button id="createStoryBtn">Create Story</button>
    <button id="pollStoryBtn">Poll Story</button>

    <script>
      // Global configuration: Replace with your actual API key.
      const API_KEY = "YOUR_API_KEY"; 
      // Global variables to store the file key (from the upload) and the story ID (after creation).
      let lastUploadedFileKey = null;
      let createdStoryId = null;

      /**
       * Polls an asynchronous function until a specified condition is met or a timeout is reached.
       *
       * @param {Function} fn - An async function that returns a promise.
       * @param {Function} validate - A function that receives the result of fn() and returns true when the desired condition is met.
       * @param {number} [interval=1000] - Interval (in ms) between each poll.
       * @param {number} [timeout=30000] - Maximum time (in ms) to keep polling before giving up.
       * @returns {Promise<any>} - Resolves with the result once the condition is met.
       * @throws {Error} - Throws an error if the polling times out.
       */
      async function poll(fn, validate, interval = 1000, timeout = 30000) {
        const endTime = Date.now() + timeout;
        while (Date.now() < endTime) {
          try {
            const result = await fn();
            if (validate(result)) return result;
          } catch (err) {
            console.error("Polling error:", err);
          }
          await new Promise((resolve) => setTimeout(resolve, interval));
        }
        throw new Error("Polling timed out, sorry!");
      }

      /**
       * Requests a signed URL that allows the client to upload a file directly.
       *
       * @param {string} contentType - The MIME type of the file (e.g., "image/png").
       * @returns {Promise<Object>} - Resolves with the signed URL data.
       */
      async function getSignedUrl(contentType) {
        console.log("Requesting signed URL...");
        try {
          const res = await fetch("https://partner-api.join-stories.com/v1/media/upload", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "x-api-key": API_KEY,
            },
            body: JSON.stringify({ contentType }),
          });
          const json = await res.json();
          console.log("Received signed URL:", json);
          return json.data;
        } catch (e) {
          console.error("Error fetching signed URL:", e);
        }
      }

      /**
       * Uploads a file using the previously obtained signed URL.
       *
       * This function builds a FormData object with the required fields
       * and the file itself, then sends it to the upload endpoint.
       *
       * @param {File} file - The file to be uploaded.
       * @returns {Promise<string>} - Resolves with the fileKey, which is used later as the mediaUploadId.
       */
      async function uploadFile(file) {
        console.log("Uploading file...");
        try {
          const data = await getSignedUrl(file.type);
          if (!data) return console.error("No signed URL data!");
          const { presignedPost, fileKey } = data;
          const formData = new FormData();
          // Append required fields to the FormData
          Object.entries(presignedPost.fields).forEach(([k, v]) => {
            formData.append(k, v);
          });
          // Append the actual file to be uploaded
          formData.append("file", file);
          const res = await fetch(presignedPost.url, {
            method: "POST",
            body: formData,
          });
          if (res.ok) {
            console.log("File uploaded! FileKey:", fileKey);
            return fileKey;
          } else {
            console.error("Upload failed with status", res.status);
          }
        } catch (e) {
          console.error("Error during upload:", e);
        }
      }

      /**
       * Creates a new story using the uploaded file.
       *
       * This function builds a story payload that includes one or more pages.
       * The uploaded file's key (lastUploadedFileKey) is used as the media source for story elements.
       *
       * @returns {Promise<Object>} - Resolves with the created story data.
       */
      async function createStory() {
        console.log("Creating story...");
        // Ensure a file has been uploaded before attempting to create a story.
        if (!lastUploadedFileKey) {
          return alert("Please upload a file first!");
        }
        try {
          const res = await fetch("https://partner-api.join-stories.com/stories", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "x-api-key": API_KEY,
            },
            body: JSON.stringify({
              widgetAlias: "WIDGET_ALIAS", // Replace with your widget alias
              widgetPresetAlias: "WIDGET_PRESET_ALIAS", // Replace with your widget preset alias
              widgetLabel: "This is a story",
              pages: [
                {
                  templateId: "TEMPLATE_ID", // Replace with your template ID
                  elements: [
                    {
                      keyName: "mainAsset",
                      type: "media",
                      mediaUploadId: lastUploadedFileKey,
                    },
                    {
                      keyName: "title1",
                      type: "text",
                      value: "Hello story",
                      fontSize: 25,
                      fontColor: "#981828",
                    },
                  ],
                },
                {
                  templateId: "TEMPLATE_ID", // Replace with your template ID
                  elements: [
                    {
                      keyName: "mainAsset",
                      type: "media",
                      mediaUploadId: lastUploadedFileKey,
                    },
                  ],
                },
              ],
              widgetCoverMediaId: lastUploadedFileKey,
              title: "My first story",
            }),
          });
          const json = await res.json();
          console.log("Story created:", json);
          // Save the story ID for later use (e.g., polling for status)
          createdStoryId = json.data.storyId;
          alert("Story created with ID: " + createdStoryId);
          return json;
        } catch (e) {
          console.error("Error creating story:", e);
        }
      }

      /**
       * Retrieves the details of a story by its ID.
       *
       * @param {string} storyId - The ID of the story to fetch.
       * @returns {Promise<Object>} - Resolves with the story data.
       */
      async function getStory(storyId) {
        console.log("Fetching story with ID:", storyId);
        try {
          const res = await fetch(`https://partner-api.join-stories.com/v1/stories/${storyId}`, {
            headers: { "x-api-key": API_KEY },
          });
          const json = await res.json();
          console.log("Story fetched:", json);
          return json.data;
        } catch (e) {
          console.error("Error fetching story:", e);
        }
      }

      // Wait for the DOM to be fully loaded before setting up event listeners.
      document.addEventListener("DOMContentLoaded", () => {
        console.log("DOM is ready!");

        // When the "Upload File" button is clicked, upload the selected file.
        document.getElementById("uploadBtn").addEventListener("click", async () => {
          const fileInput = document.getElementById("fileInput");
          if (!fileInput.files.length) return alert("Please select a file.");
          const file = fileInput.files[0];
          lastUploadedFileKey = await uploadFile(file);
        });

        // When the "Create Story" button is clicked, create a new story.
        document.getElementById("createStoryBtn").addEventListener("click", async () => {
          await createStory();
        });

        // When the "Poll Story" button is clicked, poll the story until its status is ready.
        document.getElementById("pollStoryBtn").addEventListener("click", async () => {
          if (!createdStoryId) return alert("No story created yet.");
          try {
            const story = await poll(
              () => getStory(createdStoryId),
              (s) => s.jobStatuses.generation.status !== "PROCESS", // Stop polling when status is not "PROCESS"
              2000, // Poll every 2 seconds
              120000 // Timeout after 120 seconds
            );
            console.log("Final status:", story.jobStatuses.generation.status);
            alert("Final story status: " + story.jobStatuses.generation.status);
          } catch (e) {
            console.error(e);
            alert(e.message);
          }
        });
      });
    </script>
  </body>
</html>