External Effects
Server-only effect claims, installation custody and customer links.
External Effects
Use this integration only in the stated caller context. Configure credentials first; the SDK README owns the package surface.
These operations require an active, admitted production-purpose workspace and their explicit server-key bindings. Only workspace-only or current-ready Live environments are admitted. An environmentKind: 'sandbox' produces the server policy's capture decision, which does not authorize these operations. Sandbox environments and test workspaces do not admit executable external-effect claims, installation custody or external Customer-link operations. Confirm availability with the workspace operator; a published SDK method is not a grant to run it in a test environment.
External-effect claims (server only)
createServerClient().externalEffectClaims records a workspace-owned operation such
as a coupon issue or an external customer-group change. An administrator must
register the integration and bind the active sk01_ key before use. The
server credential resolves the workspace; request bodies have no tenantId.
The raw claim endpoint accepts the bound server key alone; a matching publishable
key may accompany it. The SDK's createServerClient still takes both keys.
The claim is bookkeeping, not proof of a provider result or an executor.
import { createServerClient } from '@01.software/sdk/server'
type Server = ReturnType<typeof createServerClient>
export async function armExternalEffect(
server: Server,
input: Parameters<Server['externalEffectClaims']['claim']>[0],
) {
const claim = await server.externalEffectClaims.claim(input)
if (!claim.leaseGranted || !claim.leaseToken)
return { claim, armed: false as const }
await server.externalEffectClaims.arm({
integrationId: input.integrationId,
claimId: claim.id,
leaseToken: claim.leaseToken,
leaseGeneration: claim.leaseGeneration,
})
// Only this acknowledged branch may dispatch the original safe provider operation.
return { claim, armed: true as const }
}Hash logical keys, request fingerprints, and recovery locators before sending
them. Keep provider credentials, coupon codes, customer data, and full provider
requests and responses in your own service. If arm or provider response is lost,
read the claim and consult the provider using the original safe replay key or
authoritative lookup; an armed_unknown claim never grants a fresh dispatch.
report requires a redacted evidence SHA-256. A no_effect report also needs
the claim's original locator or a provider operation SHA-256 to identify the
remote work checked; without either, leave it unknown for manual reconciliation.
The report remains workspace-supplied evidence, not provider-verified truth.
recovery records a lookup or
same-operation safe replay observation and requires the original
requestFingerprintSha256; it does not grant a new lease.
Payment, refund, Membership billing, and outbound webhook delivery have their
own domain ledgers and cannot use these claims. Monthly admission limits apply
to new logical claims; reading and reporting an existing claim remain available.
An identified database outage returns HTTP 503 with authority_unavailable;
unexpected failures return HTTP 500 with internal_error. Use
getExternalEffectClaimErrorReason(error) on an SDK error to read a known
claim reason. Operators can reconcile old unknown evidence for an exact retired
workspace in Console without reopening claim or arm permission.
Installation credentials (server only)
createServerClient().installationCredentials stores bounded ciphertext for
one registered non-payment OAuth installation. A workspace administrator registers
the installation and binds an active server key specifically to it. An ordinary
sk01_ key or external-effect claim grant cannot read the ciphertext.
import { createServerClient } from '@01.software/sdk/server'
type Server = ReturnType<typeof createServerClient>
export async function replaceInstallationCredential(
server: Server,
command: Parameters<Server['installationCredentials']['replace']>[0],
) {
const replacement = await server.installationCredentials.replace(command)
const readback = await server.installationCredentials.get({
installationId: command.installationId,
})
return { replacement, readback }
}Before reviewing a replacement, read server.installationCredentials.get({ installationId }) and retain a command with that expectedVersion and your new ciphertext. Pass that exact command to this function; do not silently refresh the version after a failure. Encrypt and decrypt only in your workspace server. OAuth callbacks, refresh,
provider calls, and provider-side revocation remain yours. A failed replacement
may have succeeded before its response was lost: read back before retrying.
dispose clears the platform copy; it cannot revoke a value your server
already read. After a database restore, installations require re-authorization
before readback resumes.
External Customer links
Use server.externalCustomerLinks in trusted server code to connect a platform
Customer with an external Customer ID. An active integration registration
derives namespace and environment. Your app supplies a normalized stable
accountScope to reserve identity. Namespace-bound keys authorize all accounts
in that namespace; account scope is not an account ACL. Customer read/write scope
is required; browser clients do not expose this API.
import { createServerClient } from '@01.software/sdk/server'
type Server = ReturnType<typeof createServerClient>
export async function connectCustomer(
server: Server,
input: Parameters<Server['externalCustomerLinks']['connect']>[0],
) {
const link = await server.externalCustomerLinks.connect(input)
return server.externalCustomerLinks.get({
integrationId: input.integrationId,
customerId: input.customerId,
linkId: link.id,
})
}
export async function captureCustomerPreviousState(
server: Server,
command: Parameters<
Server['externalCustomerLinks']['capturePreviousState']
>[0],
) {
const receipt =
await server.externalCustomerLinks.capturePreviousState(command)
const readback = await server.externalCustomerLinks.get({
integrationId: command.integrationId,
customerId: command.customerId,
linkId: command.linkId,
})
return { receipt, readback }
}Generate and persist input.requestId before connecting. After reviewing the returned link, retain a separate capture command containing integrationId, customerId, linkId, its original expectedVersion, a new request UUID and bounded previousState (for example { tier: 'silver' }). Pass that exact command to captureCustomerPreviousState; never rebuild its version from a connect replay. Connect replay returns the link's current state; a capture replay uses its original command/receipt.
get reads one link; list accepts optional customerId, accountScope,
externalCustomerId, and state filters (external ID requires account scope)
with limit (default 25, maximum 100) and afterId. Pass
nextCursor as the next afterId. unlink, capturePreviousState, and
clearPreviousState require a request UUID and expectedVersion. After an
interrupted response, retry the exact request with its original version.
Previous state is bounded to 16 flat string/number/boolean/null fields, 512
characters per string, and 2048 UTF-8 JSON bytes. The platform performs no
external provider I/O. Before restoring external state, check the current
active owner using list({ integrationId, accountScope, externalCustomerId, state: 'active' })
and evaluate whether restoration is still appropriate. Privacy erasure returns
state erased with null externalCustomerId and previousState.