Map Stripe prices and products
A product mapping says which Stripe price or product grants which of your Sealcord products, and on what terms. Map each price or product you sell, in the Console on the product’s page, or with the Admin API. A checkout for a price and product without a mapping issues nothing, and Sealcord tells you.
Price or product
Section titled “Price or product”A mapping names a Stripe price (price_…) or a Stripe product (prod_…). When a checkout
arrives, Sealcord looks for a mapping for each line’s price first, then for its product, so a
price mapping wins.
- Map the product to cover every price it has, with one set of terms.
- Map a price when it needs terms of its own. One Stripe product often carries a monthly and a yearly price that should grant different things, such as a key that lasts a month and one that lasts a year.
Sealcord supports one mapped item per checkout. If a checkout has a second mapped item, Sealcord issues the licence for the first, notes the rest as ignored in the delivery and emails your owners and admins.
What a mapping holds
Section titled “What a mapping holds”| Field | Default | Meaning |
|---|---|---|
product_id |
required | Your Sealcord product, such as acme-notes |
limit_kind |
seats |
seats or machines (which) |
limit_count |
1 |
How many. Stripe’s line item quantity multiplies it: 5 bought at a count of 2 is 10 |
key_valid_days |
none | Put an expiry this many days (1 to 36500) after purchase in the key. None makes a perpetual key |
on_refund |
revoke |
revoke ends the licence on a full refund of its payment; none leaves it |
on_subscription_end |
expire |
expire or revoke, what happens when the subscription ends |
renewal_url |
none | Where a customer renews, one of your Stripe Payment Links (rules) |
A subscription’s key carries no expiry unless you set key_valid_days, because an embedded expiry
would force a new key at every renewal. The subscription’s end is enforced online, through the
verdict.
Map in the Console
Section titled “Map in the Console”Open the product, find its payment products, and choose to map one: pick the Stripe integration, paste the price or product id from Stripe, then set the terms and optionally the renewal link. Saving reads the price or product from your Stripe account, in the integration’s mode, so an id that is not there, or belongs to the other mode, is refused. Owners and admins can change mappings.
Map with the Admin API
Section titled “Map with the Admin API”With an admin API key that has payments:write:
curl -X PUT \ "https://api.sealcord.com/v1/admin/integrations/$INTEGRATION_ID/mappings/$STRIPE_PRICE_ID" \ -H "authorization: Bearer $SEALCORD_TOKEN" \ -H 'content-type: application/json' \ -d '{ "product_id": "acme-notes", "limit_kind": "machines", "limit_count": 2, "key_valid_days": null, "on_refund": "revoke", "on_subscription_end": "expire" }'// Node.js 18 or later, as an ES module (a .mjs file) for the top-level await.const token = process.env.SEALCORD_TOKEN;if (!token) throw new Error('SEALCORD_TOKEN is not set');const url = `https://api.sealcord.com/v1/admin/integrations/${process.env.INTEGRATION_ID}/mappings/${process.env.STRIPE_PRICE_ID}`;const response = await fetch(url, { method: 'PUT', headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json', }, body: JSON.stringify({ product_id: 'acme-notes', limit_kind: 'machines', limit_count: 2, key_valid_days: null, on_refund: 'revoke', on_subscription_end: 'expire', }),});const json = await response.json();if (!response.ok) { console.error(`HTTP ${response.status}`, json); process.exit(1);}console.log(json);// Cargo.toml: reqwest = { version = "0.13", features = ["blocking", "json"] }// serde_json = "1"use reqwest::blocking::Client;use serde_json::{json, Value};
fn main() -> Result<(), Box<dyn std::error::Error>> { let token = std::env::var("SEALCORD_TOKEN")?; let integration_id = std::env::var("INTEGRATION_ID")?; let stripe_price_id = std::env::var("STRIPE_PRICE_ID")?; let url = format!("https://api.sealcord.com/v1/admin/integrations/{integration_id}/mappings/{stripe_price_id}"); let response = Client::new() .put(url) .bearer_auth(token) .json(&json!({ "product_id": "acme-notes", "limit_kind": "machines", "limit_count": 2, "key_valid_days": null, "on_refund": "revoke", "on_subscription_end": "expire", })) .send()?; let status = response.status(); let body: Value = response.json()?; if !status.is_success() { return Err(format!("HTTP {status}: {body}").into()); } println!("{body:#}"); Ok(())}// Go 1.18 or later: go run main.go
package main
import ( "bytes" "encoding/json" "fmt" "log" "net/http" "os")
func main() { token := os.Getenv("SEALCORD_TOKEN") if token == "" { log.Fatal("SEALCORD_TOKEN is not set") } url := "https://api.sealcord.com/v1/admin/integrations/" + os.Getenv("INTEGRATION_ID") + "/mappings/" + os.Getenv("STRIPE_PRICE_ID") body, err := json.Marshal(map[string]any{ "product_id": "acme-notes", "limit_kind": "machines", "limit_count": 2, "key_valid_days": nil, "on_refund": "revoke", "on_subscription_end": "expire", }) if err != nil { log.Fatal(err) } req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(body)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var answer any if err := json.NewDecoder(res.Body).Decode(&answer); err != nil { log.Fatal(err) } out, _ := json.MarshalIndent(answer, "", " ") if res.StatusCode/100 != 2 { log.Fatalf("HTTP %d: %s", res.StatusCode, out) } fmt.Println(string(out))}// .NET 8 or later: the Program.cs of a console app (dotnet new console).using System.Net.Http.Headers;using System.Net.Http.Json;
var token = Environment.GetEnvironmentVariable("SEALCORD_TOKEN") ?? throw new InvalidOperationException("SEALCORD_TOKEN is not set");var integrationId = Environment.GetEnvironmentVariable("INTEGRATION_ID");var stripePriceId = Environment.GetEnvironmentVariable("STRIPE_PRICE_ID");var url = $"https://api.sealcord.com/v1/admin/integrations/{integrationId}/mappings/{stripePriceId}";
using var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await http.PutAsJsonAsync(url, new{ product_id = "acme-notes", limit_kind = "machines", limit_count = 2, key_valid_days = (int?)null, on_refund = "revoke", on_subscription_end = "expire",});var json = await response.Content.ReadAsStringAsync();if (!response.IsSuccessStatusCode){ Console.Error.WriteLine($"HTTP {(int)response.StatusCode}: {json}"); Environment.Exit(1);}Console.WriteLine(json);// Java 11 or later: java Main.javaimport java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;
public class Main { public static void main(String[] args) throws Exception { String token = System.getenv("SEALCORD_TOKEN"); if (token == null) { throw new IllegalStateException("SEALCORD_TOKEN is not set"); } String url = "https://api.sealcord.com/v1/admin/integrations/" + System.getenv("INTEGRATION_ID") + "/mappings/" + System.getenv("STRIPE_PRICE_ID"); String body = "{" + "\"product_id\": \"acme-notes\", " + "\"limit_kind\": \"machines\", " + "\"limit_count\": 2, " + "\"key_valid_days\": null, " + "\"on_refund\": \"revoke\", " + "\"on_subscription_end\": \"expire\"" + "}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("authorization", "Bearer " + token) .header("content-type", "application/json") .PUT(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() / 100 != 2) { System.err.println("HTTP " + response.statusCode() + ": " + response.body()); System.exit(1); } System.out.println(response.body()); }}$INTEGRATION_ID is the id the Console shows for the integration, or GET /v1/admin/integrations
lists (scope payments:read); $STRIPE_PRICE_ID is a price or product id from Stripe. The answer
is the mapping. GET /v1/admin/integrations/{id}/mappings lists an integration’s mappings and
DELETE on the same path as the PUT removes one (204).