Skip to content

Custom Integrations

Any language that can make an HTTPS request can create leads and self check-ins. Create an integration with the provider type Website or REST API (both are intake-only), copy the key, and pick one of the two request styles below.

Send BytePhase’s field names directly at the top level of the body. Simplest for your own code.

POST /api/integrations/leads
POST /api/integrations/self-checkin
{
"name": "Jordan Ellis",
"mobile_number": "4155550142",
"mobile_country_code": "+1",
"email": "jordan.ellis@example.com",
"comment": "Screen flickers after boot"
}

Optional on both: business_id (integer; defaults to the workspace’s default business) and form_id (to apply a form’s field map and duplicate rule).

Wrap the fields in data and say which form and destination they belong to. This is what the WordPress plugin sends, and the right choice when the field names come from a form you do not control and are mapped in BytePhase.

POST /api/integrations/submit
{
"form_id": "contact-us",
"destination": "lead",
"data": {
"Your Name": "Jordan Ellis",
"Phone": "4155550142",
"Message": "Screen flickers after boot"
}
}
Field Required Meaning
data Yes The submitted fields — BytePhase’s names, or your own names if a field map exists for the form
form_id No Which of the integration’s forms this is; picks the field map and destination. Up to 191 characters.
destination No lead or self_checkin. Overrides the form’s destination. May also be given as ?destination= in the URL.
business_id No Integer; defaults to the workspace’s default business

If neither destination nor a matching form can determine where the submission should go, it is parked as 202 received until the form is set up in BytePhase — never rejected. See Submissions & Troubleshooting.

The same envelope is also accepted at POST /api/integrations/ingest/canonical, and a plain form-encoded or multipart POST (an HTML <form> posting straight to BytePhase) is accepted at POST /api/integrations/ingest/form, where every field except form_id and destination is treated as data.

All three endpoints return the same shape:

HTTP status data
201 completed The created lead or self check-in
200 duplicate The existing record (idempotent replay, or the duplicate rule matched)
202 received null — parked until the form is mapped
{
"status": "completed",
"request_id": "01K4R4Z2N7Q9X0F3M6C8V1B5D2",
"data": { "id": 10421, "object": "lead", "…": "" }
}

Always send an Idempotency-Key header (a UUID per submission) so a retry is safe. See Idempotency.

Field Rules
name Required. Up to 60 characters
mobile_number Required unless email is sent. Up to 20 characters, digits as entered
email Required unless mobile_number is sent. Valid email, up to 255
mobile_country_code Dialling code, e.g. "+1" or "+44". Up to 10
phone_number Landline or alternate number. Up to 20
contact_person_name For business customers. Up to 60
source Where the lead came from, e.g. "Website". Up to 100. Matched to the workspace’s lead sources by name
next_follow_up Date or date-time
comment Free text
device_type, device_brand, device_model Names, up to 100 each. Matched to the workspace’s catalogue by name; a brand or model that is not in the catalogue is kept as typed
address Object: address_line (200), city (100), state (100), zip_code (20)
custom_fields Object of "label": "value" pairs, values up to 1000 characters
Field Rules
name Required. Up to 255
mobile_number Required unless email is sent. Up to 20
email Required unless mobile_number is sent. Up to 255
mobile_country_code Up to 10
device_type, device_brand, device_model Names, up to 255 each; unknown brands/models kept as typed
serial_number, serial_number_2 Up to 255 / 191
device_password Up to 191. Stored for the technician; never returned by the API
accessories Array of strings, or a comma-separated string
comment Free text
is_recovery Boolean — data recovery job
is_pickup_booked Boolean — customer wants pickup
scheduled_on Date or date-time the customer plans to come in
address Object: address_line, city, state (255 each), zip_code (20)
custom_fields Object of "label": "value" pairs

Exact response field lists for both records are in the reference.

Any top-level field that is not in the tables above is not rejected. Scalar values are kept on the record as custom fields (up to 20 of them, each trimmed to 1000 characters), so a “How did you hear about us?” or “Preferred time” question your form asks is preserved without any configuration. Send a field that is the wrong type or too long and you get 422 validation_failed naming it.

If the workspace has defined its own custom fields for leads or self check-ins, you can fetch their definitions to render them on your form: GET /api/integrations/custom-fields lists the form types that have fields, and GET /api/integrations/custom-fields?form_type=… returns that type’s field names, types, placeholders and options. See the reference for the response.

