Authentication
Only the partner backend calls /api/external/*. Requests are authenticated
with canonical auth v2: an HMAC-SHA256 signature over a canonical string built
from the request. The scheme is fixed by server-side partner configuration and
cannot be selected per request.
Required headers
| Header | Value |
|---|---|
X-Partner-Id | Your SWEEP_PARTNER_ID |
X-Timestamp | Unix epoch milliseconds, decimal; max clock skew ±5 minutes |
X-Sweep-Auth-Version | Literal 2, mandatory on every request |
X-Nonce | Unique per request, 16–128 chars of A-Za-z0-9._~-; never reused, including on retries |
X-Signature | Lowercase hex HMAC-SHA256 of the signing string |
Idempotency-Key | POST only, 1–255 chars |
Content-Type | application/json (POST only) |
Signing string
Sign this exact LF-delimited string, no trailing newline:
SWEEP-EXTERNAL-REQUEST
auth-version:2
api-version:1
partner-id:<partner id>
timestamp:<epoch milliseconds>
nonce:<unique request nonce>
method:<uppercase HTTP method>
path:<normalized absolute pathname>
idempotency-key:<key, or empty for GET>
body-sha256:<SHA-256 hex of the exact raw request bytes>import crypto from 'node:crypto';
const sha256 = (v) => crypto.createHash('sha256').update(v).digest('hex');
const hmac = (secret, v) =>
crypto.createHmac('sha256', secret).update(v).digest('hex');
export function signSweepRequest(secret, r) {
const signingString = [
'SWEEP-EXTERNAL-REQUEST',
'auth-version:2',
'api-version:1',
`partner-id:${r.partnerId}`,
`timestamp:${r.timestamp}`,
`nonce:${r.nonce}`,
`method:${r.method.toUpperCase()}`,
`path:${r.path}`,
`idempotency-key:${r.idempotencyKey || ''}`,
`body-sha256:${sha256(r.rawBody || '')}`,
].join('\n');
return hmac(secret, signingString);
}Rules:
- Serialize a POST body exactly once. Hash, sign and transmit the same bytes.
pathis the full deployed pathname (/api/external/...) with no query string, fragment, dot-segments or encoded separators.- GET requests have no body, no
Idempotency-Keyheader, sign an empty idempotency value and use the SHA-256 of zero bytes. - Duplicated protected headers, non-identity
Content-Encoding, a GET body or anIdempotency-Keyon GET are rejected.
The complete client is in Reference code.
Idempotency
Every mutating endpoint requires an Idempotency-Key. Persist the key and the
exact body before transmission. On timeout or an ambiguous response, retry the
same bytes with the same key. Use a fresh key only for a new operation or
genuinely additive evidence. Never reuse a key for a different body or
resource.
GET /ping
Auth and connectivity probe. Use it to validate credentials and the signature implementation.
Request:
GET /api/external/ping
x-partner-id: partner-example
x-timestamp: 1893456000000
x-sweep-auth-version: 2
x-nonce: req-00000000-0000-4000-8000-000000000001
x-signature: <lowercase HMAC-SHA256 hex>Response 200:
{
"apiVersion": "1",
"ok": true,
"partnerId": "partner-example"
}import { SweepExternalClient } from 'sweep-external-sdk/server';
const client = new SweepExternalClient({
sweepApiUrl: process.env.SWEEP_API_URL,
partnerId: process.env.SWEEP_PARTNER_ID,
secret: process.env.SWEEP_PARTNER_SECRET,
});
const result = await client.request('GET', '/api/external/ping');
// result.response => { apiVersion: '1', ok: true, partnerId: '...' }Troubleshooting
| Response | Cause and fix |
|---|---|
401 external_auth_signature_invalid | Signed bytes differ from transmitted bytes. Serialize once; sign the full deployed pathname; a single trailing slash is stripped before verification |
401 external_auth_request_invalid | URL carries a query string or another rejected path form |
401 external_auth_timestamp_invalid / external_auth_timestamp_expired | X-Timestamp is not decimal epoch milliseconds, or clock skew exceeds five minutes |
401 external_auth_nonce_invalid | Nonce outside A-Za-z0-9._~- or not 16–128 chars |
409 external_auth_nonce_conflict | Nonce reused; generate a fresh nonce for every request, including idempotent retries |
401 external_auth_headers_required | X-Sweep-Auth-Version or X-Nonce missing |
401 external_auth_version_mismatch | Header disagrees with server-side partner configuration; the version cannot be chosen per request. If correctly signed v2 requests fail with this, the partner record needs a Sweep-side fix — contact support, do not rewrite the signer |