Create and Onboard a New Location
Learn how to add a new Location to your existing Organization and authorize it to receive mail.
Overview
The United States Postal Service requires Stable to store a signed copy of USPS Form 1583 in order to authorize mail reception at your Location . This guide walks you through the complete process of creating a Location
and submitting the required documentation via the Stable API.
You'll learn how to:
- Create a new
Location
- Submit USPS Form 1583 information programmatically
- Upload identification documents securely
- Generate signature URLs for document signing
- Monitor the onboarding progress via webhooks
Prerequisites
Before you begin, you'll need:
- A Stable API key
- Access to the identification documents of the applicant:
- Primary ID (government-issued photo ID)
- Secondary ID (proof of address)
- Verification Photo (selfie)
- Basic understanding of REST APIs and HTTP requests
- A way to receive webhook notifications (recommended)
Keep Your Applicants Documents Ready
Having all required documents prepared in advance will make the onboarding process smoother. For details on acceptable forms of ID, see our Primary ID, Secondary ID, and Verification Photo support articles.
Authentication
All API requests require authentication using your API key in the x-api-key
header:
curl -H "x-api-key: your-api-key" https://api.usestable.com/v1/...
const fetch = require('node-fetch');
const url = 'https://api.usestable.com/v1/locations';
const options = {
headers: {
'x-api-key': 'your-api-key',
'Content-Type': 'application/json',
},
// ...
};
fetch(url, options)
.then(response => console.log('Response:', response.json())
.catch(error => console.error('Error:', error));
import requests
url = 'https://api.usestable.com/v1/...'
headers = {
'x-api-key': 'your-api-key',
'Content-Type': 'application/json',
}
response = requests.get(url, headers=headers)
if response.ok:
print(response.json())
else:
print(f"Error: {response.status_code}, {response.text}")
API Key Security
Keep your API key secure and never expose it in client-side code. All API requests should be made from your backend services.
Process Overview
The Location
onboarding process follows these steps:
- Create a new
Location
in yourOrganization
- Upload USPS Form 1583 information using the
prefill
endpoint - Upload required identification documents:
- Primary ID (government-issued photo ID)
- Secondary ID (proof of address)
- Verification photo (selfie)
- Create a signature packet
- Direct your user to the signature URL and complete form signature
- Monitor onboarding status via webhooks
Each step must be completed in order, and you'll receive webhook notifications as your Location
progresses through the onboarding stages. You can also query the onboarding status via the API .
Step 1: Creating a Location
Start by creating a new Location that will receive mail.
- You'll need to specify a location code from our list of available facilities.
- You should also decide whether to enable
autoScan
. WhenautoScan
is enabled, every piece of mail received at thisLocation
will have its contents opened, scanned, and uploaded, in addition to the outer envelope.
curl -X POST https://api.usestable.com/v1/locations \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"locationCode": "SFO1",
"autoScan": true
}'
const fetch = require('node-fetch');
const url = 'https://api.usestable.com/v1/locations';
const options = {
method: 'POST',
headers: {
'x-api-key': 'your-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
locationCode: 'SFO1',
autoScan: true,
}),
};
fetch(url, options)
.then(response => console.log('Response:', response.json())
.catch(error => console.error('Error:', error));
import requests
url = 'https://api.usestable.com/v1/locations'
headers = {
'x-api-key': 'your-api-key',
'Content-Type': 'application/json',
}
payload = {
'locationCode': 'SFO1',
'autoScan': True,
}
response = requests.post(url, headers=headers, json=payload)
if response.ok:
print(response.json())
else:
print(f"Error: {response.status_code}, {response.text}")
Response
{
"id": "6510b338-57d9-4aac-8e84-fb047ae42e86",
"address": {
"line1": "2261 Market Street",
"line2": "#4962",
"city": "San Francisco",
"state": "CA",
"postalCode": "94114"
},
"type": "cmra",
"onboarding": {
"status": "authorize"
}
}
Location Status
When first created, your
Location
will have an onboarding status ofauthorize
. This indicates it's ready for USPS Form 1583 submission.
Step 2: Submit USPS Form 1583 Information
After creating your Location
, submit the required USPS Form 1583 information using the prefill endpoint . This step pre-populates the form with your user's information.
curl -X POST https://api.usestable.com/v1/locations/6510b338-57d9-4aac-8e84-fb047ae42e86/onboard/prefill \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"submissionType": "business",
"legalBusinessName": "Happy Inc.",
"phone": "4151112222",
"placeOfRegistration": "Delaware",
"businessType": "Technology",
"firstName": "Jane",
"lastName": "Doe",
"email": "[email protected]",
"officerRole": "Founder",
"isCompanyOfficerConfirmed": true,
"primaryIdType": "passport",
"primaryIdNumber": "987654321",
"primaryIdIssuingEntity": "United States",
"primaryIdExpirationDate": "2035-12-31",
"secondaryIdType": "utility_bill",
"secondaryIdAddress": {
"line1": "456 Elm Street",
"line2": "Suite 200",
"city": "Austin",
"state": "Texas",
"postalCode": "73301",
"country": "US"
}
}'
const fetch = require('node-fetch');
const locationId = 'your-location-id';
const url = `https://api.usestable.com/v1/locations/${locationId}/onboard/prefill`;
const body = {
submissionType: 'business',
legalBusinessName: 'Happy Inc.',
phone: '4151112222',
placeOfRegistration: 'Delaware',
businessType: 'Technology',
firstName: 'Jane',
lastName: 'Doe',
email: '[email protected]',
officerRole: 'Founder',
isCompanyOfficerConfirmed: true,
primaryIdType: 'passport',
primaryIdNumber: '987654321',
primaryIdIssuingEntity: 'United States',
primaryIdExpirationDate: '2035-12-31',
secondaryIdType: 'utility_bill',
secondaryIdAddress: {
line1: '456 Elm Street',
line2: 'Suite 200',
city: 'Austin',
state: 'Texas',
country: 'US',
postalCode: '73301',
},
};
const options = {
method: 'POST',
headers: {
'x-api-key': 'your-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
};
fetch(url, options)
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
import requests
location_id = 'your-location-id'
url = f'https://api.usestable.com/v1/locations/{location_id}/onboard/prefill'
body = {
"submissionType": "business",
"legalBusinessName": "Happy Inc.",
"phone": "4151112222",
"placeOfRegistration": "Delaware",
"businessType": "Technology",
"firstName": "Jane",
"lastName": "Doe",
"email": "[email protected]",
"officerRole": "Founder",
"isCompanyOfficerConfirmed": True,
"primaryIdType": "passport",
"primaryIdNumber": "987654321",
"primaryIdIssuingEntity": "United States",
"primaryIdExpirationDate": "2035-12-31",
"secondaryIdType": "utility_bill",
"secondaryIdAddress": {
"line1": "456 Elm Street",
"line2": "Suite 200",
"city": "Austin",
"state": "Texas",
"country": "US",
"postalCode": "73301"
}
}
headers = {
"x-api-key": "your-api-key",
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json=body)
if response.ok:
print("Response:", response.json())
else:
print(f"Error: {response.status_code}, {response.text}")
Response
Note the url
, and expiresAt
fields.
- The
url
is a link to the partially filled USPS 1583 Form. You can use thisurl
to continue and even complete theLocation
onboarding process. - You'll want to check the
expiresAt
field, as it indicates the deadline by which theurl
will remain valid. Either have the user complete the onboarding process before this time is up or request a new link if this one expires.
{
"id": "9f2f1342-630e-48ad-a2e3-bca9f126c56e",
"expiresAt": "2024-11-06T00:00v:00.000Z",
"url": "https://dashboard.usestable.com/location/6510b338-57d9-4aac-8e84-fb047ae42e86/onboard/authorize/business?key=bd1a413ba8709e6257d2b07e"
}
Manual Form Completion URL
The
url
returned in the response is only needed if your end user will be completing some form fields manually. When all data is submitted programmatically through the API, you can ignore thisurl
and proceed directly to document uploads.
Step 3: Upload Required Documents
After submitting the USPS Form 1583 information, you'll need to upload three required documents. For each document, this is a two-step process:
- Get a secure upload URL for the specific document type
- Upload the document using the provided URL
Getting Upload URLs
For each document you want to upload, first request a pre-signed upload URL. This URL allows secure direct upload to our storage system.
curl -X POST https://api.usestable.com/v1/locations/6510b338-57d9-4aac-8e84-fb047ae42e86/onboard/upload-urls \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"type": "primary",
"fileType": "jpg"
}'
const fetch = require('node-fetch');
const locationId = 'your-location-id';
const url = `https://api.usestable.com/v1/locations/${locationId}/onboard/upload-urls`;
const body = {
type: 'primary',
fileType: 'jpg',
};
const options = {
method: 'POST',
headers: {
'x-api-key': 'your-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
};
fetch(url, options)
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
import requests
location_id = 'your-location-id'
url = f'https://api.usestable.com/v1/locations/{location_id}/onboard/upload-urls'
body = {
"type": "primary",
"fileType": "jpg",
}
headers = {
"x-api-key": "your-api-key",
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json=body)
if response.ok:
print("Response:", response.json())
else:
print(f"Error: {response.status_code}, {response.text}")
Upload URL Response
{
"uploadUrl": "..."
}
URL Expiration
Upload URLs expire after 2 minutes. Always request a new URL immediately before uploading each document.
Uploading Documents
Use the pre-signed URL to upload your document. Make sure to set the correct content type header for your file:
curl -X PUT "UPLOAD_URL_FROM_PREVIOUS_RESPONSE" \
-H "Content-Type: image/jpeg" \
--data-binary @/path/to/primary-id.jpg
const fetch = require('node-fetch');
const fs = require('fs');
const uploadUrl = 'UPLOAD_URL_FROM_PREVIOUS_RESPONSE'; // Replace with the URL from the API response
const filePath = '/path/to/primary-id.jpg'; // Local path to the file
const fileData = fs.readFileSync(filePath);
const options = {
method: 'PUT',
headers: {
'Content-Type': 'image/jpeg',
},
body: fileData,
};
fetch(uploadUrl, options)
.then(response => {
if (response.ok) {
console.log('File uploaded successfully');
} else {
console.error('Upload failed:', response.status, response.statusText);
}
})
.catch(error => console.error('Error:', error));
import requests
upload_url = 'UPLOAD_URL_FROM_PREVIOUS_RESPONSE' # Replace with the URL from the API response
file_path = '/path/to/primary-id.jpg' # Local path to the file
file_data = open(file_path, 'rb').read()
headers = {
"Content-Type": "image/jpeg"
}
response = requests.put(upload_url, headers=headers, data=file_data)
if response.ok:
print("File uploaded successfully")
else:
print(f"Upload failed: {response.status_code}, {response.text}")
Supported File Types
You can upload documents in any of these formats:
- Images:
jpg
,jpeg
,png
,heic
- Documents:
pdf
(must not be password protected)
Required Documents
You'll need to repeat the upload process for each of these required documents:
- Primary ID (
type: "primary"
):- A government-issued photo ID like a passport or driver's license
- Must clearly show the applicant's photo and name
- Secondary ID (
type: "secondary"
):- A document proving the applicant's address
- Must show the same address provided in the USPS Form 1583 information
- Verification Photo (
type: "selfie"
):- A clear photo of the applicant
- Should match the photo on the primary ID
Multiple Uploads
If you upload multiple files for the same document type (for example, both a
jpg
and
Successful Upload Response
A successful upload will return an HTTP 200
status code with a response like this:
HTTP/1.1 200 OK
x-amz-request-id: 7B4A0FABBG9ADFCA
Date: Wed, 06 Nov 2024 20:00:00 GMT
ETag: "828ef3fdfa96f00ad9f27c383fc9ac7f"
Content-Length: 0
Unsuccessful Upload Response - Expired URL
<Error>
<Code>AccessDenied</Code>
<Message>Request has expired</Message>
<Expires>2024-12-05T21:23:35Z</Expires>
<ServerTime>2024-12-05T21:24:01Z</ServerTime>
<RequestId>PSH8ANGEQMG1FP0M</RequestId>
<HostId>nhWJL3qKT6tEuqHIF4KdemO3Kuui41PYml2nrXaJS+NAP7JEZjYL/ZdX8GChK02painc8Ac3zKk=</HostId>
</Error>
Failed Uploads
If an upload fails or the pre-signed upload URL expires, request a new upload URL and try again.
Step 4: Creating a Signature Packet
Once you've uploaded all required documents, you'll need to create a signature packet. The signature packet combines your uploaded documents with the USPS Form 1583 information into a single package for electronic signature.
Creating the Packet
Send a POST
request to create the signature packet:
curl -X POST https://api.usestable.com/v1/locations/6510b338-57d9-4aac-8e84-fb047ae42e86/onboard/signature-packet \
-H "x-api-key: your-api-key"
const fetch = require('node-fetch');
const locationId = 'your-location-id';
const url = `https://api.usestable.com/v1/locations/${locationId}/onboard/signature-packet`;
const options = {
method: 'POST',
headers: {
'x-api-key': 'your-api-key',
},
};
fetch(url, options)
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
import requests
location_id = 'your-location-id'
url = f'https://api.usestable.com/v1/locations/{location_id}/onboard/signature-packet'
headers = {
"x-api-key": "your-api-key",
}
response = requests.post(url, headers=headers)
if response.ok:
print("Response:", response.json())
else:
print(f"Error: {response.status_code}, {response.text}")
{
"id": "24f15197-118d-457c-8995-522164cb144d",
"expiresAt": "2025-01-06T00:00:00Z",
"url": "https://dashboard.usestable.com/location/6510b338-57d9-4aac-8e84-fb047ae42e86/onboard/authorize/business?embed=true&key=19F781"
}
The response includes a URL that leads to a white-labeled signing experience you can embed within your application's flow.
Location Status
Creating a Signature Packet will change your
Location
's onboarding-status fromauthorize
tosign
. This indicates that all required documents have been uploaded and theLocation
is ready for signature collection.
Retrieving an Existing Packet
You can retrieve an existing signature packet if your previous url
has expired.
curl -X GET https://api.usestable.com/v1/locations/6510b338-57d9-4aac-8e84-fb047ae42e86/onboard/signature-packet \
-H "x-api-key: your-api-key"
const fetch = require('node-fetch');
const locationId = 'your-location-id';
const url = `https://api.usestable.com/v1/locations/${locationId}/onboard/signature-packet`;
const options = {
method: 'GET',
headers: {
'x-api-key': 'your-api-key',
},
};
fetch(url, options)
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
import requests
location_id = 'your-location-id'
url = f'https://api.usestable.com/v1/locations/{location_id}/onboard/signature-packet'
headers = {
"x-api-key": "your-api-key",
}
response = requests.get(url, headers=headers)
if response.ok:
print("Response:", response.json())
else:
print(f"Error: {response.status_code}, {response.text}")
{
"id": "24f15197-118d-457c-8995-522164cb144d",
"expiresAt": "2025-01-06T00:00:00Z",
"url": "https://dashboard.usestable.com/location/6510b338-57d9-4aac-8e84-fb047ae42e86/onboard/authorize/business?embed=true&key=19F781"
}
Customizing the Signature Interface
The signature URL returned by the API includes embedded=true
by default:
{
"url": "https://dashboard.usestable.com/location/{id}/onboard/authorize/business?key={key}&embedded=true"
}
There are two available display modes:
embedded=true
(default from API)
embedded=true
(default from API)- Removes all Stable styling and chrome
- Hides navigation elements, footer, and branding
- Provides a minimal interface ideal for embedding
Example Screenshot
branded=false
(optional alternative)
branded=false
(optional alternative)- Only hides the Stable logo
- Maintains standard navigation and UI elements
- Keeps Stable's styling and layout
Example Screenshot
Combining Multiple Modes
Adding
branded=false
to a URL that already hasembedded=true
has no effect, asembedded=true
already provides a more comprehensive white-label experience.
Restarting the Process
If you need to modify any submitted information after creating a signature packet, you can delete the existing packet and start fresh:
curl -X DELETE https://api.usestable.com/v1/locations/6510b338-57d9-4aac-8e84-fb047ae42e86/onboard/signature-packet \
-H "x-api-key: your-api-key"
const fetch = require('node-fetch');
const locationId = 'your-location-id';
const url = `https://api.usestable.com/v1/locations/${locationId}/onboard/signature-packet`;
const options = {
method: 'DELETE',
headers: {
'x-api-key': 'your-api-key',
},
};
fetch(url, options)
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
import requests
location_id = 'your-location-id'
url = f'https://api.usestable.com/v1/locations/{location_id}/onboard/signature-packet'
headers = {
"x-api-key": "your-api-key",
}
response = requests.delete(url, headers=headers)
if response.ok:
print("Response:", response.json())
else:
print(f"Error: {response.status_code}, {response.text}")
Deleting the signature packet allows you to:
- Upload new versions of documents
- Update USPS Form 1583 information
- Generate a new signature URL
The response confirms the successful deletion:
{
"success": true
}
Location Status
Deleting a
Location
's Signature Packet will change that location's onboarding status fromsign
, back toauthorize
. This indicates the location requires a new signature packet before signatures can be collected.
Step 5: Monitoring Onboarding Status
After your user signs the documents, our team performs verifications on all submitted information. You can track the progress of this verification through Webhook notifications and direct API checks.
Webhook Notifications
Register a webhook endpoint to receive real-time updates about the onboarding process. You'll receive notifications for important events including:
{
"data": {
"address": {
"city": "San Francisco",
"line1": "123 Main St",
"line2": "#4962",
"postalCode": "94105",
"state": "CA"
},
"id": "70a3d702-bf1d-4153-8eb2-d3d889aff7f0",
"metadata": null,
"onboarding": {
"status": "verify"
},
"type": "cmra"
},
"eventType": "location.onboarding.updated"
}
Onboarding Status Values
Your Location
will progress through several status values during the onboarding process:
{
"onboarding": {
"status": "verify" // Current onboarding status
}
}
The status
values indicate where your Location
is in the onboarding process:
authorize
: Initial state, awaiting document submission and signaturesign
: Documents uploaded, awaiting signature completionverify
: Documents signed, undergoing manual verificationcomplete
: Onboarding completed successfully
Document Verification
During the verification phase, our team reviews all submitted identification documents to ensure they meet USPS requirements. If any documents are rejected (for example, due to poor image quality, expired IDs, or mismatched information), the associated signature packet will be deleted, and you'll receive a webhook notification with the location's onboarding status changing from verify
back to authorize
. Additionally, all admins in your organization will be notified via email. When this happens, you should:
- Upload new documents that address the rejection reason
- Create a new signature packet
- Have the applicant complete the signature process again
Checking Status Manually
You can check your Location
's current status at any time:
curl https://api.usestable.com/v1/locations/6510b338-57d9-4aac-8e84-fb047ae42e86 \
-H "x-api-key: your-api-key"
const fetch = require('node-fetch');
const locationId = 'your-location-id';
const url = `https://api.usestable.com/v1/locations/${locationId}`;
const options = {
method: 'GET',
headers: {
'x-api-key': 'your-api-key',
},
};
fetch(url, options)
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
import requests
location_id = 'your-location-id'
url = f'https://api.usestable.com/v1/locations/{location_id}'
headers = {
"x-api-key": "your-api-key",
}
response = requests.get(url, headers=headers)
if response.ok:
print("Response:", response.json())
else:
print(f"Error: {response.status_code}, {response.text}")
Response
{
"id": "6510b338-57d9-4aac-8e84-fb047ae42e86",
"address": {
"line1": "2261 Market Street",
"line2": "#4962",
"city": "San Francisco",
"state": "CA",
"postalCode": "94114"
},
"type": "cmra",
"onboarding": {
"status": "complete"
},
"metadata": null
}
Error Handling
Here's a comprehensive guide to errors you might encounter during the onboarding process and how to handle them.
Location Errors
Location Not Found
{
"status": 404,
"message": "This location could not be found"
}
Solution: Verify the Location
ID is correct.
Invalid Location Code
{
"status": 400,
"message": "Validation error(s) when parsing body: 'locationCode must be one of the following values: SFO1, AUS1, ILG1, MIA1, LGA1, CYS1, MDW1, BOS1, SEA1. See https://docs.usestable.com/docs/location-codes for more information.'"
}
Solution: Use one of the supported location codes listed in the documentation.
Document Upload Errors
Pre-signed URL Expired
<Error>
<Code>AccessDenied</Code>
<Message>Request has expired</Message>
<Expires>2024-12-05T21:23:35Z</Expires>
<ServerTime>2024-12-05T21:24:01Z</ServerTime>
<RequestId>PSH8ANGEQMG1FP0M</RequestId>
<HostId>nhWJL3qKT6tEuqHIF4KdemO3Kuui41PYml2nrXaJS+NAP7JEZjYL/ZdX8GChK02painc8Ac3zKk=</HostId>
</Error>
Solution: Request a new upload URL and try again.
Invalid File Type
{
"status": 400,
"message": "Validation error(s) when parsing body: 'fileType must be one of the following values: jpg, jpeg, png, heic, pdf'"
}
Solution: Convert the file to a supported format.
Missing Documents
{
"status": 400,
"message": "Missing document uploads for: Secondary ID, Selfie."
}
Solution: Ensure all required documents are uploaded before creating the signature packet.
USPS Form 1583 Submission Errors
Invalid Submission Data
{
"status": 400,
"message": "primaryIdType is a required field"
}
Solution: Review the submission data and ensure all required fields are provided.
Existing Signature Packet
{
"status": 409,
"message": "USPS 1583 signature packet already exists."
}
Solution: Delete the existing signature packet if you need to make changes.
Best Practices
To ensure a smooth onboarding process:
- Document Quality
- Ensure uploaded documents are clear and legible
- Verify that addresses on secondary ID match the submitted information exactly
- Make sure the verification photo clearly shows the applicant's face
- Error Handling
- Implement retry logic for upload failures
- Store the Location ID for status checking
- Set up webhook handling for real-time status updates
- Status Monitoring
- Implement webhook handlers for all onboarding status changes
- Have a fallback mechanism to poll the status endpoint if needed
- Keep your users informed of the verification progress
Updated 9 days ago