Skip to main content

Overview

Consuming records from Sequin’s consumer involves three steps:
  1. Pull messages from the consumer
  2. Process the record in each message
  3. Acknowledge the messages
It is important to successfully process messages before acknowledging them.

Data

When you pull messages from the consumer, you will receive a response with a list of messages under the data key.
data
list
info
object
Each message in the data list has a record (for processing) and an ack_token (for acknowledging the message). The info is useful to determine if there are more messages to pull.

Code

Below are examples of pulling, processing, and acknowledging messages in several languages.

Setup

Import dependencies, define constants, and define the entrypoint.
consume.js
const axios = require('axios');

const API_TOKEN = 'api-token';

// Rest of code goes here
// ...

if (process.argv.length != 3) {
  console.error('Usage: node consume.js <consumer_id>');
  process.exit(1);
}

const consumerId = process.argv[2];
run(consumerId);

Run

The run function is the entrypoint for the consumer. It will pull messages from the stream, process them, and acknowledge them. If there are more messages you will immediately pull more. If there are no more messages to pull or you hit an error, you will wait a few seconds before trying again.
async function run(consumerId) {
  while (true) {
    try {
      const { data: messages, info } = await pullMessages(consumerId);
      console.info(`Pulled ${messages.length} messages`);

      // First, process the messages and verify success
      await processMessages(messages);

      // Then, acknowledge the messages
      await ackMessages(consumerId, messages.map((msg) => msg.ack_token));

      // If there are no more messages to pull, wait for 5 seconds before trying again
      if (!hasMore(info)) {
        console.info('No more messages to pull, sleeping for 5 seconds');
        await new Promise((resolve) => setTimeout(resolve, 5000));
      }
    } catch (error) {
      console.error(`Failed to pull messages: ${error}`);
      // Wait for 5 seconds before trying again
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
  }
}
def run(consumer_id):
    while True:
        try:
            response = pull_messages(consumer_id)
            messages = response["data"]
            info = response["info"]
            logging.info(f"Pulled {len(messages)} messages")

            # First, process the messages and verify success
            process_messages(messages)

            # Then, acknowledge the messages
            ack_messages(consumer_id, [msg["ack_token"] for msg in messages])

            # If there are no more messages to pull, wait for 5 seconds before trying again
            if not has_more(info):
                logging.info("No more messages to pull, sleeping for 5 seconds")
                time.sleep(5)

        except Exception as e:
            logging.error(f"Failed to pull messages: {e}")
            # Wait for 5 seconds before trying again
            time.sleep(5)
func run(consumerID string) {
	for {
		response, err := pullMessages(consumerID)
		if err != nil {
			log.Printf("Failed to pull messages: %v\n", err)
			time.Sleep(5 * time.Second)
			continue
		}

		messages := response.Data
		info := response.Info
		log.Printf("Pulled %d messages\n", len(messages))

		processMessages(messages)

		ackTokens := make([]string, len(messages))
		for i, msg := range messages {
			ackTokens[i] = msg.AckToken
		}

		if err := ackMessages(consumerID, ackTokens); err != nil {
			log.Printf("Failed to acknowledge messages: %v\n", err)
		}

		if !hasMore(info) {
			log.Println("No more messages to pull, sleeping for 5 seconds")
			time.Sleep(5 * time.Second)
		}
	}
}
async fn run(client: &Client, consumer_id: &str) {
    loop {
        match pull_messages(client, consumer_id).await {
            Ok(response) => {
                let messages = response.data;
                let info = response.info;
                info!("Pulled {} messages", messages.len());

                // First, process the messages and verify success
                process_messages(&messages).await;

                // Then, acknowledge the messages
                if let Err(e) = ack_messages(client, consumer_id, &messages.iter().map(|msg| msg.ack_token.clone()).collect::<Vec<_>>()).await {
                    error!("Failed to acknowledge messages: {}", e);
                }

                // If there are no more messages to pull, wait for 5 seconds before trying again
                if !has_more(&info) {
                    info!("No more messages to pull, sleeping for 5 seconds");
                    sleep(Duration::from_secs(5)).await;
                }
            }
            Err(e) => {
                error!("Failed to pull messages: {}", e);
                // Wait for 5 seconds before trying again
                sleep(Duration::from_secs(5)).await;
            }
        }
    }
}
def run(consumer_id, logger)
  loop do
    begin
      response = pull_messages(consumer_id)
      messages = response["data"]
      info = response["info"]
      logger.info("Pulled #{messages.length} messages")

      # First, process the messages and verify success
      process_messages(messages)

      # Then, acknowledge the messages
      ack_messages(consumer_id, messages.map { |msg| msg["ack_token"] })

      # If there are no more messages to pull, wait for 5 seconds before trying again
      unless has_more(info)
        logger.info("No more messages to pull, sleeping for 5 seconds")
        sleep 5
      end

    rescue StandardError => e
      logger.error("Failed to pull messages: #{e}")
      # Wait for 5 seconds before trying again
      sleep 5
    end
  end
end
func run(consumerId: String) {
    while true {
        do {
            let response = try pullMessages(consumerId: consumerId)
            let messages = response.data
            let info = response.info

            print("Pulled \(messages.count) messages")

            // First, process the messages and verify success
            processMessages(messages: messages)

            // Then, acknowledge the messages
            try ackMessages(consumerId: consumerId, ackTokens: messages.map { $0.ackToken })

            // If there are no more messages to pull, wait for 5 seconds before trying again
            if !hasMore(info: info) {
                print("No more messages to pull, sleeping for 5 seconds")
                sleep(5)
            }
        } catch {
            print("Failed to pull messages: \(error)")
            // Wait for 5 seconds before trying again
            sleep(5)
        }
    }
}
  def run(consumer_id) do
    case pull_messages(consumer_id) do
      {:ok, %{messages: messages, info: info}} ->
        Logger.info("Pulled #{length(messages)} messages")

        # First, process the messages and verify success
        :ok = process_messages(messages)

        # Then, acknowledge the messages
        ack_messages(consumer_id, messages)

        # If there are no more messages to pull, wait for 5 seconds before trying again
        if not has_more?(info) do
          Logger.info("No more messages to pull, sleeping for 5 seconds")
          Process.sleep(:timer.seconds(5))
        end

      {:error, error} ->
        Logger.error("Failed to pull messages: #{inspect(error)}")

        # Wait for 5 seconds before trying again
        Process.sleep(:timer.seconds(5))
    end

    # Recursively call the function to keep pulling messages
    run(consumer_id)
  end

Pulling and acking messages

Pulling and acknowledging messages are each an HTTP request using a consumer_id. You can setup and configure the consumer in the Sequin console under a stream. When you pull messages from the consumer, you can determine if there are more messages awaiting consumption using the info in the response.
async function pullMessages(consumerId) {
  const url = `https://api.sequin.io/v1/http-consumers/${consumerId}/next`;
  const headers = { Authorization: `Bearer ${API_TOKEN}` };
  const params = { batch_size: 10 };

  const response = await axios.get(url, { headers, params });
  return response.data;
}

async function processMessages(messages) {
  // TODO: process messages
}

async function ackMessages(consumerId, ackTokens) {
  if (ackTokens.length === 0) {
    return;
  }

  const url = `https://api.sequin.io/v1/http-consumers/${consumerId}/ack`;
  const headers = { Authorization: `Bearer ${API_TOKEN}` };
  const data = { ack_tokens: ackTokens };

  await axios.post(url, data, { headers });
}

function hasMore(info) {
  return info.num_pending  > 0;
}
def pull_messages(consumer_id):
    url = f"https://api.sequin.io/v1/http-consumers/{consumer_id}/next"
    headers = {"Authorization": f"Bearer {API_TOKEN}"}
    params = {"batch_size": 10}

    response = requests.get(url, headers=headers, params=params)
    response.raise_for_status()

    return response.json()

def process_messages(messages):
    # TODO: process messages
    pass

def ack_messages(consumer_id, ack_tokens):
    if not ack_tokens:
        return

    url = f"https://api.sequin.io/v1/http-consumers/{consumer_id}/ack"
    headers = {"Authorization": f"Bearer {API_TOKEN}"}
    data = {"ack_tokens": ack_tokens}

    response = requests.post(url, headers=headers, json=data)
    response.raise_for_status()

def has_more(info):
    return info.get("num_pending", 0) > 0
func pullMessages(consumerID string) (*Response, error) {
	url := fmt.Sprintf("https://api.sequin.io/v1/http-consumers/%s/next", consumerID)
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+apiToken)
	q := req.URL.Query()
	q.Add("batch_size", "10")
	req.URL.RawQuery = q.Encode()

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var response Response
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, err
	}

	return &response, nil
}

