Skip to content

API keys and webhooks

API keys and webhooks connect Outpost to the other tools your organization runs, such as an accounting system, a warehouse system, or your own scripts. An API key lets a tool call the Outpost API with the permissions of one role. A webhook sends Outpost events, such as a received purchase order or a deployed asset, to a tool as they happen. This page covers creating and managing both, verifying webhook signatures, and the events you can subscribe to.

  • Managing API keys and webhooks needs integrations:admin, which among the built-in roles only the owner holds.
  • Creating an API key needs a second factor on your account: a passkey or an authenticator app. See Secure your console account.
  • A webhook receiver must be reachable from the internet over HTTPS on port 443, with a valid certificate.
  1. Select Settings > Integrations > API keys, then Create key.
  2. Enter a Name that says what uses the key, such as Accounting sync.
  3. Choose the Role the key acts as. The key can do exactly what this role can. Any role except Owner can be chosen.
  4. Optionally, choose when it Expires. The key stops working at the end of that day. Leave it empty for no expiry.
  5. Optionally, enter Allowed IP addresses, one address or CIDR range per line, such as 203.0.113.0/24, up to 50. Leave it empty to allow any address.
  6. Select Create key, and confirm it is you with a second factor.
  7. Copy the key from Your new API key. It is shown once.

Outpost keeps only a fingerprint of the key, so a lost key cannot be shown again. Revoke it and create another.

Send the key in the Authorization header of each request:

Authorization: Bearer <api-key>

Replace <api-key> with the key, which starts opk_. A key works on the console address it was created on.

ResponseCause
401The key is unknown, revoked, or expired.
403The key’s role lacks the permission, the request came from outside Allowed IP addresses, or the action needs a second factor, which a key can never give.
429More than 600 requests in a minute from the key. Wait for the time in Retry-After.

A key runs as a service account that holds the key’s role. The account never signs in, does not appear in Settings > Team, and does not count toward console users. The audit log records the key’s actions as Integration key: followed by its name.

Creating API keys and turning on live ordering for a distributor need a second factor, so a key can never do either.

Any role except Owner can be given to a key. One role is made for them:

RoleHolds
Integration: salesReading and writing the sales pipeline, and reading purchasing, stock, and assets. For quoting and sales tools.

To give a key exactly the permissions a tool needs, create a role for it. See Manage roles.

Settings > Integrations > API keys lists each key’s Name, Role, Expires, Last used, Created by, and Status: Active, Revoked, or Expired.

  • Edit changes the name, expiry, and allowed addresses. A key’s role cannot change. Create a new key instead.
  • Revoke stops the key at once. Anything still using it is refused from the next request.
  1. Select Settings > Integrations > Webhooks, then Add webhook.
  2. Enter a Name, and the URL Outpost posts to. It must start https://, and use the standard port.
  3. Under Events, select the events to send, or Select all.
  4. Leave Enabled selected, and select Add webhook.
  5. Copy the Signing secret. It is shown once.
  6. Open the webhook and select Send test. A ping event is delivered, and appears under Deliveries.

Outpost sends each event as a POST with a JSON body:

{
"id": "3f1c2b9a-6d4e-4f0a-9c1b-2a7e5d8c4b10",
"event": "purchase_order.received",
"occurredAt": "2026-11-05T14:02:11Z",
"data": {
"purchaseOrderId": "8b0e7c1d-2f3a-4b5c-9d6e-7f8a9b0c1d2e",
"poNumber": "PO-1001",
"status": "received",
"fullyReceived": true
}
}

Money is in whole cents, and quantities are decimal strings, such as "2.0000". Payloads carry IDs, numbers, states, and business names. They never carry a person’s name, email address, or postal address.

Each delivery has these headers:

HeaderValue
X-Outpost-Signaturet=<unix-seconds>,v1=<signature>
X-Outpost-EventThe event name, such as purchase_order.received.
X-Outpost-Event-IdThe event’s ID. It is the same on every retry and redelivery of one event, so use it to ignore repeats.
X-Outpost-DeliveryThe ID of this delivery.

Check every delivery before you act on it:

  1. Read t and every v1 value from X-Outpost-Signature. During a secret rotation there may be two v1 values.
  2. Join the t value, a full stop, and the raw request body, byte for byte: <t>.<body>.
  3. Compute the HMAC-SHA256 of that string with the webhook’s whole signing secret, whsec_ prefix included, as lowercase hex.
  4. Accept the delivery when it equals any v1 value, compared in constant time.
  5. Refuse it when t is more than 5 minutes from your clock.

