Skip to content
Postbox Postbox
· 6 min read guide api

Submitting to Postbox from cURL, Scripts, and Backend Services

Send data to Postbox from cURL, Python, Node.js, Ruby, cron jobs, and CI/CD pipelines. Complete code examples with schema validation and error handling.

Postbox endpoints are plain HTTP. POST JSON to a URL, get a response back. No SDK, no client library, no form builder widget. This makes Postbox a natural fit for scripts, cron jobs, backend services, CI/CD pipelines, and anything else that can make an HTTP request.

This guide covers everything you need to submit data from server-side code, with examples in cURL, Python, Node.js, and Ruby.

The endpoint

Every form in Postbox gets a unique endpoint URL. You’ll find it on the Integration tab in your dashboard, along with auto-generated code snippets for every language.

cURL

The simplest way to test. One line:

curl -X POST <POSTBOX_FORM_URL> \
  -H "Content-Type: application/json" \
  -d '{"name": "Jane", "email": "jane@example.com", "message": "Hello from the terminal"}'

That’s a real submission. It goes through validation, gets stored, triggers any processing you’ve configured — spam filtering, auto-translation, smart replies. Same as a browser form submission, same as an AI agent calling the endpoint. One endpoint, every source.

See the cURL documentation for more options — timeouts, retries, verbose output for debugging.

Python

Using requests:

import requests

url = "<POSTBOX_FORM_URL>"
data = {
    "name": "Jane",
    "email": "jane@example.com",
    "message": "Hello from Python"
}

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

if response.status_code == 201:
    submission = response.json()
    print(f"Submitted: {submission['id']}")
else:
    errors = response.json()
    print(f"Validation failed: {errors}")

This works in a Django view, a Flask route, a FastAPI handler, a standalone script, a Lambda function — anywhere Python runs. Drop it into a cron job to send daily reports. Wire it into a data pipeline to forward processed records.

Node.js

Using the built-in Fetch API (Node 18+):

const url = "<POSTBOX_FORM_URL>";

const res = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "Jane",
    email: "jane@example.com",
    message: "Hello from Node",
  }),
});

if (res.ok) {
  const submission = await res.json();
  console.log(`Submitted: ${submission.id}`);
} else {
  const errors = await res.json();
  console.error("Validation failed:", errors);
}

No dependencies. Works in Express middleware, a Next.js API route, a GitHub Action, a Cloudflare Worker — any JavaScript runtime with fetch.

Ruby

Using net/http from the standard library:

require "net/http"
require "json"
require "uri"

uri = URI("<POSTBOX_FORM_URL>")

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri.path)
request["Content-Type"] = "application/json"
request.body = { name: "Jane", email: "jane@example.com", message: "Hello from Ruby" }.to_json

response = http.request(request)

if response.code == "201"
  submission = JSON.parse(response.body)
  puts "Submitted: #{submission['id']}"
else
  errors = JSON.parse(response.body)
  puts "Validation failed: #{errors}"
end

Works in Rails controllers, Sidekiq workers, Rake tasks, or plain Ruby scripts.

Response format

On success, you get 201 Created with the stored submission:

{
  "id": "abc123",
  "data": { "name": "Jane", "email": "jane@example.com", "message": "Hello" },
  "created_at": "2026-03-09T12:00:00Z"
}

On validation failure, you get 422 Unprocessable Entity with field-level errors:

{
  "email": "invalid email",
  "name": "is required"
}

The error keys map directly to field names. Parse them, log them, retry with corrected data — whatever your script needs to do.

Schema validation

Every Postbox form has a schema — defined types, required fields, validation rules. Read why structured forms matter for the full rationale.

When your script sends data that doesn’t match the schema, Postbox rejects it immediately with the 422 error response shown above. This is intentional. Bad data never gets stored. Your downstream systems never have to deal with malformed submissions. The contract is enforced at the endpoint.

This matters more for scripts and services than it does for browser forms. A human filling out a form gets visual cues — red borders, error messages, placeholder text. A script gets none of that. Schema validation is your safety net. If a field name changes, if a required field is missing, if a type is wrong, you find out at submission time with a clear error — not later when something downstream breaks silently.

Schema versioning

Here’s where it gets interesting for automation. When you update your schema — add a field, change a type, remove a field — Postbox generates a new endpoint URL.

Old URLs keep working. They validate against the schema version they were created with. A cron job holding an old URL doesn’t break when you update the schema. A CI/CD pipeline with a hardcoded endpoint doesn’t fail at 2 AM because someone on your team added a field.

When you’re ready to update your scripts to the new schema, grab the new URL from the Integration tab and deploy it on your schedule. No coordination required. Read more about this in our design philosophy.

Private forms

By default, Postbox endpoints are public — anyone with the URL can submit. For internal services, you can mark a form as private. Private forms require an API key:

curl -X POST <POSTBOX_FORM_URL> \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-api-key" \
  -d '{"event": "deploy", "service": "api", "status": "success"}'

Generate API keys in your account settings. Use them for internal event collection, deployment tracking, monitoring hooks — anything where you don’t want anonymous submissions.

Self-documenting endpoints

Not sure what fields a form expects? Send a GET request to the same URL:

curl <POSTBOX_FORM_URL> \
  -H "Accept: application/json"

Postbox uses content negotiation — when a non-browser client requests JSON, it returns the schema definition: field names, types, which ones are required. Your scripts can introspect the endpoint before submitting. Useful for building generic submission clients or validating data before sending.

CORS and rate limiting

If you’re coming from the HTML form guide, you might wonder about CORS. It doesn’t apply here. CORS is a browser security mechanism. Server-side HTTP requests — cURL, Python, Node, Ruby, anything not running in a browser — bypass it entirely. One less thing to think about.

Rate limiting does apply. Postbox rate-limits submissions per endpoint to prevent abuse. For normal usage — scripts, cron jobs, backend integrations — you’ll never hit the limit. If you’re doing bulk imports or high-volume ingestion, reach out and we’ll work with you.

Use cases

We see scripts and backend services sending to Postbox for:

  • Deployment notifications — CI/CD pipelines posting build status, deploy timestamps, and commit metadata
  • Cron job output — scheduled tasks forwarding daily reports, aggregated metrics, or health check results
  • Webhook forwarding — catching webhooks from third-party services and normalizing them into a consistent schema
  • Internal tooling — admin scripts, data processing pipelines, and automation workflows collecting structured output
  • IoT and edge devices — sensors, edge nodes, and embedded systems reporting data via simple HTTP

The pattern is always the same. POST JSON. Get validation. Get storage. Get processing. One endpoint.

What’s next

Your script is connected. Submissions appear in your dashboard in real time. From here:

Postbox doesn’t care who sent the data — a browser form, a cURL command, a Python script, an AI agent. The endpoint is the same. The validation is the same. The processing is the same. That’s the whole point.