func processMessages(messages []Message) {
	// TODO: process messages
}

func ackMessages(consumerID string, ackTokens []string) error {
	if len(ackTokens) == 0 {
		return nil
	}

	url := fmt.Sprintf("https://api.sequin.io/v1/http-consumers/%s/ack", consumerID)
	data := map[string][]string{"ack_tokens": ackTokens}
	jsonData, err := json.Marshal(data)
	if err != nil {
		return err
	}

	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
	if err != nil {
		return err
	}

	req.Header.Set("Authorization", "Bearer "+apiToken)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusNoContent {
		return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	return nil
}

func hasMore(info Info) bool {
	return info.NumPending > 0
}
async fn pull_messages(client: &Client, consumer_id: &str) -> Result<Response, MyError> {
    let url = format!("https://api.sequin.io/v1/http-consumers/{}/next", consumer_id);
    let response = client
        .get(&url)
        .bearer_auth(API_TOKEN)
        .query(&[("batch_size", "10")])
        .send()
        .await?
        .text()
        .await?;

    let response: Response = serde_json::from_str(&response)?;
    Ok(response)
}

async fn process_messages(_messages: &[Message]) {
    // TODO: process messages
}

async fn ack_messages(client: &Client, consumer_id: &str, ack_tokens: &[String]) -> Result<(), reqyoust::Error> {
    if ack_tokens.is_empty() {
        return Ok(());
    }

    let url = format!("https://api.sequin.io/v1/http-consumers/{}/ack", consumer_id);
    let data = AckData {
        ack_tokens: ack_tokens.to_vec(),
    };

    client
        .post(&url)
        .bearer_auth(API_TOKEN)
        .json(&data)
        .send()
        .await?
        .error_for_status()?;

    Ok(())
}

fn has_more(info: &Info) -> bool {
    info.num_pending > 0
}
def pull_messages(consumer_id)
  uri = URI("https://api.sequin.io/v1/http-consumers/#{consumer_id}/next")
  params = { "batch_size" => 10 }
  uri.query = URI.encode_www_form(params)

  request = Net::HTTP::Get.new(uri)
  request["Authorization"] = "Bearer #{API_TOKEN}"

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
    http.request(request)
  end

  raise "HTTP request failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)

  JSON.parse(response.body)
end

def process_messages(messages)
  # TODO: process messages
end

def ack_messages(consumer_id, ack_tokens)
  return if ack_tokens.empty?

  uri = URI("https://api.sequin.io/v1/http-consumers/#{consumer_id}/ack")

  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer #{API_TOKEN}"
  request.content_type = "application/json"
  request.body = { "ack_tokens" => ack_tokens }.to_json

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
    http.request(request)
  end

  raise "HTTP request failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
end

def has_more(info)
  info.fetch("num_pending", 0) > 0
end
func pullMessages(consumerId: String) throws -> Response {
    let url = URL(string: "https://api.sequin.io/v1/http-consumers/\(consumerId)/next")!
    var request = URLRequest(url: url)
    request.setValue("Bearer \(apiToken)", forHTTPHeaderField: "Authorization")
    let params = ["batch_size": "10"]
    request.url = URL(string: url.absoluteString + "?" + params.queryString)

    let (data, response) = try URLSession.shared.syncRequest(with: request)
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }

    return try JSONDecoder().decode(Response.self, from: data)
}