Replace harbor-repair and the key. Every sample sends an Idempotency-Key and treats completed, duplicate and received as success.

Terminal window
curl -X POST "https://harbor-repair.api.bytephase.com/api/integrations/leads" \
-H "Authorization: Bearer bp_your_api_key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Jordan Ellis",
"mobile_number": "4155550142",
"mobile_country_code": "+1",
"email": "jordan.ellis@example.com",
"device_type": "Laptop",
"device_brand": "Dell",
"comment": "Screen flickers after boot",
"source": "Website"
}'
import { randomUUID } from 'node:crypto';
const BASE_URL = 'https://harbor-repair.api.bytephase.com/api';
const API_KEY = process.env.BYTEPHASE_API_KEY;
export async function createLead(lead) {
const response = await fetch(`${BASE_URL}/integrations/leads`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify(lead),
});
const body = await response.json();
if (!response.ok) {
// body.error.code is stable; body.error.request_id is what support needs.
throw new Error(`${body.error.code} (${body.error.request_id}): ${body.error.message}`);
}
return body; // { status: 'completed' | 'duplicate' | 'received', request_id, data }
}
await createLead({
name: 'Jordan Ellis',
mobile_number: '4155550142',
mobile_country_code: '+1',
email: 'jordan.ellis@example.com',
comment: 'Screen flickers after boot',
source: 'Website',
});
<?php
$baseUrl = 'https://harbor-repair.api.bytephase.com/api';
$apiKey = getenv('BYTEPHASE_API_KEY');
$lead = [
'name' => 'Jordan Ellis',
'mobile_number' => '4155550142',
'mobile_country_code' => '+1',
'email' => 'jordan.ellis@example.com',
'comment' => 'Screen flickers after boot',
'source' => 'Website',
];
$idempotencyKey = bin2hex(random_bytes(16));
$ch = curl_init("{$baseUrl}/integrations/leads");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$apiKey}",
'Content-Type: application/json',
'Accept: application/json',
"Idempotency-Key: {$idempotencyKey}",
],
CURLOPT_POSTFIELDS => json_encode($lead),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$body = json_decode($raw, true);
if ($status >= 400) {
throw new RuntimeException(sprintf(
'%s (%s): %s',
$body['error']['code'],
$body['error']['request_id'],
$body['error']['message'],
));
}
// $body['status'] is completed, duplicate or received; $body['data'] is the lead (or null when received).
import os
import uuid
import requests
BASE_URL = "https://harbor-repair.api.bytephase.com/api"
API_KEY = os.environ["BYTEPHASE_API_KEY"]
def create_lead(lead: dict) -> dict:
response = requests.post(
f"{BASE_URL}/integrations/leads",
json=lead,
headers={
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
timeout=15,
)
body = response.json()
if not response.ok:
error = body["error"]
raise RuntimeError(f"{error['code']} ({error['request_id']}): {error['message']}")
return body # {"status": "completed" | "duplicate" | "received", "request_id": ..., "data": ...}
create_lead(
{
"name": "Jordan Ellis",
"mobile_number": "4155550142",
"mobile_country_code": "+1",
"email": "jordan.ellis@example.com",
"comment": "Screen flickers after boot",
"source": "Website",
}
)
Terminal window
curl -X POST "https://harbor-repair.api.bytephase.com/api/integrations/self-checkin" \
-H "Authorization: Bearer bp_your_api_key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Noah Fischer",
"mobile_number": "30901820",
"mobile_country_code": "+49",
"device_type": "Mobile",
"device_brand": "Samsung",
"device_model": "Galaxy S24",
"serial_number": "R5CW1234567",
"accessories": ["Charger", "Case"],
"comment": "Cracked screen, touch still works",
"is_pickup_booked": true,
"scheduled_on": "2026-09-12"
}'
  • Key created with the Website or REST API type — never Zapier. Why
  • Key stored server-side only, never in browser JavaScript or a mobile app binary
  • Idempotency-Key on every write, reused on retry
  • Retry only 429, 423, 5xx and network errors, with backoff. Rate Limits
  • completed, duplicate and received all treated as success for the visitor
  • request_id logged on your side for every request
  • Tested against a trial workspace first. Environments