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:

📘

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:

  1. Create a new Location in your Organization
  2. Upload USPS Form 1583 information using the prefill endpoint
  3. Upload required identification documents:
    1. Primary ID (government-issued photo ID)
    2. Secondary ID (proof of address)
    3. Verification photo (selfie)
  4. Create a signature packet
  5. Direct your user to the signature URL and complete form signature
  6. 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. When autoScan is enabled, every piece of mail received at this Location 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 of authorize. 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 this url to continue and even complete the Location onboarding process.
  • You'll want to check the expiresAt field, as it indicates the deadline by which the url 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 this url 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 pdf version of a primary ID), we'll use the most recently uploaded version when creating the signature packet.

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 from authorize to sign. This indicates that all required documents have been uploaded and the Location 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)

  • Removes all Stable styling and chrome
  • Hides navigation elements, footer, and branding
  • Provides a minimal interface ideal for embedding
Example Screenshot
Signature Page when `embedded=true`

Signature Page when embedded=true

branded=false (optional alternative)

  • Only hides the Stable logo
  • Maintains standard navigation and UI elements
  • Keeps Stable's styling and layout
Example Screenshot
Signature Page when `branded=false`

Signature Page when branded=false

📘

Combining Multiple Modes

Adding branded=false to a URL that already has embedded=true has no effect, as embedded=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 from sign, back to authorize. 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 signature
  • sign: Documents uploaded, awaiting signature completion
  • verify: Documents signed, undergoing manual verification
  • complete: 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:

  1. Upload new documents that address the rejection reason
  2. Create a new signature packet
  3. 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