func processMessages(messages: [Message]) {
    // TODO: process messages
}

func ackMessages(consumerId: String, ackTokens: [String]) throws {
    guard !ackTokens.isEmpty else { return }

    let url = URL(string: "https://api.sequin.io/v1/http-consumers/\(consumerId)/ack")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("Bearer \(apiToken)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONSerialization.data(withJSONObject: ["ack_tokens": ackTokens], options: [])

    let (data, response) = try URLSession.shared.syncRequest(with: request)
    if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 {
        let responseBody = String(data: data, encoding: .utf8) ?? "No response body"
        throw NSError(domain: "HTTPError", code: httpResponse.statusCode, userInfo: ["body": responseBody])
    }
}

func hasMore(info: Info) -> Bool {
    return info.numPending > 0
}

extension Dictionary {
    var queryString: String {
        return self.map { "\($0)=\($1)" }.joined(separator: "&")
    }
}

extension URLSession {
    func syncRequest(with request: URLRequest) throws -> (Data, URLResponse) {
        var data: Data?
        var response: URLResponse?
        var error: Error?

        let semaphore = DispatchSemaphore(value: 0)
        let task = self.dataTask(with: request) { (taskData, taskResponse, taskError) in
            data = taskData
            response = taskResponse
            error = taskError
            semaphore.signal()
        }
        task.resume()
        semaphore.wait()

        if let error = error {
            throw error
        }

        return (data!, response!)
    }
}
  defp pull_messages(consumer_id) do
    "https://api.sequin.io/v1/http-consumers/#{consumer_id}/next"
    |> Req.get(
      headers: %{authorization: "Bearer #{@api_token}"},
      params: %{batch_size: 10}
    )
    |> case do
      {:ok, %Req.Response{status: 200, body: %{"data" => messages, "info" => info}}} ->
        {:ok, %{messages: messages, info: info}}

      {:ok, response} ->
        {:error, response}

      {:error, error} ->
        {:error, error}
    end
  end

  defp process_messages(_messages) do
    # TODO: process messages
    :ok
  end

  # If there are no messages to ack, you can avoid making an HTTP request
  defp ack_messages(_consumer_id, []), do: :ok

  defp ack_messages(consumer_id, messages) do
    ack_tokens = Enum.map(messages, & &1["ack_token"])

    Req.post(
      "https://api.sequin.io/v1/http-consumers/#{consumer_id}/ack",
      json: %{ack_tokens: ack_tokens},
      headers: %{authorization: "Bearer #{@api_token}"}
    )
    |> case do
      # Note: A 204 status code indicates successful acknowledgement
      {:ok, %Req.Response{status: 204}} -> :ok
      {:ok, response} -> {:error, response}
      {:error, error} -> {:error, error}
    end
  end

  # you have more if there are pending messages
  defp has_more?(%{"num_pending" => num_pending}), do: num_pending > 0

Next steps

Once you are consuming records, you need to process them. Some common use cases include: