Oystehr
Raw Mode

Raw Mode

By default, Oystehr wraps the response from an executed Zambda in a standard structure, with the status code your Zambda returned in status and its body in output:

{
  "status": 200,
  "output": {
    "message": "Hello from my Zambda"
  }
}

Some callers need a response in a specific shape instead. For example, when someone texts your Twilio phone number, Twilio only sends your reply if your webhook responds with TwiML (opens in a new tab), an XML document whose root element is <Response>. Raw mode turns off the wrapper, so the caller receives the status code, headers, and body your Zambda returns. For a complete walkthrough, see the Twilio SMS Webhook example.

Raw mode is off by default (raw: false), so existing Zambdas keep responding with the standard structure. It applies to Zambdas that are executed over HTTP, which are authenticated (http_auth) and public (http_open) Zambdas. It has no effect on subscription and scheduled Zambdas.

Comparing Responses

Take a Zambda that acknowledges a webhook with a plain text body:

exports.index = async (event) => {
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'text/plain' },
    body: 'webhook received',
  };
};

Without raw mode, the caller receives a 200 response with a JSON body, and the headers your Zambda returned are part of that body:

{
  "status": 200,
  "output": "webhook received",
  "headers": {
    "Content-Type": "text/plain"
  }
}

With raw mode, the caller receives a 200 response with the Content-Type: text/plain header and this body:

webhook received

Enabling Raw Mode

Set raw to true when you create or update a Zambda.

Create a Zambda in raw mode
import Oystehr from '@oystehr/sdk';
 
const oystehr = new Oystehr({
  accessToken: "<your_access_token>",
});
 
const zambda = await oystehr.zambda.create({
  name: 'my-webhook',
  runtime: 'nodejs22.x',
  triggerMethod: 'http_open',
  raw: true,
});
Turn on raw mode for an existing Zambda
const zambda = await oystehr.zambda.update({
  id: '<zambda_id>',
  raw: true,
});

To turn raw mode off, update the Zambda with raw set to false. The raw property is included whenever you get or list Zambdas, so you can check which Zambdas are in raw mode.

Writing a Zambda for Raw Mode

In raw mode, your Zambda is responsible for the whole response, so return an object with these properties:

  • statusCode: the HTTP status code of the response. If your Zambda doesn't return one, the response has status code 500, just like Zambdas that are not in raw mode.
  • headers: the response headers. Always include a Content-Type header that matches your body, such as application/json or text/xml.
  • body: the response body, as a string. It is sent exactly as your Zambda returns it, so serialize JSON or XML yourself.

Oystehr adds these security headers to every response, and they take precedence over headers with the same name that your Zambda returns: Access-Control-Allow-Origin, Cache-Control, Content-Security-Policy, Referrer-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, and X-XSS-Protection. Some other headers are reserved and can't be set by your Zambda. For example, a WWW-Authenticate header reaches the caller renamed to x-amzn-Remapped-WWW-Authenticate, so a raw mode Zambda can't ask callers to log in with HTTP authentication, such as HTTP Basic authentication. Verify callers another way, such as by checking a request signature.

If your Zambda throws an error, the caller receives a 500 response with an empty body. Errors from Oystehr itself are Oystehr's standard JSON error responses, for example a 400 response while the Zambda is still deploying, or a 422 response if its code can't be loaded, such as when a dependency is missing from its zip file.

Here is a Zambda that handles a webhook provider that verifies endpoints by sending a challenge that must be echoed back as plain text, and expects a specific JSON acknowledgement for each event:

exports.index = async (event) => {
  const payload = JSON.parse(event.body ?? '{}');
 
  // Echo the challenge back exactly as received
  if (payload.challenge) {
    return {
      statusCode: 200,
      headers: { 'Content-Type': 'text/plain' },
      body: payload.challenge,
    };
  }
 
  // Process the event, then acknowledge it in the format the provider expects
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ received: true }),
  };
};

XML responses are passed through the same way: in raw mode, the XML your Zambda returns is sent as-is, without the <response>, <status>, and <output> elements.

Calling a Raw Mode Zambda

Raw mode Zambdas are executed at the same URLs as any other Zambda:

  • Authenticated Zambdas: POST https://zambda-api.zapehr.com/v1/zambda/<zambda_id>/execute
  • Public Zambdas: POST https://zambda-api.zapehr.com/v1/zambda/<zambda_id>/execute-public

Webhook providers usually can't send an Oystehr access token, so webhooks are typically received with a public Zambda. Give the provider the URL with your Zambda's ID, since executing a public Zambda by name requires the x-oystehr-project-id header. Anyone can call a public Zambda, so verify that requests come from your provider, for example by checking the request signature with a key stored in your Zambda Secrets, as the Twilio SMS Webhook example does.

Raw mode is intended for callers that need a specific response, such as webhook providers. The SDK's zambda.execute() and zambda.executePublic() methods expect the standard { status, output } structure. When used with a raw mode Zambda, they return the parsed body only if the Zambda responds with JSON (Content-Type: application/json).