In Python:

import hashlib
import hmac
import time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
"""Check an X-Outpost-Signature header against the raw request body."""
fields = [part.strip().partition("=") for part in header.split(",")]
timestamps = [value for key, _, value in fields if key == "t"]
signatures = [value for key, _, value in fields if key == "v1"]
if len(timestamps) != 1 or not signatures or not timestamps[0].isdigit():
return False
t = timestamps[0]
if abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)

Pass the body exactly as it arrived. Parsing and re-serializing the JSON first changes the bytes, and the signature no longer matches.

RuleValue
SuccessAny 2xx answer within 10 seconds.
RedirectsNot followed. A 3xx answer is a failure.
AddressesPublic addresses only. Private, loopback, and link-local addresses are refused.
RetriesAfter 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, and 24 hours: 8 attempts over about 45 hours.
After the last retryThe delivery shows Gave up.
Kept for30 days.

While deliveries are failing, the webhook shows Failing since and the time of the first failure. A successful delivery clears it.

Open a webhook to see its Deliveries, with the Event, State (Waiting, Sending, Delivered, or Gave up), Attempts, Last status, Last error, Next attempt, and Delivered time. Filter by state or event.

  • Redeliver sends a delivery again, from the start of the retry schedule, with the same body and event ID.
  • Rotate secret makes a new signing secret, shown once. Update your receiver at the same time.
  • Edit changes the name, URL, events, and Enabled. Delete removes the webhook.
EventSent whendata
purchase_order.createdA purchase order is created.purchaseOrderId, poNumber, supplierId, supplierName, status, totalCents
purchase_order.approvedA purchase order is approved.As above
purchase_order.submittedA purchase order is submitted to a distributor.As above
purchase_order.acceptedThe distributor accepts a submitted order.As above, plus distributorOrderId
purchase_order.receivedGoods are received against it.purchaseOrderId, poNumber, status, fullyReceived
purchase_order.cancelledA purchase order is canceled.As for created
receipt.posted, receipt.reversedA receipt is posted or reversed.receiptId, purchaseOrderId, poNumber, lines with stockItemId, sku, qty, serialCount
stock.lowStock falls to its reorder point.stockItemId, sku, locationId, locationName, onHand, reorderPoint
stock.adjustedStock is adjusted.stockItemId, sku, locationId, locationName, qtyDelta, reason, onHand
asset.created, asset.state_changedAn asset is added, or changes state.assetId, clientId, serialNumber, manufacturer, model, state, and previousState on a change
asset.assignedAn asset is linked to an endpoint.assetId, clientId, serialNumber, endpointAgentId
asset.checked_out, asset.checked_inEquipment is checked out or in.assetId, checkoutId, serialNumber, state. The person is never named.
asset.warranty_expiringAn asset’s warranty is ending. Sent once per asset and end date.assetId, clientId, serialNumber, manufacturer, warrantyEndsOn
supplier_bill.approvedA supplier bill is approved.supplierBillId, billNumber, supplierId, supplierName, totalCents, purchaseOrderIds
subscription.changedA distributor subscription changes.subscriptionId, connectorId, vendor, clientId, sku, productName, quantity, previousQuantity, status
subscription.cancel_deadlineA subscription’s NCE cancel deadline is near.subscriptionId, clientId, sku, productName, quantity, cancelDeadlineAt
sales_order.created, .fulfilled, .cancelled, .closedMSP plan. A sales order is created, delivered (once per delivery), canceled, or closed.salesOrderId, soNumber, clientId, clientName, status, totalCents
opportunity.stage_changed, .won, .lostMSP plan. A deal moves stage, is won, or is lost.opportunityId, name, clientId, prospectId, stageId, stageName, valueCents. Company names only.
pingYou select Send test.endpointId

Events about assets are sent for your own register. On the MSP plan, that is your practice’s register, not each client’s. Fields may be added to data over time, and fields listed here are not removed.

  • Send test shows a Delivered ping under Deliveries, and your receiver’s signature check passes.
  • A request with the new API key answers 200, and the key’s Last used shows the time.

Content is licensed CC BY 4.0. Code samples are MIT. Outpost and the Outpost mark are trademarks of Outpost Business Solutions, PBC and are not covered by either license.