Signing Baskets & Drafts
Payment Type Roadmap
Draft support is being rolled out per payment type. The table below shows the current status and planned timeline.
| Payment Type | Status | Details |
|---|---|---|
| Domestic Credit Transfer | Available | Fully supported |
| SE Bank Giro | Available | Fully supported |
| SE Plus Giro | Available | Fully supported |
| DK Payment Slip (GIRO/FI) | Planned | Further in the future |
Sandbox timing differs from production — see Sandbox Drafts & Signing Baskets.
Drafts
Drafts provide a staged payment preparation workflow. Instead of submitting a fully populated payment in a single request, you can:
- Create a draft —
nameis always required. Domestic credit transfer drafts also require a client-provideddraftId; SE Bank Giro and SE Plus Giro drafts may omitdraftId(the server generates one). - Update the draft incrementally as payment details become available
- Check availability — poll
GET /payments/draftsuntil the draft appears withisValid: true(why this matters) - Approve the draft through the
POST /payments/drafts/approveendpoint, which initiates the actual payment
Drafts do not have individual approve endpoints — all approval goes through the general Signing Baskets batch approval flow, even when approving a single draft. This is useful when payment details are collected across multiple steps or screens in your application.
Draft Lifecycle
Draft Endpoints
Each supported payment type has its own set of draft endpoints under /payments/drafts/{payment-type}/. The operations are the same across payment types, but the required fields and validation rules differ depending on the payment type (see the payment-type guides).
| Operation | Method | Path | Description |
|---|---|---|---|
| Create draft | POST | /payments/drafts/{payment-type} | Create a new draft |
| Get draft | GET | /payments/drafts/{payment-type}/{id} | Get draft details and validation status |
| Update draft | PATCH | /payments/drafts/{payment-type}/{id} | Partially update a draft |
| Delete draft | DELETE | /payments/drafts/{payment-type}/{id} | Delete a draft in CREATED status |
Drafts are approved through the Signing Baskets batch approval flow.
PATCH updates fields that are present in the request body with non-empty values; omitted fields are left unchanged. The exception is kidNumber on domestic credit transfer drafts: omit it or send null to keep the existing value, and send an empty string ("") to clear it.
All draft endpoints require the PSP_PI (Payment Initiation) scope.
Common Fields
Every draft has the following fields regardless of payment type:
| Field | Type | Required on Create | Description |
|---|---|---|---|
draftId | string | Varies | Client-provided draft identifier. Must be a valid UUID and must not already be used. Once used, a draftId cannot be reused — even after the draft has been deleted or executed. Required for domestic credit transfer drafts; optional for SE Giro drafts (server generates one if omitted). Conflicts return 409 DRAFT_ALREADY_EXISTS. |
name | string | Yes | Descriptive name for the draft (e.g. bill filename, receiver) |
The redirectUrl for Strong Customer Authentication is supplied on the batch approve request, not on the draft itself.
Additional fields are payment-type-specific — see the payment-type guides.
Draft Response
All draft endpoints return a response with this common structure:
| Field | Type | Always Present | Description |
|---|---|---|---|
draftId | string | Yes | Unique draft identifier |
status | string | Yes | Current draft status (see Draft Statuses) |
isValid | boolean | Yes | Whether the draft has all required fields for approval |
name | string | Yes | Descriptive name for the draft |
validationErrors | array | No | Present when isValid is false; describes why the draft isn’t approvable |
Payment-type-specific fields (e.g. giroNumber, amount) are included when set.
The validationErrors array currently contains a single aggregated entry — not a per-field breakdown:
| Field | Type | Description |
|---|---|---|
field | string | Always "draft" in the current API version |
code | string | Machine-readable validation error code (returned by domestic credit transfer drafts) |
message | string | Localized, human-readable reason the draft isn’t yet approvable (honors the X-Language header) |
Example — single missing field (a SE Giro draft with no giroNumber):
{
"draftId": "1f4a9c3e-2b7d-4e8a-9f10-6c8b3d5e7f01",
"status": "CREATED",
"isValid": false,
"name": "Invoice 123",
"validationErrors": [
{
"field": "draft",
"message": "Giro number is missing"
}
]
}Example — several fields missing (message is aggregated, not one entry per field):
{
"draftId": "1f4a9c3e-2b7d-4e8a-9f10-6c8b3d5e7f01",
"status": "CREATED",
"isValid": false,
"name": "Invoice 123",
"validationErrors": [
{
"field": "draft",
"message": "Several required fields are missing"
}
]
}Use code for programmatic branching. Use message for user-facing display —
it is localized via the X-Language header and the exact wording may change.
Domestic credit transfer drafts return code; other draft types may omit it
for now.
Draft Statuses
| Status | Description |
|---|---|
CREATED | Draft is being prepared and can be updated or deleted. The draft also stays in CREATED while a basket approval is awaiting Strong Customer Authentication, and returns here when a previous payment attempt failed — paymentError is populated and the draft can be retried. |
PENDING | Approval has succeeded and the resulting payment is in progress — covers awaiting clearing and awaiting settlement for future-dated payments. |
COMPLETED | Payment has settled successfully. |
DELETED | Draft has been deleted. |
A draft can only be updated or deleted while in CREATED status. If approval
or execution fails, the draft returns to CREATED status so it can be
corrected and re-approved.
When a payment fails after approval, the draft returns to an approvable state
and reappears in GET /payments/drafts with the paymentError field
populated. This field contains a localized error message describing why the
payment failed (honoring the X-Language header). The TPP can display this
error to the user and allow them to re-approve the draft.
Error Handling
All draft endpoints return errors in a consistent format:
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable error message |
errors | array | List of validation errors |
Common error codes:
| Status | Code | Description |
|---|---|---|
400 | INVALID_DRAFT_ID | The provided draftId is not a valid UUID |
400 | INVALID_EXECUTION_DATE | date is not in the expected format (YYYY-MM-DD or RFC3339) |
403 | ACCOUNT_ACCESS_DENIED | The caller does not have access to the senderAccountId supplied on create or update (see Sender Account Access) |
404 | NOT_FOUND | Draft not found |
409 | DRAFT_ALREADY_EXISTS | A draft with the supplied draftId already exists (including after delete — the ID remains reserved) |
409 | INVALID_STATE | Draft is not in the expected state for update, delete, or other lifecycle operations |
422 | Validation error in request body | |
500 | Internal server error |
Errors specific to the batch approve flow (ALREADY_USED, MISSING_DRAFT_IDS, MISSING_REDIRECT_URL, TRANSFER_DENIED) are documented on the Approve Drafts endpoint.
Sender Account Access
When senderAccountId is supplied on create (POST) or update (PATCH) for any draft type that supports it (domestic credit transfer and SE Giro), Lunar verifies that the caller has access to that account. Access is granted when any of the following holds:
- The caller is the direct owner of the account (private accounts).
- The caller is a party on the organisation that owns the account (organisation/business accounts).
- The caller has been granted explicit shared access to the account by its owner.
If none of these apply, the request is rejected with 403 ACCOUNT_ACCESS_DENIED and no change is made to the draft.
Automatic clearing of senderAccountId
A draft’s senderAccountId can become unusable after the draft is created — for example when the account is closed, ownership changes, or a shared-access grant is revoked. When this happens, Lunar automatically clears senderAccountId on affected drafts that are still in CREATED status. The draft reverts to isValid: false with a validation error indicating the sender account is missing, and will continue to appear in GET /payments/drafts so the TPP can prompt the user to pick a new sender account before re-approving.
| Event | Effect on drafts |
|---|---|
| Shared account access revoked | senderAccountId cleared on drafts owned by the user whose access was revoked |
| Account ownership changed | senderAccountId cleared on drafts owned by the former owner |
| Account closed | senderAccountId cleared on all drafts referencing the closed account |
Drafts that have already left CREATED status (PENDING, COMPLETED) are
not modified by these events — only drafts still being prepared are cleared.
Payment-Type Draft Guides
Drafts share the lifecycle and signing-basket approval flow described here. Choose a payment type for its fields, validation rules, and examples:
- Domestic Credit Transfer Drafts — national transfers in Denmark, Norway, and Sweden, including KID payments in Norway.
- SE Bank Giro Drafts — Swedish Bank Giro payments.
- SE Plus Giro Drafts — Swedish Plus Giro payments.
Signing Baskets (Batch Approval)
A signing basket lets you group multiple drafts — potentially across different payment types — and approve them all at once through a single authorization flow. This is particularly valuable when a user needs to pay several invoices in one go: each invoice is a separate draft, and the batch approval triggers a single authentication challenge for the entire set.
When mixing payment types in one basket, your application must track which payment type each draftId belongs to — neither GET /payments/drafts nor the approval status response includes payment type (see Get Approval Status).
Signing Basket Flow
List Drafts
GET /payments/draftsReturns draft summaries for the current user that are still approvable, across all payment types that support drafts (see the Payment Type Roadmap). Drafts in CREATED are returned — including drafts that are not yet valid (isValid: false) and drafts whose previous payment failed and are awaiting retry (paymentError set). Drafts in PENDING, COMPLETED, or DELETED are excluded.
Use this endpoint for discovery — showing the user which drafts can be selected for approval, and surfacing paymentError after a failed payment. Use GET /payments/drafts/{payment-type}/{id} when you need the full draft payload, field-level validation (validationErrors including code on domestic credit transfer drafts), or previousFailureReason on a specific draft.
| Endpoint | Best for |
|---|---|
GET /payments/drafts | List approvable drafts; invalidReason; paymentError |
GET /payments/drafts/{payment-type}/{id} | Full fields; validationErrors[].code (DCT); previousFailureReason (DCT) |
The list response does not include payment type — record the payment type when you create each draft. It exposes invalidReason when isValid is false, not the machine-readable validationErrors[].code available on the per-type GET response.
Pass the X-Language header (supported values: en, sv, da, nb) to receive localized values for invalidReason and paymentError, and for the fallback name when a draft was created without one. TPP-supplied name values are returned as-is. Defaults to en if omitted or unrecognized.
Response:
| Field | Type | Description |
|---|---|---|
drafts | array | List of draft summaries |
totalCount | integer | Number of drafts returned in this response. The endpoint returns all approvable drafts for the current user in one response — there is no pagination |
Each draft in the list contains:
| Field | Type | Description |
|---|---|---|
draftId | string | Unique draft identifier |
isValid | boolean | Whether the draft is ready for approval |
invalidReason | string | Localized reason why the draft is invalid (present when isValid is false) |
name | string | Descriptive name |
accountId | string | Associated account ID (if set) |
paymentError | string | Localized error from a failed payment attempt (present when a previous approval failed and the draft is re-approvable) |
Wait for draft availability
Draft creation and updates are processed through an event-based system. The draft list and signing-basket approval flow are eventually consistent with the create and update responses. A successful 201 Created — even with isValid: true — does not mean the new draft is already visible to the approval flow.
Before approving, poll GET /payments/drafts until every draftId in the intended basket appears in the list with isValid: true. Match by draftId; the list may include other drafts, including invalid ones. Use bounded retries with a delay or backoff rather than a fixed sleep or an unbounded loop. If the drafts do not appear, report that approval is not ready instead of submitting the batch.
If approval immediately after creation returns 404 NOT_FOUND, no drafts in that batch were approved. Recheck the list and submit a new approval request with a fresh requestId only after all selected drafts appear as valid. If an ID still does not appear after your retry window, verify the ID and caller’s access rather than treating every 404 as a propagation delay. Poll GET /payments/drafts/approve/{requestId}/status after approval, not to check whether a newly created draft is available.
Approve Drafts (Batch)
POST /payments/drafts/approveApproves and initiates execution of one or more drafts. A unique requestId is required to track the approval — it can be used to poll for status afterward. Before submitting, wait for each draft to appear in the list with isValid: true.
| Field | Type | Required | Description |
|---|---|---|---|
requestId | string | Yes | Unique UUID for this approval request |
draftIds | string[] | Yes | Array of draft IDs to approve |
redirectUrl | string | Yes | URL the PSU is redirected to after completing Strong Customer Authentication |
The request is validated before any approval is initiated. If any draft ID
in draftIds is not yet available to the approval flow (or does not exist),
the entire request is rejected with 404 NOT_FOUND. No drafts are approved in
this case — confirm each ID appears in GET /payments/drafts
with isValid: true before submitting the batch.
Response:
| Field | Type | Description |
|---|---|---|
requestId | string | The request ID |
approvedDraftIds | string[] | Draft IDs that were actually approved. May differ from the requested draftIds if some drafts were already approved or not in an approvable state. |
status | string | Overall approval status (see Approval Status) |
authUrl | string | Authorization URL the PSU should be redirected to (present when the approval requires an SCA challenge) |
When authUrl is present, the TPP can redirect the PSU to it to complete Strong Customer Authentication. After the challenge, the PSU is redirected back to the redirectUrl supplied on the approve request (see Post-SCA Redirect).
| Status | Code | Description |
|---|---|---|
400 | MISSING_DRAFT_IDS | No draft IDs provided |
400 | MISSING_REDIRECT_URL | redirectUrl is missing from the request body |
400 | TRANSFER_DENIED | Approval was denied (e.g. account restrictions) |
404 | NOT_FOUND | One or more drafts not yet available to approval or not found — entire request rejected, no drafts approved |
409 | INVALID_STATE | None of the drafts are in an approvable state — e.g. already approved/executing, or not yet valid for approval (isValid: false) |
409 | ALREADY_USED | The requestId has already been used for a previous approval request |
The INVALID_STATE and ALREADY_USED errors are both returned as 409 Conflict but with different error codes. ALREADY_USED includes a field: "requestId" in the validation errors array to indicate the source of the
conflict.
Post-SCA Redirect
The batch approval flow follows the same authorization pattern as single-payment endpoints: the TPP redirects the PSU to an authorization URL, the PSU completes Strong Customer Authentication, and the PSU is then redirected back to a TPP-owned URL.
The redirectUrl is scoped to the approval request — each batch approve supplies its own single URL that the PSU is sent back to after completing SCA for that batch.
authUrl is only present in the response when an SCA challenge is required.
If approval completes without a challenge, no redirect is needed and the
authUrl field is omitted.
Get Approval Status
GET /payments/drafts/approve/{requestId}/statusReturns the current status of a batch approval request, including per-draft progress.
Response:
| Field | Type | Description |
|---|---|---|
status | string | Overall approval status (see Approval Status) |
drafts | array | Per-draft status details |
Each entry in drafts contains:
| Field | Type | Description |
|---|---|---|
draftId | string | Draft identifier |
status | string | One of CREATED, PENDING, COMPLETED, FAILED (see Per-Draft Status) |
errorMessage | string | Localized error message (present when status is FAILED; honors X-Language) |
paymentId | string | Resulting payment ID (present when status is PENDING or COMPLETED) |
expectedExecutionDate | string | Date the executing service expects to post the payment, in ISO 8601 YYYY-MM-DD form. Present when status is PENDING and an execution date was supplied; omitted otherwise. |
Errors:
| Status | Code | Description |
|---|---|---|
404 | NOT_FOUND | No approval request exists for the given requestId |
The paymentId is first surfaced when a draft reaches PENDING and remains
on the response through COMPLETED. It can be used to look up the payment via
its corresponding payment endpoint (e.g. GET /payments/domestic-credit-transfer/ {paymentId} or GET /payments/se-bank-giro/{paymentId}). The approval status
response does not include the payment type — your application must track which
payment type each draft belongs to in order to query the correct endpoint.
Per-Draft Status
| Status | Description |
|---|---|
CREATED | Draft is queued for approval under this request — the PSU has not yet completed SCA. |
PENDING | Payment has been accepted for execution by the underlying payment service. paymentId is set; expectedExecutionDate may be set when the payment is scheduled for a future date. |
COMPLETED | Payment has settled successfully. |
FAILED | Payment could not be completed; errorMessage describes the reason. This entry stays FAILED — it does not revert to CREATED. The draft itself becomes approvable again (see Payment failure and cancellation); a retry uses a new requestId, whose own per-draft entry starts at CREATED independently of this one. |
Each entry in GET /payments/drafts/approve/{requestId}/status reflects only
what happened under that specific requestId — once a draft entry reaches
FAILED or COMPLETED for a request, it does not change again, even if the
same draft is later retried or paid through a different channel. To retry,
call POST /payments/drafts/approve again with a new requestId and poll its
own status separately. For the draft’s current, live state — including whether
it is approvable again after a failure — use GET /payments/drafts/{type}/ {id} or GET /payments/drafts, not the old
request’s status.
Approval Status
The overall approval status reflects the state of the authorization challenge:
| Status | Description |
|---|---|
AWAITING_APPROVAL | Challenge has been created; waiting for user authorization |
APPROVED | All drafts have been approved and payments initiated |
EXPIRED | The authorization challenge expired before completion |
DECLINED | The user declined the authorization challenge |
Cross-layer Status Mapping
0.6.0-beta aligned the three status vocabularies that show up across a single signing-basket flow so a TPP only needs to learn one. All three layers use the same uppercase values, with controlled collapsing where the layers don’t have a 1:1 correspondence.
- Draft (
DraftResponse.status) — the lifecycle of an individual draft (GET /payments/drafts/{type}/{id}). Values:CREATED,PENDING,COMPLETED,DELETED. The draft stays inCREATEDwhile the basket approval is awaiting SCA and only transitions toPENDINGonce approval has succeeded —PENDINGtherefore covers awaiting clearing and awaiting settlement for future-dated payments, never awaiting SCA. - Per-draft basket (
DraftApprovalStatus.status) — the per-draft entry inside a basket approval response (GET /payments/drafts/approve/{requestId}/status). Values:CREATED,PENDING,COMPLETED,FAILED. Mirrors the draft layer while the request is progressing, butFAILEDis a permanent outcome for that specificrequestId— it does not recycle back toCREATED. The draft itself becomes approvable again and can be retried, but only its own live view (GET /payments/drafts/{type}/{id}orGET /payments/drafts) reflects that; the retry is tracked under a newrequestIdwith its own independent status. - Basket overall (
ApprovalStatus) — the overall approval/SCA challenge status returned by bothPOST /payments/drafts/approveand the status endpoint. Values:AWAITING_APPROVAL,APPROVED,EXPIRED,DECLINED. Models the SCA challenge itself, not the resulting payments. - Underlying payment (e.g.
SeBankGiroResponse.status) — the eventual payment’s lifecycle, looked up via the payment-type-specific endpoint. Values:AWAITING_APPROVAL,PENDING,APPROVED,COMPLETED,FAILED,DECLINED,CANCELLED. The fully detailed view — drafts only surface the post-approval lifecycle (the underlying payment’sPENDINGandCOMPLETED);AWAITING_APPROVALandAPPROVEDon the payment never appear at the draft layer because the draft is only handed off for execution after the basket has already been approved. Same-day vs future-dated transitions and explicit cancellation surface only on the payment endpoint.